From d1d9b7e6efb0c30bbfcc26564f06297ce3bc953e Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 3 Oct 2025 15:47:41 -0500 Subject: [PATCH 01/40] Update CUDA architectures to include additional support --- src/c/Cuda/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c/Cuda/CMakeLists.txt b/src/c/Cuda/CMakeLists.txt index cf419dd..cb40aed 100644 --- a/src/c/Cuda/CMakeLists.txt +++ b/src/c/Cuda/CMakeLists.txt @@ -2,7 +2,7 @@ option(USE_PROCESS_MUTEX "Use process-level mutex to guard GPU calls" OFF) # Common settings for both libraries -set(CUDA_ARCHITECTURES "52;61;70;75;86;89") +set(CUDA_ARCHITECTURES "75;86;89;120") set(COMMON_INCLUDES $ $ From 67e6410e1dcf7ddb756e963f422799c6f8eaaab0 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 17 Oct 2025 14:53:50 -0500 Subject: [PATCH 02/40] Use LLVM OpenMP on MSVC and apply compiler-specific OpenMP flags --- CMakeLists.txt | 16 +++++++++++++--- src/c/Cuda/CMakeLists.txt | 35 ++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index acb80ca..455486f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,15 +2,25 @@ cmake_minimum_required(VERSION 3.22) project(HydraImageProcessor LANGUAGES C CXX CUDA) -set(HYDRA_MODULE_NAME "HIP") +set(HYDRA_MODULE_NAME "Hydra") # Use CMake's modern FindCUDAToolkit module find_package(CUDAToolkit REQUIRED) + +# Find OpenMP - prefer LLVM implementation on MSVC for static linking +if(MSVC) + # Set flags before finding OpenMP to prefer LLVM implementation + set(OpenMP_C_FLAGS "/openmp:llvm" CACHE STRING "" FORCE) + set(OpenMP_CXX_FLAGS "/openmp:llvm" CACHE STRING "" FORCE) + set(OpenMP_C_LIB_NAMES "libomp" CACHE STRING "" FORCE) + set(OpenMP_CXX_LIB_NAMES "libomp" CACHE STRING "" FORCE) + set(OpenMP_libomp_LIBRARY "libomp" CACHE STRING "" FORCE) +endif() find_package(OpenMP) # Find optional dependencies find_package(Matlab COMPONENTS MAIN_PROGRAM OPTIONAL_COMPONENTS MEX_COMPILER) -find_package(Python COMPONENTS Development NumPy OPTIONAL) +find_package(Python3 COMPONENTS Development NumPy OPTIONAL) # Setup backend Hydra CUDA library (static) for CUDA building @@ -23,6 +33,6 @@ if (Matlab_FOUND) endif() # Setup Python interface if Python is found -if (Python_FOUND) +if (Python3_FOUND) add_subdirectory(src/c/Python) endif() diff --git a/src/c/Cuda/CMakeLists.txt b/src/c/Cuda/CMakeLists.txt index cb40aed..ff23ca1 100644 --- a/src/c/Cuda/CMakeLists.txt +++ b/src/c/Cuda/CMakeLists.txt @@ -78,12 +78,15 @@ set(COMMON_CPP_SOURCES # Determine the OpenMP flag based on the compiler if(MSVC) message(STATUS "MSVC compiler detected") - set(OPENMP_FLAG "/openmp") - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -Xcompiler=/openmp") + # Use LLVM OpenMP for better static linking support + # Check if /openmp:llvm is available (VS 2019 16.9+) + set(OPENMP_FLAG "/openmp:llvm") + set(OPENMP_STATIC_FLAG "/openmp:llvm") + message(STATUS "Using LLVM OpenMP for static linking") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") message(STATUS "GCC/Clang compiler detected") set(OPENMP_FLAG "-fopenmp") - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -Xcompiler=-fopenmp") + set(OPENMP_STATIC_FLAG "-fopenmp") else() message(FATAL_ERROR "Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}") endif() @@ -97,15 +100,25 @@ function(setup_cuda_library LIB_NAME STATIC_OR_SHARED) PUBLIC ${COMMON_INCLUDES} ) + # Link CUDA static runtime + target_link_libraries(${LIB_NAME} PRIVATE CUDA::cudart_static) + # Link OpenMP libraries - target_link_libraries(${LIB_NAME} PRIVATE CUDA::cudart_static PRIVATE OpenMP::OpenMP_CXX) - - # Set CUDA-specific compile options - # Pass the OpenMP flag to the host compiler via nvcc - target_compile_options(${LIB_NAME} PRIVATE - # $<$:${CUDA_NVCC_FLAGS}> - $<$:-Xcompiler=${OPENMP_FLAG}> - ) + if(MSVC) + # For MSVC with LLVM OpenMP, manually link the static library + target_link_libraries(${LIB_NAME} PRIVATE OpenMP::OpenMP_CXX) + # Add compiler flags for static OpenMP + target_compile_options(${LIB_NAME} PRIVATE + $<$:${OPENMP_STATIC_FLAG}> + $<$:-Xcompiler=${OPENMP_STATIC_FLAG}> + ) + else() + target_link_libraries(${LIB_NAME} PRIVATE OpenMP::OpenMP_CXX) + # Set CUDA-specific compile options for non-MSVC + target_compile_options(${LIB_NAME} PRIVATE + $<$:-Xcompiler=${OPENMP_FLAG}> + ) + endif() set_target_properties(${LIB_NAME} PROPERTIES CUDA_STANDARD 11 From 5ea04df0539a150f089ee581924bdd2a38cdb25e Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 17 Oct 2025 14:56:37 -0500 Subject: [PATCH 03/40] Rename HIP Python module to Hydra; use Python3 CMake targets; bundle OpenMP DLL; add Python README --- src/Python/README.md | 75 ++++++++++++++++++++++++++++++++++ src/c/Python/CMakeLists.txt | 30 ++++++++++++-- src/c/Python/HydraPyModule.cpp | 32 +++++++-------- src/c/Python/PyArgConverter.h | 6 +-- src/c/Python/PyIncludes.h | 2 +- 5 files changed, 122 insertions(+), 23 deletions(-) create mode 100644 src/Python/README.md diff --git a/src/Python/README.md b/src/Python/README.md new file mode 100644 index 0000000..ece535e --- /dev/null +++ b/src/Python/README.md @@ -0,0 +1,75 @@ +# Hydra Python Module + +This directory contains the Hydra Python extension module for GPU-accelerated image processing. + +## Files + +- **Hydra.pyd** - The Python extension module (Windows) +- **libomp140.x86_64.dll** - OpenMP runtime library (bundled for portability) +- **test_import.py** - Test script to verify the module imports correctly + +## Distribution + +To distribute this module to others, simply copy both files: +1. `Hydra.pyd` +2. `libomp140.x86_64.dll` + +Keep them in the same directory, and Python will automatically find the DLL when importing the module. + +## Requirements + +- Python 3.12 +- NumPy +- NVIDIA CUDA-capable GPU (for running the image processing operations) + +## Usage + +```python +import Hydra + +# Check available CUDA devices +num_devices = Hydra.DeviceCount() +print(f"Available CUDA devices: {num_devices}") + +# Get help on available functions +Hydra.Help() + +# Example: Apply Gaussian filter +# result = Hydra.Gaussian(input_array, sigma=1.5) +``` + +## Testing + +Run the test script to verify the module is working: + +```bash +python test_import.py +``` + +## Building from Source + +The module is built using CMake with the following key features: +- Linked against Python 3.12 +- Uses LLVM OpenMP (`/openmp:llvm`) for better performance +- Statically links CUDA runtime +- Automatically bundles the OpenMP DLL for portability + +To rebuild: + +```bash +cd ../.. # Go to project root +cmake --preset dev +cmake --build build --config Release --target HydraPy +``` + +The build system will automatically: +1. Find the correct Python 3.12 installation +2. Link against the hydra conda environment +3. Copy the OpenMP DLL to this directory + +## Notes + +- The module requires the OpenMP DLL at runtime. This is automatically bundled during the build process. +- The module was compiled against Python 3.12.11 and requires that specific Python version. +- CUDA runtime is statically linked, so no separate CUDA runtime installation is needed for basic import. +- However, NVIDIA GPU drivers must be installed to actually run GPU operations. diff --git a/src/c/Python/CMakeLists.txt b/src/c/Python/CMakeLists.txt index 8c86a71..e83f795 100644 --- a/src/c/Python/CMakeLists.txt +++ b/src/c/Python/CMakeLists.txt @@ -1,4 +1,4 @@ -# Setup MEX interface if Matlab was found +# Setup pyd interface if Python was found add_library(HydraPy MODULE "") # Require c++11 and set build definition to PY_BUILD @@ -11,7 +11,7 @@ if ( USE_PROCESS_MUTEX ) endif() # Link against Python and NumPy libraries -target_link_libraries(HydraPy PRIVATE HydraCudaStatic Python::Python Python::NumPy) +target_link_libraries(HydraPy PRIVATE HydraCudaStatic Python3::Python Python3::NumPy) # Change output library name to Hydra. set_target_properties(HydraPy @@ -25,10 +25,34 @@ set_target_properties(HydraPy # On windows specifically set the suffix to .pyd if ( WIN32 ) set_target_properties(HydraPy PROPERTIES SUFFIX .pyd) + + # Copy OpenMP DLL to the output directory for portability + # Try to find the libomp140.x86_64.dll + find_file(LIBOMP_DLL + NAMES libomp140.x86_64.dll + PATHS + "C:/Windows/System32" + "$ENV{SystemRoot}/System32" + "${CMAKE_CXX_COMPILER_DIR}" + "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Redist/MSVC/*/x64/Microsoft.VC143.OpenMP.LLVM" + NO_DEFAULT_PATH + ) + + if(LIBOMP_DLL) + message(STATUS "Found OpenMP DLL: ${LIBOMP_DLL}") + add_custom_command(TARGET HydraPy POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${LIBOMP_DLL}" + "${PROJECT_SOURCE_DIR}/src/Python/libomp140.x86_64.dll" + COMMENT "Copying OpenMP DLL to output directory for portability" + ) + else() + message(WARNING "Could not find libomp140.x86_64.dll - the module may not be portable") + endif() endif() # Setup Python/NumPy include directories -target_include_directories(HydraPy PRIVATE Python::Python Python::NumPy) +target_include_directories(HydraPy PRIVATE Python3::Python Python3::NumPy) # Setup src include directories target_include_directories(HydraPy diff --git a/src/c/Python/HydraPyModule.cpp b/src/c/Python/HydraPyModule.cpp index 4b7131d..912c13a 100644 --- a/src/c/Python/HydraPyModule.cpp +++ b/src/c/Python/HydraPyModule.cpp @@ -1,6 +1,6 @@ #include -// This define forces inclusion of numpy symbols only in the hip_module.cpp file +// This define forces inclusion of numpy symbols only in the Hydra_module.cpp file #define NUMPY_IMPORT_MODULE #include "ScriptCmds/ScriptIncludes.h" #include "ScriptCmds/HydraConfig.h" @@ -8,13 +8,13 @@ HYDRA_CONFIG_MODULE(); // Make this a unique pointer just in case init can be run more than once -static std::unique_ptr hip_methods = nullptr; -static std::unique_ptr hip_docstrs = nullptr; +static std::unique_ptr Hydra_methods = nullptr; +static std::unique_ptr Hydra_docstrs = nullptr; -static struct PyModuleDef hip_moduledef = +static struct PyModuleDef Hydra_moduledef = { PyModuleDef_HEAD_INIT, - "HIP", + "Hydra", PyDoc_STR("Python wrappers for the Hydra Image Processing Library."), -1, nullptr @@ -22,12 +22,12 @@ static struct PyModuleDef hip_moduledef = // Main python module initialization entry point -MODULE_INIT_FUNC(HIP) +MODULE_INIT_FUNC(Hydra) { ScriptCommand::CommandList cmds = ScriptCommand::commands(); - hip_methods = std::unique_ptr(new PyMethodDef[cmds.size()+1]); - hip_docstrs = std::unique_ptr(new std::string[cmds.size()]); + Hydra_methods = std::unique_ptr(new PyMethodDef[cmds.size()+1]); + Hydra_docstrs = std::unique_ptr(new std::string[cmds.size()]); ScriptCommand::CommandList::const_iterator it = cmds.cbegin(); for ( int i=0; it != cmds.cend(); ++it, ++i ) @@ -35,24 +35,24 @@ MODULE_INIT_FUNC(HIP) const ScriptCommand::FuncPtrs& cmdFuncs = it->second; const char* cmdName = it->first.c_str(); - hip_docstrs[i] = cmdFuncs.help(); + Hydra_docstrs[i] = cmdFuncs.help(); - hip_methods[i] = {cmdName,cmdFuncs.dispatch, - METH_VARARGS, PyDoc_STR(hip_docstrs[i].c_str()) }; + Hydra_methods[i] = {cmdName,cmdFuncs.dispatch, + METH_VARARGS, PyDoc_STR(Hydra_docstrs[i].c_str()) }; } // Methods list must end with null element - hip_methods[cmds.size()] ={ nullptr, nullptr, 0, nullptr }; + Hydra_methods[cmds.size()] ={ nullptr, nullptr, 0, nullptr }; - hip_moduledef.m_methods = hip_methods.get(); + Hydra_moduledef.m_methods = Hydra_methods.get(); - PyObject* hip_module = PyModule_Create(&hip_moduledef); - if ( !hip_module ) + PyObject* hydra_module = PyModule_Create(&Hydra_moduledef); + if ( !hydra_module ) return nullptr; // Support for numpy arrays import_array(); - return hip_module; + return hydra_module; } diff --git a/src/c/Python/PyArgConverter.h b/src/c/Python/PyArgConverter.h index 3fe7c2b..75fd715 100644 --- a/src/c/Python/PyArgConverter.h +++ b/src/c/Python/PyArgConverter.h @@ -84,7 +84,7 @@ namespace Script template static Script::ObjectType* store_out(const std::tuple& outputs) { - static_assert(sizeof... (ScrOuts) == N, "HIP_COMPILE: Output argument selector size mismatch"); + static_assert(sizeof... (ScrOuts) == N, "Hydra_COMPILE: Output argument selector size mismatch"); Script::ObjectType* scriptOut = PyTuple_New(N); store_out_impl(scriptOut, outputs, mph::make_index_sequence{}); @@ -99,7 +99,7 @@ namespace Script template static Script::ObjectType* store_out(const std::tuple& outputs) { - static_assert(sizeof... (ScrOuts) == 1, "HIP_COMPILE: Output argument selector size mismatch"); + static_assert(sizeof... (ScrOuts) == 1, "Hydra_COMPILE: Output argument selector size mismatch"); return reinterpret_cast(Script::unwrap_script_out(std::get<0>(outputs))); } }; @@ -110,7 +110,7 @@ namespace Script template static Script::ObjectType* store_out(const std::tuple& outputs) { - static_assert(sizeof... (ScrOuts) == 0, "HIP_COMPILE: Output argument selector size mismatch"); + static_assert(sizeof... (ScrOuts) == 0, "Hydra_COMPILE: Output argument selector size mismatch"); Py_RETURN_NONE; } }; diff --git a/src/c/Python/PyIncludes.h b/src/c/Python/PyIncludes.h index 0f65a29..ebda1a2 100644 --- a/src/c/Python/PyIncludes.h +++ b/src/c/Python/PyIncludes.h @@ -8,7 +8,7 @@ #define NO_IMPORT_ARRAY #endif -#define PY_ARRAY_UNIQUE_SYMBOL HIP_ARRAY_API +#define PY_ARRAY_UNIQUE_SYMBOL Hydra_ARRAY_API #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include From 090235dffb6f546782219ffad01b1bd14c229a59 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 17 Oct 2025 14:56:52 -0500 Subject: [PATCH 04/40] Add src/Python/hydra_image_processor.egg-info to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7e5b4b1..faa1ca8 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ CmakeCache.txt out .vs CMakeSettings.json +src/Python/hydra_image_processor.egg-info From 984b746ab0067f2743db3d1bbb52b278810020cd Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 17 Oct 2025 14:59:06 -0500 Subject: [PATCH 05/40] Update src/Python/environment.yml: pin package versions, add tooling deps, pip packages and environment prefix --- src/Python/environment.yml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Python/environment.yml b/src/Python/environment.yml index 8c6d2aa..1f0b6f9 100644 --- a/src/Python/environment.yml +++ b/src/Python/environment.yml @@ -1,9 +1,21 @@ name: hydra channels: - conda-forge - - defaults dependencies: - - python=3.9 - - scikit-image - - matplotlib - - numpy + - cmake=4.1.2 + - ipython=9.6.0 + - libpython=2.3 + - matplotlib=3.10.6 + - ninja=1.13.1 + - numpy=2.3.3 + - pip=25.2 + - pkg-config=0.29.2 + - python=3.12 + - python-devtools=0.12.2 + - scikit-image=0.25.2 + - pip: + - e==1.4.5 + - hydra-image-processor==0.1.0 + - pefile==2024.8.26 + +prefix: "D:\\mamba_envs\\hydra" From 6e4022a3d25f60a661c6590b870dc5a0d2149cc5 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 17 Oct 2025 16:29:38 -0500 Subject: [PATCH 06/40] Fix CUDA error handling: actually throw exceptions on ROI send failures and invalid kernel/copy cases; use cudaDeviceSynchronize in debug check --- src/c/Cuda/CudaAddTwoImages.cuh | 4 ++-- src/c/Cuda/CudaClosure.cuh | 2 +- src/c/Cuda/CudaElementWiseDifference.cuh | 4 ++-- src/c/Cuda/CudaEntropyFilter.cuh | 2 +- src/c/Cuda/CudaGaussian.cuh | 2 +- src/c/Cuda/CudaHighPassFilter.cuh | 2 +- src/c/Cuda/CudaIdentityFilter.cuh | 2 +- src/c/Cuda/CudaLoG.cuh | 6 +++--- src/c/Cuda/CudaMaxFilter.cuh | 2 +- src/c/Cuda/CudaMeanAndVariance.cuh | 2 +- src/c/Cuda/CudaMeanFilter.cuh | 2 +- src/c/Cuda/CudaMedianFilter.cuh | 2 +- src/c/Cuda/CudaMinFilter.cuh | 2 +- src/c/Cuda/CudaMinMax.cuh | 2 +- src/c/Cuda/CudaMultiplySum.cuh | 2 +- src/c/Cuda/CudaNLMeans.cuh | 2 +- src/c/Cuda/CudaOpener.cuh | 2 +- src/c/Cuda/CudaStdFilter.cuh | 2 +- src/c/Cuda/CudaSum.cuh | 2 +- src/c/Cuda/CudaUtilities.h | 2 +- src/c/Cuda/CudaVarFilter.cuh | 2 +- src/c/Cuda/CudaWienerFilter.cuh | 2 +- src/c/Cuda/ImageChunk.h | 2 +- src/c/Cuda/Kernel.cu | 2 +- src/c/Cuda/_TemplateKernel.cuh | 2 +- 25 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/c/Cuda/CudaAddTwoImages.cuh b/src/c/Cuda/CudaAddTwoImages.cuh index ef8df26..9e8c149 100644 --- a/src/c/Cuda/CudaAddTwoImages.cuh +++ b/src/c/Cuda/CudaAddTwoImages.cuh @@ -59,10 +59,10 @@ void cAddTwoImages(ImageView imageIn1, ImageView ima for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn1, deviceIn1.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); if (!chunks[i].sendROI(imageIn2, deviceIn2.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceIn1.setAllDims(chunks[i].getFullChunkSize()); deviceIn2.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaClosure.cuh b/src/c/Cuda/CudaClosure.cuh index b17edc4..e90bbee 100644 --- a/src/c/Cuda/CudaClosure.cuh +++ b/src/c/Cuda/CudaClosure.cuh @@ -44,7 +44,7 @@ void cClosure(ImageView imageIn, ImageView imageOut, for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaElementWiseDifference.cuh b/src/c/Cuda/CudaElementWiseDifference.cuh index 723114a..98a89cc 100644 --- a/src/c/Cuda/CudaElementWiseDifference.cuh +++ b/src/c/Cuda/CudaElementWiseDifference.cuh @@ -59,9 +59,9 @@ void cElementWiseDifference(ImageView image1In, ImageView imageIn, ImageView imageOut, I for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaGaussian.cuh b/src/c/Cuda/CudaGaussian.cuh index 50fd49b..c964531 100644 --- a/src/c/Cuda/CudaGaussian.cuh +++ b/src/c/Cuda/CudaGaussian.cuh @@ -50,7 +50,7 @@ void cGaussian(ImageView imageIn, ImageView imageOut, for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaHighPassFilter.cuh b/src/c/Cuda/CudaHighPassFilter.cuh index 1c20128..16164f9 100644 --- a/src/c/Cuda/CudaHighPassFilter.cuh +++ b/src/c/Cuda/CudaHighPassFilter.cuh @@ -51,7 +51,7 @@ void cHighPassFilter(ImageView imageIn, ImageView ima for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImagesIn.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImagesIn.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaIdentityFilter.cuh b/src/c/Cuda/CudaIdentityFilter.cuh index 3e5be06..55dbd43 100644 --- a/src/c/Cuda/CudaIdentityFilter.cuh +++ b/src/c/Cuda/CudaIdentityFilter.cuh @@ -55,7 +55,7 @@ void cIdentityFilter(ImageView imageIn, ImageView ima for ( int i = CUDA_IDX; i < chunks.size(); i += N_THREADS ) { if ( !chunks[i].sendROI(imageIn, deviceImages.getCurBuffer()) ) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaLoG.cuh b/src/c/Cuda/CudaLoG.cuh index 446e417..af3127a 100644 --- a/src/c/Cuda/CudaLoG.cuh +++ b/src/c/Cuda/CudaLoG.cuh @@ -69,7 +69,7 @@ void cLoG(ImageView imageIn, ImageView imageOut, Vec if (sigmas.x!=0) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); cudaMultiplySumBias<<> > (*(deviceImages.getCurBuffer()), *(deviceImages.getNextBuffer()), constLoGKernelMem_x, MIN_VAL, MAX_VAL, constGausKernelMem_x, true); deviceImages.incrementBuffer(); @@ -91,7 +91,7 @@ void cLoG(ImageView imageIn, ImageView imageOut, Vec if (sigmas.y!=0) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); if (sigmas.x!=0) { @@ -113,7 +113,7 @@ void cLoG(ImageView imageIn, ImageView imageOut, Vec if (sigmas.z!=0) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); if (sigmas.x!=0) { diff --git a/src/c/Cuda/CudaMaxFilter.cuh b/src/c/Cuda/CudaMaxFilter.cuh index c72e981..eac1381 100644 --- a/src/c/Cuda/CudaMaxFilter.cuh +++ b/src/c/Cuda/CudaMaxFilter.cuh @@ -71,7 +71,7 @@ void cMaxFilter(ImageView imageIn, ImageView imageOut for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaMeanAndVariance.cuh b/src/c/Cuda/CudaMeanAndVariance.cuh index ba088cd..95c3871 100644 --- a/src/c/Cuda/CudaMeanAndVariance.cuh +++ b/src/c/Cuda/CudaMeanAndVariance.cuh @@ -129,7 +129,7 @@ void cMeanAndVariance(ImageView imageIn, ImageView mu for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaMeanFilter.cuh b/src/c/Cuda/CudaMeanFilter.cuh index 1ada559..434ee96 100644 --- a/src/c/Cuda/CudaMeanFilter.cuh +++ b/src/c/Cuda/CudaMeanFilter.cuh @@ -60,7 +60,7 @@ void cMeanFilter(ImageView imageIn, ImageView imageOu for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaMedianFilter.cuh b/src/c/Cuda/CudaMedianFilter.cuh index d534330..e89353b 100644 --- a/src/c/Cuda/CudaMedianFilter.cuh +++ b/src/c/Cuda/CudaMedianFilter.cuh @@ -160,7 +160,7 @@ void cMedianFilter(ImageView imageIn, ImageView image for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaMinFilter.cuh b/src/c/Cuda/CudaMinFilter.cuh index 498602e..207c893 100644 --- a/src/c/Cuda/CudaMinFilter.cuh +++ b/src/c/Cuda/CudaMinFilter.cuh @@ -71,7 +71,7 @@ void cMinFilter(ImageView imageIn, ImageView imageOut for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaMinMax.cuh b/src/c/Cuda/CudaMinMax.cuh index 059adbf..beaccfb 100644 --- a/src/c/Cuda/CudaMinMax.cuh +++ b/src/c/Cuda/CudaMinMax.cuh @@ -204,7 +204,7 @@ void cMinMax(ImageView imageIn, PixelType& outMin, PixelType& outMax, for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaMultiplySum.cuh b/src/c/Cuda/CudaMultiplySum.cuh index 93fd830..f8da1e8 100644 --- a/src/c/Cuda/CudaMultiplySum.cuh +++ b/src/c/Cuda/CudaMultiplySum.cuh @@ -126,7 +126,7 @@ void cMultiplySum(ImageView imageIn, ImageView imageO for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaNLMeans.cuh b/src/c/Cuda/CudaNLMeans.cuh index cd3e124..5b538c4 100644 --- a/src/c/Cuda/CudaNLMeans.cuh +++ b/src/c/Cuda/CudaNLMeans.cuh @@ -175,7 +175,7 @@ void cNLMeans(ImageView imageIn, ImageView imageOut, for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaOpener.cuh b/src/c/Cuda/CudaOpener.cuh index 46a94be..120a057 100644 --- a/src/c/Cuda/CudaOpener.cuh +++ b/src/c/Cuda/CudaOpener.cuh @@ -44,7 +44,7 @@ void cOpener(ImageView imageIn, ImageView imageOut, I for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaStdFilter.cuh b/src/c/Cuda/CudaStdFilter.cuh index 2e4758d..6e10d6b 100644 --- a/src/c/Cuda/CudaStdFilter.cuh +++ b/src/c/Cuda/CudaStdFilter.cuh @@ -61,7 +61,7 @@ void cStdFilter(ImageView imageIn, ImageView imageOut for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaSum.cuh b/src/c/Cuda/CudaSum.cuh index 02218b3..2cbfd99 100644 --- a/src/c/Cuda/CudaSum.cuh +++ b/src/c/Cuda/CudaSum.cuh @@ -150,7 +150,7 @@ void cSum(ImageView imageIn, OutType& outVal, int device=-1) for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaUtilities.h b/src/c/Cuda/CudaUtilities.h index 446efb3..b56d7fd 100644 --- a/src/c/Cuda/CudaUtilities.h +++ b/src/c/Cuda/CudaUtilities.h @@ -44,7 +44,7 @@ static void HandleError( cudaError_t err, const char *file, int line ) #define HANDLE_ERROR( err ) (HandleError( err, __FILE__, __LINE__ )) #ifdef _DEBUG -#define DEBUG_KERNEL_CHECK() { cudaThreadSynchronize(); HandleError( cudaPeekAtLastError(), __FILE__, __LINE__ ); } +#define DEBUG_KERNEL_CHECK() { cudaDeviceSynchronize(); HandleError( cudaPeekAtLastError(), __FILE__, __LINE__ ); } #else #define DEBUG_KERNEL_CHECK() {} #endif // _DEBUG diff --git a/src/c/Cuda/CudaVarFilter.cuh b/src/c/Cuda/CudaVarFilter.cuh index 282c448..9b1f195 100644 --- a/src/c/Cuda/CudaVarFilter.cuh +++ b/src/c/Cuda/CudaVarFilter.cuh @@ -60,7 +60,7 @@ void cVarFilter(ImageView imageIn, ImageView imageOut for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/CudaWienerFilter.cuh b/src/c/Cuda/CudaWienerFilter.cuh index cc9d8aa..ec48a43 100644 --- a/src/c/Cuda/CudaWienerFilter.cuh +++ b/src/c/Cuda/CudaWienerFilter.cuh @@ -63,7 +63,7 @@ void cWienerFilter(ImageView imageIn, ImageView image for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); diff --git a/src/c/Cuda/ImageChunk.h b/src/c/Cuda/ImageChunk.h index 9da0bd5..6db109a 100644 --- a/src/c/Cuda/ImageChunk.h +++ b/src/c/Cuda/ImageChunk.h @@ -33,7 +33,7 @@ class ImageChunk } else { - std::runtime_error("This copy direction is not supported!"); + throw std::runtime_error("This copy direction is not supported!"); } } diff --git a/src/c/Cuda/Kernel.cu b/src/c/Cuda/Kernel.cu index 0d35928..8c4cad4 100644 --- a/src/c/Cuda/Kernel.cu +++ b/src/c/Cuda/Kernel.cu @@ -119,7 +119,7 @@ __host__ Kernel& Kernel::getOffsetCopy(Vec dimensions, std::size_t kernOut->init(); if (dims.product() < startOffset + dimensions.product()) - std::runtime_error("Trying to make a Kernel that access outside of the original memory space!"); + throw std::runtime_error("Trying to make a Kernel that access outside of the original memory space!"); kernOut->dims = dimensions; kernOut->cudaKernel = cudaKernel + startOffset; diff --git a/src/c/Cuda/_TemplateKernel.cuh b/src/c/Cuda/_TemplateKernel.cuh index 8f9dead..9b523a1 100644 --- a/src/c/Cuda/_TemplateKernel.cuh +++ b/src/c/Cuda/_TemplateKernel.cuh @@ -72,7 +72,7 @@ void cFooFilter(ImageView imageIn, ImageView imageOut for (int i = CUDA_IDX; i < chunks.size(); i += N_THREADS) { if (!chunks[i].sendROI(imageIn, deviceImages.getCurBuffer())) - std::runtime_error("Error sending ROI to device!"); + throw std::runtime_error("Error sending ROI to device!"); deviceImages.setAllDims(chunks[i].getFullChunkSize()); From 728f8676b5018c488faaf15ff22a5d0fec546f7c Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 17 Oct 2025 17:18:55 -0500 Subject: [PATCH 07/40] Restore default OpenMP detection on MSVC and remove obsolete build scripts/docs --- CMakeLists.txt | 10 +--------- build-linux/build.md | 3 --- build-win32/build.bat | 2 -- build-win32/build.md | 3 --- 4 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 build-linux/build.md delete mode 100644 build-win32/build.bat delete mode 100644 build-win32/build.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 455486f..caceafe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,15 +7,7 @@ set(HYDRA_MODULE_NAME "Hydra") # Use CMake's modern FindCUDAToolkit module find_package(CUDAToolkit REQUIRED) -# Find OpenMP - prefer LLVM implementation on MSVC for static linking -if(MSVC) - # Set flags before finding OpenMP to prefer LLVM implementation - set(OpenMP_C_FLAGS "/openmp:llvm" CACHE STRING "" FORCE) - set(OpenMP_CXX_FLAGS "/openmp:llvm" CACHE STRING "" FORCE) - set(OpenMP_C_LIB_NAMES "libomp" CACHE STRING "" FORCE) - set(OpenMP_CXX_LIB_NAMES "libomp" CACHE STRING "" FORCE) - set(OpenMP_libomp_LIBRARY "libomp" CACHE STRING "" FORCE) -endif() +# Find OpenMP - use default MSVC OpenMP find_package(OpenMP) # Find optional dependencies diff --git a/build-linux/build.md b/build-linux/build.md deleted file mode 100644 index cd53a99..0000000 --- a/build-linux/build.md +++ /dev/null @@ -1,3 +0,0 @@ -In Linux Run: -cmake .. -cmake --build . diff --git a/build-win32/build.bat b/build-win32/build.bat deleted file mode 100644 index 47b060f..0000000 --- a/build-win32/build.bat +++ /dev/null @@ -1,2 +0,0 @@ -cmake -G"Ninja" .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -cmake --build . --config Release diff --git a/build-win32/build.md b/build-win32/build.md deleted file mode 100644 index c7e3856..0000000 --- a/build-win32/build.md +++ /dev/null @@ -1,3 +0,0 @@ -In Windows Run (Use the appropriate generator for your version of msvc): -cmake -G"Visual Studio 15 2017 Win64" .. -cmake --build . --config Release From 9e6f246968d8096af01ca415b454cce89f7b2b01 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 17 Oct 2025 17:19:46 -0500 Subject: [PATCH 08/40] Restore MSVC default OpenMP flag and simplify OpenMP linking; move Python module output and remove libomp DLL copy --- src/c/Cuda/CMakeLists.txt | 22 ++++++---------------- src/c/Python/CMakeLists.txt | 26 +------------------------- 2 files changed, 7 insertions(+), 41 deletions(-) diff --git a/src/c/Cuda/CMakeLists.txt b/src/c/Cuda/CMakeLists.txt index ff23ca1..4304dae 100644 --- a/src/c/Cuda/CMakeLists.txt +++ b/src/c/Cuda/CMakeLists.txt @@ -78,15 +78,12 @@ set(COMMON_CPP_SOURCES # Determine the OpenMP flag based on the compiler if(MSVC) message(STATUS "MSVC compiler detected") - # Use LLVM OpenMP for better static linking support - # Check if /openmp:llvm is available (VS 2019 16.9+) - set(OPENMP_FLAG "/openmp:llvm") - set(OPENMP_STATIC_FLAG "/openmp:llvm") - message(STATUS "Using LLVM OpenMP for static linking") + # Use default MSVC OpenMP (vcomp) + set(OPENMP_FLAG "/openmp") + message(STATUS "Using MSVC OpenMP (vcomp)") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") message(STATUS "GCC/Clang compiler detected") set(OPENMP_FLAG "-fopenmp") - set(OPENMP_STATIC_FLAG "-fopenmp") else() message(FATAL_ERROR "Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}") endif() @@ -104,18 +101,11 @@ function(setup_cuda_library LIB_NAME STATIC_OR_SHARED) target_link_libraries(${LIB_NAME} PRIVATE CUDA::cudart_static) # Link OpenMP libraries - if(MSVC) - # For MSVC with LLVM OpenMP, manually link the static library + if(OpenMP_CXX_FOUND) target_link_libraries(${LIB_NAME} PRIVATE OpenMP::OpenMP_CXX) - # Add compiler flags for static OpenMP - target_compile_options(${LIB_NAME} PRIVATE - $<$:${OPENMP_STATIC_FLAG}> - $<$:-Xcompiler=${OPENMP_STATIC_FLAG}> - ) - else() - target_link_libraries(${LIB_NAME} PRIVATE OpenMP::OpenMP_CXX) - # Set CUDA-specific compile options for non-MSVC + # Set CUDA-specific compile options target_compile_options(${LIB_NAME} PRIVATE + $<$:${OPENMP_FLAG}> $<$:-Xcompiler=${OPENMP_FLAG}> ) endif() diff --git a/src/c/Python/CMakeLists.txt b/src/c/Python/CMakeLists.txt index e83f795..56ad62c 100644 --- a/src/c/Python/CMakeLists.txt +++ b/src/c/Python/CMakeLists.txt @@ -19,36 +19,12 @@ set_target_properties(HydraPy OUTPUT_NAME ${HYDRA_MODULE_NAME} PREFIX "" POSITION_INDEPENDENT_CODE ON - LIBRARY_OUTPUT_DIRECTORY $ + LIBRARY_OUTPUT_DIRECTORY $ ) # On windows specifically set the suffix to .pyd if ( WIN32 ) set_target_properties(HydraPy PROPERTIES SUFFIX .pyd) - - # Copy OpenMP DLL to the output directory for portability - # Try to find the libomp140.x86_64.dll - find_file(LIBOMP_DLL - NAMES libomp140.x86_64.dll - PATHS - "C:/Windows/System32" - "$ENV{SystemRoot}/System32" - "${CMAKE_CXX_COMPILER_DIR}" - "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Redist/MSVC/*/x64/Microsoft.VC143.OpenMP.LLVM" - NO_DEFAULT_PATH - ) - - if(LIBOMP_DLL) - message(STATUS "Found OpenMP DLL: ${LIBOMP_DLL}") - add_custom_command(TARGET HydraPy POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${LIBOMP_DLL}" - "${PROJECT_SOURCE_DIR}/src/Python/libomp140.x86_64.dll" - COMMENT "Copying OpenMP DLL to output directory for portability" - ) - else() - message(WARNING "Could not find libomp140.x86_64.dll - the module may not be portable") - endif() endif() # Setup Python/NumPy include directories From 949f0cf9f9d1063dcedbf9d11297553511763d3f Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:12:00 -0600 Subject: [PATCH 09/40] docs: update nomenclature from HIP to Hydra in core documentation --- CONTRIBUTING.md | 10 +++++----- readme.md | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31d8940..8bdde87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,15 +1,15 @@ -# Extending HIP is easy -Hydra Image Processor (HIP) has been written to expedite the addition of functionality. All of the machinery needed to distribute data and iterate over neighborhoods is included. Adding a new function is as easy as copying a template file and replacing the requisite lines of code. This paradigm is intended to encourage research and development of operations that would otherwise be intractable without hardware acceleration. +# Extending Hydra is easy +Hydra Image Processor (Hydra) has been written to expedite the addition of functionality. All of the machinery needed to distribute data and iterate over neighborhoods is included. Adding a new function is as easy as copying a template file and replacing the requisite lines of code. This paradigm is intended to encourage research and development of operations that would otherwise be intractable without hardware acceleration. # How to contribute Contribution is as easy as a pull request on GitHub. Following the guidelines will ensure requests are not rejected for minor issues. Once your code conforms to the guidelines, please submit your requests [here](https://github.com/ericwait/hydra-image-processor/pulls). -Alternatively, you can contribute by detailing your needs on this [forum](https://www.hydraimageprocessor.com/forum/request-functionality). HIP was also designed to be a practical library that meets the needs of microscopist. I really enjoy when theory and application meet. By explaining in detail (examples are always helpful as well) what your needs are, the community will be able to find novel ways accomplish your goals. +Alternatively, you can contribute by detailing your needs on this [forum](https://www.hydraimageprocessor.com/forum/request-functionality). Hydra was also designed to be a practical library that meets the needs of microscopist. I really enjoy when theory and application meet. By explaining in detail (examples are always helpful as well) what your needs are, the community will be able to find novel ways accomplish your goals. # Guidelines ## Code is not correct until it is clean -From the very beginning HIP was written to be clean, consistent, and as clear as possible. The use of object oriented programming and templates have made this project quickly extensible as well as maintainable. Each portion of code, down to the lowest operation, should be as "_glanceable_" as possible. Meaning that use of spacing, letter case, and naming scheme should be as information dense as possible. It all boils down to, make code that assists the reader in their understanding of what it is intended to accomplish. +From the very beginning Hydra was written to be clean, consistent, and as clear as possible. The use of object oriented programming and templates have made this project quickly extensible as well as maintainable. Each portion of code, down to the lowest operation, should be as "_glanceable_" as possible. Meaning that use of spacing, letter case, and naming scheme should be as information dense as possible. It all boils down to, make code that assists the reader in their understanding of what it is intended to accomplish. ## Main points to follow @@ -17,6 +17,6 @@ From the very beginning HIP was written to be clean, consistent, and as clear as 1. Do not duplicate functionality. Use existing functions/classes when possible. Consider extending existing functionality before creating new. 1. Use templates to ensure code maintainability. 1. Mimic what as already been done. If code looks inconsistent or "out of place." Correct it or bring it to the community's attention. -1. _**Try crazy things**_! HIP was built to quickly get operations onto GPU hardware. Use this opportunity to create things that would not otherwise be tractable. +1. _**Try crazy things**_! Hydra was built to quickly get operations onto GPU hardware. Use this opportunity to create things that would not otherwise be tractable. ### These guidelines are intended for GitHub pull requests. Do not let them be a barrier to experimentation. Have FUN! diff --git a/readme.md b/readme.md index 7960da8..df12f97 100644 --- a/readme.md +++ b/readme.md @@ -1,9 +1,9 @@ -# Hydra Image Processor (HIP) +# Hydra Image Processor (Hydra) Check out the website at [https://www.hydraimageprocessor.com](https://www.hydraimageprocessor.com) Hydra Image Processor is a hardware accelerated signal processing library written with [CUDA](https://developer.nvidia.com/cuda-zone). -HIP aims to create a signal processing library that can be incorporated into many software tools. +Hydra aims to create a signal processing library that can be incorporated into many software tools. This library is licensed under BSD 3-Clause to encourage use in open-source and commercial software. My only plea is that if you find bugs or make changes that you contribute them back to this repository. Happy processing and enjoy! @@ -14,4 +14,4 @@ Happy processing and enjoy! ## Feedback -If you would like to provide feedback about this tutorial or HIP in general, please use the forum [here](https://www.hydraimageprocessor.com/forum). +If you would like to provide feedback about this tutorial or Hydra in general, please use the forum [here](https://www.hydraimageprocessor.com/forum). From 8f1c12485e48f709a635049f2f7a532503b0e1bc Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:12:08 -0600 Subject: [PATCH 10/40] feat: add conda packaging recipe and build infrastructure --- .condaignore | 7 +++++++ recipe/bld.bat | 31 ++++++++++++++++++++++++++++ recipe/build.sh | 18 ++++++++++++++++ recipe/meta.yaml | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 .condaignore create mode 100644 recipe/bld.bat create mode 100644 recipe/build.sh create mode 100644 recipe/meta.yaml diff --git a/.condaignore b/.condaignore new file mode 100644 index 0000000..f7c541a --- /dev/null +++ b/.condaignore @@ -0,0 +1,7 @@ +build/ +out/ +.vscode/ +.claude/ +*.mltbx +nul +*.reg diff --git a/recipe/bld.bat b/recipe/bld.bat new file mode 100644 index 0000000..9c4fd08 --- /dev/null +++ b/recipe/bld.bat @@ -0,0 +1,31 @@ +setlocal EnableDelayedExpansion + +:: Create a unique build directory for conda +mkdir conda-build-dir +cd conda-build-dir + +:: Configure CMake +:: We need to point to the root CMakeLists.txt +cmake -G "Ninja" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DPython3_EXECUTABLE="%PYTHON%" ^ + -DCMAKE_INSTALL_PREFIX="%LIBRARY_PREFIX%" ^ + -DCMAKE_CUDA_ARCHITECTURES="75;86;89;120" ^ + "%SRC_DIR%" +if errorlevel 1 exit 1 + +:: Build the HydraPy target +cmake --build . --config Release --target HydraPy +if errorlevel 1 exit 1 + +:: The build should have placed Hydra.pyd into src/Python/hydra_image_processor +:: Verify it exists (optional but good for debugging) +if not exist "%SRC_DIR%\src\Python\hydra_image_processor\Hydra.pyd" ( + echo "Error: Hydra.pyd not found in expected location!" + exit 1 +) + +:: Install the Python package +cd "%SRC_DIR%\src\Python" +"%PYTHON%" -m pip install . --no-deps --ignore-installed -v +if errorlevel 1 exit 1 diff --git a/recipe/build.sh b/recipe/build.sh new file mode 100644 index 0000000..d373b41 --- /dev/null +++ b/recipe/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +mkdir -p conda-build-dir +cd conda-build-dir + +# Configure CMake +cmake ${CMAKE_ARGS} -G "Ninja" + -DCMAKE_BUILD_TYPE=Release + -DPython3_EXECUTABLE="$PYTHON" + -DCMAKE_INSTALL_PREFIX="$PREFIX" + "$SRC_DIR" + +# Build the HydraPy target +cmake --build . --config Release --target HydraPy + +# Install the Python package +cd "$SRC_DIR/src/Python" +"$PYTHON" -m pip install . --no-deps --ignore-installed -v diff --git a/recipe/meta.yaml b/recipe/meta.yaml new file mode 100644 index 0000000..9c58b41 --- /dev/null +++ b/recipe/meta.yaml @@ -0,0 +1,53 @@ +{% set name = "hydra-image-processor" %} +{% set version = "0.1.0" %} + +package: + name: {{ name|lower }} + version: {{ version }} + +source: + path: .. + +build: + number: 0 + script_env: + - CMAKE_GENERATOR=Ninja + +requirements: + build: + - cmake + - ninja + host: + - python + - pip + - setuptools + - wheel + - numpy + - cuda-version 12.4 + run: + - python + - {{ pin_compatible('numpy') }} + - cuda-version >=12.4 + +test: + imports: + - hydra_image_processor + - hydra_image_processor.cuda + - hydra_image_processor.local + commands: + - python -c "import hydra_image_processor; print(hydra_image_processor.__version__)" + +about: + home: https://github.com/zfphil/hydra-image-processor + license: BSD-3-Clause + license_file: LICENSE.md + summary: 'A high-performance image processing library with Python bindings.' + description: | + Hydra Image Processor (Hydra) is a collection of signal filters for image analysis + that can handle 1-5 dimensional data (x, y, z, channels, time), efficiently processing + data larger than GPU memory by optimally chunking it. + dev_url: https://github.com/zfphil/hydra-image-processor + +extra: + recipe-maintainers: + - ericwait From 99b7dc86d4f89844c2d211fcd3f6cd052eb5e902 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:12:17 -0600 Subject: [PATCH 11/40] build: configure python package structure and metadata for distribution --- src/Python/MANIFEST.in | 1 + src/Python/hydra_image_processor/README.md | 223 +++++ src/Python/hydra_image_processor/__init__.py | 182 ++++ src/Python/hydra_image_processor/core.py | 278 ++++++ .../hydra_image_processor/cuda/__init__.py | 90 ++ src/Python/hydra_image_processor/cuda/core.py | 867 ++++++++++++++++++ .../hydra_image_processor/local/__init__.py | 77 ++ .../hydra_image_processor/local/core.py | 369 ++++++++ .../hydra_image_processor/utils/__init__.py | 16 + .../hydra_image_processor/utils/masks.py | 152 +++ src/Python/pyproject.toml | 22 + 11 files changed, 2277 insertions(+) create mode 100644 src/Python/MANIFEST.in create mode 100644 src/Python/hydra_image_processor/README.md create mode 100644 src/Python/hydra_image_processor/__init__.py create mode 100644 src/Python/hydra_image_processor/core.py create mode 100644 src/Python/hydra_image_processor/cuda/__init__.py create mode 100644 src/Python/hydra_image_processor/cuda/core.py create mode 100644 src/Python/hydra_image_processor/local/__init__.py create mode 100644 src/Python/hydra_image_processor/local/core.py create mode 100644 src/Python/hydra_image_processor/utils/__init__.py create mode 100644 src/Python/hydra_image_processor/utils/masks.py create mode 100644 src/Python/pyproject.toml diff --git a/src/Python/MANIFEST.in b/src/Python/MANIFEST.in new file mode 100644 index 0000000..677cea1 --- /dev/null +++ b/src/Python/MANIFEST.in @@ -0,0 +1 @@ +recursive-include hydra_image_processor *.pyd *.dll *.so *.dylib diff --git a/src/Python/hydra_image_processor/README.md b/src/Python/hydra_image_processor/README.md new file mode 100644 index 0000000..f69a91c --- /dev/null +++ b/src/Python/hydra_image_processor/README.md @@ -0,0 +1,223 @@ +# Hydra Image Processor - Python Package + +A Pythonic wrapper for the Hydra Image Processor C++/CUDA library, providing GPU-accelerated image processing operations with automatic CPU fallback. + +## Installation + +The package can be installed in development mode: + +```bash +cd src/Python +pip install -e . +``` + +## Quick Start + +```python +import hydra_image_processor as HIP +import numpy as np + +# Check available CUDA devices +num_devices = HIP.device_count() +print(f"Found {num_devices} CUDA device(s)") + +# Create a test image +image = np.random.rand(100, 100, 50).astype(np.float32) + +# Apply Gaussian smoothing +smoothed = HIP.gaussian(image, sigmas=[2.0, 2.0, 1.0]) + +# Apply median filter with a ball-shaped kernel +kernel = HIP.make_ball_mask(radius=3) +filtered = HIP.median_filter(image, kernel) + +# Get image statistics +min_val, max_val = HIP.get_min_max(image) +total = HIP.sum_array(image) +``` + +## Package Structure + +The package follows a three-layer architecture: + +``` +hydra_image_processor/ +├── __init__.py # Public API +├── core.py # GPU-first fallback logic +├── cuda/ # GPU/CUDA implementations +│ ├── __init__.py +│ └── core.py # Wrappers around Hydra.pyd +├── local/ # CPU fallback implementations (placeholders) +│ ├── __init__.py +│ └── core.py +└── utils/ # Utility functions + ├── __init__.py + └── masks.py # Mask creation utilities +``` + +## Features + +### Device Management +- `device_count()` - Get number of CUDA devices +- `device_stats()` - Get memory statistics +- `check_config()` - Get library configuration +- `info()` - List all available commands +- `help(command)` - Get help for specific command + +### Neighborhood Filters +- `mean_filter()` - Mean/average filtering +- `max_filter()` - Maximum filter (morphological dilation) +- `min_filter()` - Minimum filter (morphological erosion) +- `median_filter()` - Median filtering for noise reduction +- `std_filter()` - Local standard deviation +- `var_filter()` - Local variance + +### Gaussian-Based Filters +- `gaussian()` - Gaussian smoothing +- `LoG()` - Laplacian of Gaussian (edge/blob detection) +- `high_pass_filter()` - High-pass filtering + +### Morphological Operations +- `closure()` - Morphological closing (dilation → erosion) +- `opener()` - Morphological opening (erosion → dilation) + +### Advanced Filters +- `entropy_filter()` - Local entropy for texture analysis +- `wiener_filter()` - Wiener denoising +- `nlmeans()` - Non-Local Means denoising + +### Utility Operations +- `multiply_sum()` - Convolution with custom kernel +- `element_wise_difference()` - Element-wise subtraction +- `make_ball_mask()` - Create spherical structuring element +- `make_ellipsoid_mask()` - Create ellipsoidal structuring element + +### Reduction Operations +- `sum_array()` - Sum of all array elements +- `get_min_max()` - Get minimum and maximum values + +## API Conventions + +### Array Dimensions +Unlike MATLAB's `(X, Y, Z, Channel, Time)` convention, this package follows standard Python/NumPy conventions. The specific dimension ordering depends on your use case, but functions accept 1-5D arrays. + +### Naming Conventions +- Functions use `snake_case` (Pythonic style) +- Acronyms preserve their case (e.g., `LoG` not `log`) +- The conventional import alias is `HIP`: `import hydra_image_processor as HIP` + +### Device Selection +All processing functions accept an optional `device` parameter: +- `device=None` (default): Automatically selects best device(s) +- `device=-1`: Explicitly use all available GPUs +- `device=0, 1, 2, ...`: Use specific GPU device + +```python +# Let the library choose +result = HIP.gaussian(image, sigmas=[1, 1, 1]) + +# Use all GPUs explicitly +result = HIP.gaussian(image, sigmas=[1, 1, 1], device=-1) + +# Use specific GPU +result = HIP.gaussian(image, sigmas=[1, 1, 1], device=0) +``` + +### GPU-First Fallback +The package automatically attempts GPU acceleration first, falling back to CPU implementations if GPU is unavailable. Currently, CPU implementations are placeholders and will raise `NotImplementedError`. + +## Documentation Style + +The package uses NumPy-style docstrings, which are compatible with Sphinx and provide clear, structured documentation. Access documentation via: + +```python +# Python's built-in help +help(HIP.gaussian) + +# Hydra's help system (from C++ layer) +HIP.help('Gaussian') + +# IPython/Jupyter +HIP.gaussian? +``` + +## Examples + +### Example 1: Basic Filtering + +```python +import hydra_image_processor as HIP +import numpy as np + +# Create test image +image = np.random.rand(128, 128, 64).astype(np.float32) + +# Apply Gaussian blur +blurred = HIP.gaussian(image, sigmas=[3.0, 3.0, 1.5]) + +# Apply median filter for noise reduction +kernel = HIP.make_ball_mask(radius=2) +denoised = HIP.median_filter(image, kernel) +``` + +### Example 2: Morphological Operations + +```python +import hydra_image_processor as HIP +import numpy as np + +# Binary image +binary = (np.random.rand(100, 100, 50) > 0.5).astype(np.float32) + +# Create structuring element +se = HIP.make_ball_mask(radius=3) + +# Morphological closing (fill small holes) +closed = HIP.closure(binary, se) + +# Morphological opening (remove small objects) +opened = HIP.opener(binary, se) +``` + +### Example 3: Edge Detection + +```python +import hydra_image_processor as HIP +import numpy as np + +# Load or create image +image = np.random.rand(256, 256).astype(np.float32) + +# Detect edges using Laplacian of Gaussian +edges = HIP.LoG(image, sigmas=[2.0, 2.0, 0.0]) + +# Or use high-pass filtering +high_freq = HIP.high_pass_filter(image, sigmas=[3.0, 3.0, 0.0]) +``` + +## Requirements + +- Python >= 3.9 +- NumPy +- Hydra.pyd (compiled C++/CUDA extension) +- CUDA-capable GPU (for GPU acceleration) +- OpenMP DLL (libomp140.x86_64.dll on Windows) + +## Contributing + +CPU fallback implementations are currently placeholders. Contributions of NumPy/SciPy-based CPU implementations are welcome! The MATLAB package in `src/MATLAB/+HIP/+Local` provides reference implementations that can be translated to Python. + +## License + +BSD-3-Clause (same as the main Hydra Image Processor project) + +## Authors + +- Python Package: Eric Wait +- Original C++/CUDA Library: Eric Wait + +## See Also + +- Main Project: [Hydra Image Processor](../../) +- MATLAB Package: [src/MATLAB/+HIP](../../MATLAB/+HIP) +- C++ Source: [src/c](../../c) diff --git a/src/Python/hydra_image_processor/__init__.py b/src/Python/hydra_image_processor/__init__.py new file mode 100644 index 0000000..478d5c0 --- /dev/null +++ b/src/Python/hydra_image_processor/__init__.py @@ -0,0 +1,182 @@ +""" +Hydra Image Processor - High-Performance GPU-Accelerated Image Processing + +A Python package providing GPU-accelerated image processing operations with +automatic CPU fallback. Built on top of the Hydra C++/CUDA library. + +Basic Usage +----------- +Import the package with the conventional alias:: + + import hydra_image_processor as HIP + +Check available CUDA devices:: + + num_devices = HIP.device_count() + print(f"Found {num_devices} GPU(s)") + +Apply filters to images:: + + import numpy as np + + # Create test image + image = np.random.rand(100, 100, 50).astype(np.float32) + + # Apply Gaussian smoothing + smoothed = HIP.gaussian(image, sigmas=[2.0, 2.0, 1.0]) + + # Apply median filter with custom kernel + kernel = HIP.make_ball_mask(radius=3) + filtered = HIP.median_filter(image, kernel) + +GPU Management +-------------- +By default, functions automatically select the best available GPU or split +work across multiple GPUs. You can explicitly control device selection:: + + # Use specific GPU + result = HIP.gaussian(image, sigmas=[1, 1, 1], device=0) + + # Explicitly use all GPUs + result = HIP.gaussian(image, sigmas=[1, 1, 1], device=-1) + +Package Organization +-------------------- +- `hydra_image_processor.cuda`: Direct GPU/CUDA implementations +- `hydra_image_processor.local`: CPU fallback implementations (stubs) +- `hydra_image_processor.utils`: Utility functions (mask creation, etc.) + +Available Functions +------------------- +**Device Management** + - device_count, device_stats, check_config, info, help + +**Neighborhood Filters** + - mean_filter, max_filter, min_filter, median_filter, std_filter, var_filter + +**Gaussian-Based Filters** + - gaussian, LoG, high_pass_filter + +**Morphological Operations** + - closure, opener + +**Advanced Filters** + - entropy_filter, wiener_filter, nlmeans + +**Utility Operations** + - multiply_sum, element_wise_difference + +**Reduction Operations** + - sum_array, get_min_max + +**Utilities** + - make_ball_mask, make_ellipsoid_mask + +**Test/Debug** + - identity_filter +""" + +__version__ = "0.1.0" +__author__ = "Eric Wait" +__email__ = "info@ericwait.com" + +# Import public API +from .core import ( + # Device management + device_count, + device_stats, + check_config, + info, + help, + + # Neighborhood filters + mean_filter, + max_filter, + min_filter, + median_filter, + std_filter, + var_filter, + + # Gaussian-based filters + gaussian, + LoG, + high_pass_filter, + + # Morphological operations + closure, + opener, + + # Advanced filters + entropy_filter, + wiener_filter, + nlmeans, + + # Utility operations + multiply_sum, + element_wise_difference, + + # Reduction operations + sum_array, + get_min_max, + + # Identity/test + identity_filter, +) + +# Import utilities +from .utils import ( + make_ball_mask, + make_ellipsoid_mask, +) + +# Define public API +__all__ = [ + # Package metadata + '__version__', + '__author__', + '__email__', + + # Device management + 'device_count', + 'device_stats', + 'check_config', + 'info', + 'help', + + # Neighborhood filters + 'mean_filter', + 'max_filter', + 'min_filter', + 'median_filter', + 'std_filter', + 'var_filter', + + # Gaussian-based filters + 'gaussian', + 'LoG', + 'high_pass_filter', + + # Morphological operations + 'closure', + 'opener', + + # Advanced filters + 'entropy_filter', + 'wiener_filter', + 'nlmeans', + + # Utility operations + 'multiply_sum', + 'element_wise_difference', + + # Reduction operations + 'sum_array', + 'get_min_max', + + # Utilities + 'make_ball_mask', + 'make_ellipsoid_mask', + + # Identity/test + 'identity_filter', +] diff --git a/src/Python/hydra_image_processor/core.py b/src/Python/hydra_image_processor/core.py new file mode 100644 index 0000000..d144cab --- /dev/null +++ b/src/Python/hydra_image_processor/core.py @@ -0,0 +1,278 @@ +""" +Core public API for Hydra Image Processor. + +This module provides the main user-facing API with automatic fallback from +GPU (CUDA) to CPU implementations. Functions attempt to use GPU acceleration +first, falling back to CPU implementations if GPU is unavailable. +""" + +import warnings +import numpy as np +from typing import Optional, Union, List, Tuple +from functools import wraps + +from . import cuda +from . import local + + +def _gpu_with_fallback(cuda_func, local_func): + """ + Decorator factory that creates a function with GPU-first fallback logic. + + Attempts to call the CUDA implementation first. If it fails (due to no GPU, + driver issues, etc.), warns the user and falls back to the CPU implementation. + + Parameters + ---------- + cuda_func : callable + GPU implementation function. + local_func : callable + CPU fallback implementation function. + + Returns + ------- + callable + Wrapped function with fallback logic. + """ + @wraps(cuda_func) + def wrapper(*args, **kwargs): + try: + return cuda_func(*args, **kwargs) + except Exception as e: + # Check if it's the "not yet implemented" error from CPU version + # to avoid double warnings + warnings.warn( + f"GPU implementation failed: {str(e)}. " + f"Falling back to CPU implementation.", + RuntimeWarning, + stacklevel=2 + ) + return local_func(*args, **kwargs) + + return wrapper + + +# ============================================================================== +# Device Management Functions +# ============================================================================== + +def device_count() -> int: + """ + Get the number of available CUDA devices. + + Returns + ------- + int + Number of CUDA-capable devices available on the system. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + + Examples + -------- + >>> import hydra_image_processor as HIP + >>> num_devices = HIP.device_count() + >>> print(f"Found {num_devices} CUDA device(s)") + """ + return cuda.device_count() + + +def device_stats() -> List[dict]: + """ + Get memory statistics for all CUDA devices. + + Returns + ------- + List[dict] + List of dictionaries containing memory statistics for each device. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + + Examples + -------- + >>> import hydra_image_processor as HIP + >>> stats = HIP.device_stats() + >>> for i, stat in enumerate(stats): + ... print(f"Device {i}: {stat}") + """ + return cuda.device_stats() + + +def check_config() -> dict: + """ + Get Hydra library configuration information. + + Returns + ------- + dict + Configuration dictionary containing library build information, + CUDA capabilities, and other system details. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + + Examples + -------- + >>> import hydra_image_processor as HIP + >>> config = HIP.check_config() + >>> print("Library configuration:", config) + """ + return cuda.check_config() + + +def info() -> List[dict]: + """ + Get information about all available Hydra commands. + + Returns + ------- + List[dict] + List of dictionaries, each containing: + - 'command': Command name + - 'inArgs': Comma-separated input arguments + - 'outArgs': Comma-separated output arguments + - 'help': Detailed help text for the command + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + + Examples + -------- + >>> import hydra_image_processor as HIP + >>> commands = HIP.info() + >>> print(f"Available commands: {len(commands)}") + >>> for cmd in commands[:3]: + ... print(f"- {cmd['command']}: {cmd['inArgs']} -> {cmd['outArgs']}") + """ + return cuda.info() + + +def help(command: Optional[str] = None) -> str: + """ + Get help text for a specific Hydra command. + + Parameters + ---------- + command : str, optional + Name of the command to get help for. If None, returns general help. + + Returns + ------- + str + Help text for the specified command. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + + Examples + -------- + >>> import hydra_image_processor as HIP + >>> print(HIP.help('Gaussian')) + """ + return cuda.help_func(command) + + +# ============================================================================== +# Neighborhood Filter Functions +# ============================================================================== + +mean_filter = _gpu_with_fallback(cuda.mean_filter, local.mean_filter) +mean_filter.__doc__ = cuda.mean_filter.__doc__ + +max_filter = _gpu_with_fallback(cuda.max_filter, local.max_filter) +max_filter.__doc__ = cuda.max_filter.__doc__ + +min_filter = _gpu_with_fallback(cuda.min_filter, local.min_filter) +min_filter.__doc__ = cuda.min_filter.__doc__ + +median_filter = _gpu_with_fallback(cuda.median_filter, local.median_filter) +median_filter.__doc__ = cuda.median_filter.__doc__ + +std_filter = _gpu_with_fallback(cuda.std_filter, local.std_filter) +std_filter.__doc__ = cuda.std_filter.__doc__ + +var_filter = _gpu_with_fallback(cuda.var_filter, local.var_filter) +var_filter.__doc__ = cuda.var_filter.__doc__ + + +# ============================================================================== +# Gaussian-Based Filter Functions +# ============================================================================== + +gaussian = _gpu_with_fallback(cuda.gaussian, local.gaussian) +gaussian.__doc__ = cuda.gaussian.__doc__ + +LoG = _gpu_with_fallback(cuda.LoG, local.LoG) +LoG.__doc__ = cuda.LoG.__doc__ + +high_pass_filter = _gpu_with_fallback(cuda.high_pass_filter, local.high_pass_filter) +high_pass_filter.__doc__ = cuda.high_pass_filter.__doc__ + + +# ============================================================================== +# Morphological Operations +# ============================================================================== + +closure = _gpu_with_fallback(cuda.closure, local.closure) +closure.__doc__ = cuda.closure.__doc__ + +opener = _gpu_with_fallback(cuda.opener, local.opener) +opener.__doc__ = cuda.opener.__doc__ + + +# ============================================================================== +# Advanced Filter Functions +# ============================================================================== + +entropy_filter = _gpu_with_fallback(cuda.entropy_filter, local.entropy_filter) +entropy_filter.__doc__ = cuda.entropy_filter.__doc__ + +wiener_filter = _gpu_with_fallback(cuda.wiener_filter, local.wiener_filter) +wiener_filter.__doc__ = cuda.wiener_filter.__doc__ + +nlmeans = _gpu_with_fallback(cuda.nlmeans, local.nlmeans) +nlmeans.__doc__ = cuda.nlmeans.__doc__ + + +# ============================================================================== +# Utility Operations +# ============================================================================== + +multiply_sum = _gpu_with_fallback(cuda.multiply_sum, local.multiply_sum) +multiply_sum.__doc__ = cuda.multiply_sum.__doc__ + +element_wise_difference = _gpu_with_fallback( + cuda.element_wise_difference, + local.element_wise_difference +) +element_wise_difference.__doc__ = cuda.element_wise_difference.__doc__ + + +# ============================================================================== +# Reduction Operations +# ============================================================================== + +sum_array = _gpu_with_fallback(cuda.sum_array, local.sum_array) +sum_array.__doc__ = cuda.sum_array.__doc__ + +get_min_max = _gpu_with_fallback(cuda.get_min_max, local.get_min_max) +get_min_max.__doc__ = cuda.get_min_max.__doc__ + + +# ============================================================================== +# Identity/Test Functions +# ============================================================================== + +identity_filter = _gpu_with_fallback(cuda.identity_filter, local.identity_filter) +identity_filter.__doc__ = cuda.identity_filter.__doc__ diff --git a/src/Python/hydra_image_processor/cuda/__init__.py b/src/Python/hydra_image_processor/cuda/__init__.py new file mode 100644 index 0000000..a3775d4 --- /dev/null +++ b/src/Python/hydra_image_processor/cuda/__init__.py @@ -0,0 +1,90 @@ +""" +CUDA/GPU implementation layer for Hydra Image Processor. + +This module provides thin wrappers around the compiled Hydra C++ extension, +enabling GPU-accelerated image processing operations. +""" + +from .core import ( + # Device management + device_count, + device_stats, + check_config, + info, + help_func, + + # Neighborhood filters + mean_filter, + max_filter, + min_filter, + median_filter, + std_filter, + var_filter, + + # Gaussian-based filters + gaussian, + LoG, + high_pass_filter, + + # Morphological operations + closure, + opener, + + # Advanced filters + entropy_filter, + wiener_filter, + nlmeans, + + # Utility operations + multiply_sum, + element_wise_difference, + + # Reduction operations + sum as sum_array, + get_min_max, + + # Identity/test + identity_filter, +) + +__all__ = [ + # Device management + 'device_count', + 'device_stats', + 'check_config', + 'info', + 'help_func', + + # Neighborhood filters + 'mean_filter', + 'max_filter', + 'min_filter', + 'median_filter', + 'std_filter', + 'var_filter', + + # Gaussian-based filters + 'gaussian', + 'LoG', + 'high_pass_filter', + + # Morphological operations + 'closure', + 'opener', + + # Advanced filters + 'entropy_filter', + 'wiener_filter', + 'nlmeans', + + # Utility operations + 'multiply_sum', + 'element_wise_difference', + + # Reduction operations + 'sum_array', + 'get_min_max', + + # Identity/test + 'identity_filter', +] diff --git a/src/Python/hydra_image_processor/cuda/core.py b/src/Python/hydra_image_processor/cuda/core.py new file mode 100644 index 0000000..dc73a5b --- /dev/null +++ b/src/Python/hydra_image_processor/cuda/core.py @@ -0,0 +1,867 @@ +""" +Core CUDA wrapper functions for Hydra Image Processor. + +This module provides thin wrappers around the Hydra.pyd C++ extension module, +converting between Pythonic naming conventions and the underlying C++ API. +""" + +import numpy as np +from typing import Optional, Union, List, Tuple, Any +import warnings + +# Try to import the compiled Hydra module from parent package +try: + from .. import Hydra as _Hydra + _HYDRA_AVAILABLE = True +except ImportError as e: + _HYDRA_AVAILABLE = False + _HYDRA_IMPORT_ERROR = str(e) + + +def _ensure_hydra_available(): + """Raise an error if Hydra module is not available.""" + if not _HYDRA_AVAILABLE: + raise ImportError( + f"Hydra C++ extension module is not available: {_HYDRA_IMPORT_ERROR}. " + "Make sure Hydra.pyd is in the Python path along with required DLLs." + ) + + +# ============================================================================== +# Device Management Functions +# ============================================================================== + +def device_count() -> int: + """ + Get the number of available CUDA devices. + + Returns + ------- + int + Number of CUDA-capable devices available on the system. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + result = _Hydra.DeviceCount() + # DeviceCount returns (count, stats), extract just the count + if isinstance(result, tuple): + return result[0] + return result + + +def device_stats() -> List[dict]: + """ + Get memory statistics for all CUDA devices. + + Returns + ------- + List[dict] + List of dictionaries containing memory statistics for each device. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + result = _Hydra.DeviceCount() + # DeviceCount returns (count, stats), extract the stats + if isinstance(result, tuple): + return result[1] + # Fallback to DeviceStats if it exists separately + return _Hydra.DeviceStats() + + +def check_config() -> dict: + """ + Get Hydra library configuration information. + + Returns + ------- + dict + Configuration dictionary containing library build information, + CUDA capabilities, and other system details. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + return _Hydra.CheckConfig() + + +def info() -> List[dict]: + """ + Get information about all available Hydra commands. + + Returns + ------- + List[dict] + List of dictionaries, each containing: + - 'command': Command name + - 'inArgs': Comma-separated input arguments + - 'outArgs': Comma-separated output arguments + - 'help': Detailed help text for the command + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + return _Hydra.Info() + + +def help_func(command: Optional[str] = None) -> str: + """ + Get help text for a specific Hydra command. + + Parameters + ---------- + command : str, optional + Name of the command to get help for. If None, returns general help. + + Returns + ------- + str + Help text for the specified command. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + if command is None: + return _Hydra.Help() + return _Hydra.Help(command) + + +# ============================================================================== +# Neighborhood Filter Functions +# ============================================================================== + +def mean_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply mean (average) filter to image using the specified kernel. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). First 3 dimensions are spatial, + 4th is channel, 5th is time/frame. + kernel : array-like + Kernel array (1-3 dimensions) defining the neighborhood. Non-zero + elements indicate positions to include in the mean calculation. + num_iterations : int, default=1 + Number of times to apply the filter. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Filtered image with same shape and dtype as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.MeanFilter(image, kernel, num_iterations, device) + + +def max_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply maximum filter to image (morphological dilation). + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Structuring element for dilation. Non-zero elements define the + neighborhood shape. + num_iterations : int, default=1 + Number of times to apply the filter. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Filtered image with same shape and dtype as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.MaxFilter(image, kernel, num_iterations, device) + + +def min_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply minimum filter to image (morphological erosion). + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Structuring element for erosion. Non-zero elements define the + neighborhood shape. + num_iterations : int, default=1 + Number of times to apply the filter. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Filtered image with same shape and dtype as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.MinFilter(image, kernel, num_iterations, device) + + +def median_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply median filter to image for noise reduction. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Kernel defining the neighborhood shape. Non-zero elements indicate + positions to include in median calculation. + num_iterations : int, default=1 + Number of times to apply the filter. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Filtered image with same shape and dtype as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.MedianFilter(image, kernel, num_iterations, device) + + +def std_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Calculate standard deviation within local neighborhoods. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Kernel defining the neighborhood shape. + num_iterations : int, default=1 + Number of times to apply the filter. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Image containing local standard deviations, same shape as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.StdFilter(image, kernel, num_iterations, device) + + +def var_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Calculate variance within local neighborhoods. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Kernel defining the neighborhood shape. + num_iterations : int, default=1 + Number of times to apply the filter. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Image containing local variances, same shape as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.VarFilter(image, kernel, num_iterations, device) + + +# ============================================================================== +# Gaussian-Based Filter Functions +# ============================================================================== + +def gaussian( + image: np.ndarray, + sigmas: Union[List[float], Tuple[float, ...], np.ndarray], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply Gaussian smoothing filter to image. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + sigmas : array-like of float + Gaussian sigma values [sigma_x, sigma_y, sigma_z]. Use 0 for no + smoothing in that dimension. + num_iterations : int, default=1 + Number of times to apply the filter. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Smoothed image with same shape and dtype as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + sigmas = np.asarray(sigmas, dtype=np.float32) + if device is None: + device = -1 + return _Hydra.Gaussian(image, sigmas, num_iterations, device) + + +def LoG( + image: np.ndarray, + sigmas: Union[List[float], Tuple[float, ...], np.ndarray], + device: Optional[int] = None +) -> np.ndarray: + """ + Apply Laplacian of Gaussian (LoG) filter for edge/blob detection. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + sigmas : array-like of float + Gaussian sigma values [sigma_x, sigma_y, sigma_z] for the LoG kernel. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + LoG-filtered image with same shape as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + sigmas = np.asarray(sigmas, dtype=np.float32) + if device is None: + device = -1 + return _Hydra.LoG(image, sigmas, device) + + +def high_pass_filter( + image: np.ndarray, + sigmas: Union[List[float], Tuple[float, ...], np.ndarray], + device: Optional[int] = None +) -> np.ndarray: + """ + Apply high-pass filter to enhance high-frequency details. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + sigmas : array-like of float + Gaussian sigma values for the high-pass filter. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + High-pass filtered image with same shape as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + sigmas = np.asarray(sigmas, dtype=np.float32) + if device is None: + device = -1 + return _Hydra.HighPassFilter(image, sigmas, device) + + +# ============================================================================== +# Morphological Operations +# ============================================================================== + +def closure( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply morphological closing (dilation followed by erosion). + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Structuring element. Non-zero elements define the neighborhood shape. + num_iterations : int, default=1 + Number of times to apply the operation. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Morphologically closed image with same shape and dtype as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.Closure(image, kernel, num_iterations, device) + + +def opener( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply morphological opening (erosion followed by dilation). + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Structuring element. Non-zero elements define the neighborhood shape. + num_iterations : int, default=1 + Number of times to apply the operation. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Morphologically opened image with same shape and dtype as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.Opener(image, kernel, num_iterations, device) + + +# ============================================================================== +# Advanced Filter Functions +# ============================================================================== + +def entropy_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + device: Optional[int] = None +) -> np.ndarray: + """ + Calculate local entropy within neighborhoods for texture analysis. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Kernel defining the neighborhood shape. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Image containing local entropy values, same shape as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.EntropyFilter(image, kernel, device) + + +def wiener_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + noise_variance: float, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply Wiener filter for noise reduction. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Kernel defining the neighborhood shape. + noise_variance : float + Estimated variance of the noise in the image. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Wiener-filtered image with same shape as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.WienerFilter(image, kernel, noise_variance, device) + + +def nlmeans( + image: np.ndarray, + h: float, + search_window_radius: int, + neighborhood_radius: int, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply Non-Local Means denoising filter. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + h : float + Filtering parameter controlling decay. Higher values remove more noise + but may blur details. + search_window_radius : int + Radius of the search window for finding similar patches. + neighborhood_radius : int + Radius of the neighborhood/patch for comparison. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Denoised image with same shape as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + if device is None: + device = -1 + return _Hydra.NLMeans(image, h, search_window_radius, neighborhood_radius, device) + + +# ============================================================================== +# Utility Operations +# ============================================================================== + +def multiply_sum( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Optional[int] = None +) -> np.ndarray: + """ + Apply weighted sum (convolution) using the specified kernel. + + Parameters + ---------- + image : np.ndarray + Input image array (1-5 dimensions). + kernel : array-like + Convolution kernel with weights for each neighborhood position. + num_iterations : int, default=1 + Number of times to apply the convolution. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Convolved image with same shape as input. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + kernel = np.asarray(kernel) + if device is None: + device = -1 + return _Hydra.MultiplySum(image, kernel, num_iterations, device) + + +def element_wise_difference( + image1: np.ndarray, + image2: np.ndarray, + device: Optional[int] = None +) -> np.ndarray: + """ + Compute element-wise difference between two images (image1 - image2). + + Parameters + ---------- + image1 : np.ndarray + First input image. + image2 : np.ndarray + Second input image (subtracted from first). + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Difference image with same shape as inputs. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + if device is None: + device = -1 + return _Hydra.ElementWiseDifference(image1, image2, device) + + +# ============================================================================== +# Reduction Operations +# ============================================================================== + +def sum( + image: np.ndarray, + device: Optional[int] = None +) -> float: + """ + Compute the sum of all elements in the array. + + Parameters + ---------- + image : np.ndarray + Input image array. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + float + Sum of all array elements. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + if device is None: + device = -1 + return _Hydra.Sum(image, device) + + +def get_min_max( + image: np.ndarray, + device: Optional[int] = None +) -> Tuple[float, float]: + """ + Get minimum and maximum values in the array. + + Parameters + ---------- + image : np.ndarray + Input image array. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + Tuple[float, float] + Tuple containing (min_value, max_value). + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + if device is None: + device = -1 + return _Hydra.GetMinMax(image, device) + + +# ============================================================================== +# Identity/Test Functions +# ============================================================================== + +def identity_filter( + image: np.ndarray, + device: Optional[int] = None +) -> np.ndarray: + """ + Identity operation (returns copy of input). Useful for testing. + + Parameters + ---------- + image : np.ndarray + Input image array. + device : int, optional + CUDA device ID to use. If None (default), automatically selects device + or splits across multiple devices. Use -1 to explicitly use all GPUs. + + Returns + ------- + np.ndarray + Copy of input image. + + Raises + ------ + ImportError + If the Hydra C++ extension is not available. + """ + _ensure_hydra_available() + if device is None: + device = -1 + return _Hydra.IdentityFilter(image, device) diff --git a/src/Python/hydra_image_processor/local/__init__.py b/src/Python/hydra_image_processor/local/__init__.py new file mode 100644 index 0000000..ccb4889 --- /dev/null +++ b/src/Python/hydra_image_processor/local/__init__.py @@ -0,0 +1,77 @@ +""" +Local/CPU implementation layer for Hydra Image Processor. + +This module provides CPU-based fallback implementations for image processing +operations. Currently contains placeholder stubs that will be implemented +with NumPy/SciPy in the future. +""" + +from .core import ( + # Neighborhood filters + mean_filter, + max_filter, + min_filter, + median_filter, + std_filter, + var_filter, + + # Gaussian-based filters + gaussian, + LoG, + high_pass_filter, + + # Morphological operations + closure, + opener, + + # Advanced filters + entropy_filter, + wiener_filter, + nlmeans, + + # Utility operations + multiply_sum, + element_wise_difference, + + # Reduction operations + sum as sum_array, + get_min_max, + + # Identity/test + identity_filter, +) + +__all__ = [ + # Neighborhood filters + 'mean_filter', + 'max_filter', + 'min_filter', + 'median_filter', + 'std_filter', + 'var_filter', + + # Gaussian-based filters + 'gaussian', + 'LoG', + 'high_pass_filter', + + # Morphological operations + 'closure', + 'opener', + + # Advanced filters + 'entropy_filter', + 'wiener_filter', + 'nlmeans', + + # Utility operations + 'multiply_sum', + 'element_wise_difference', + + # Reduction operations + 'sum_array', + 'get_min_max', + + # Identity/test + 'identity_filter', +] diff --git a/src/Python/hydra_image_processor/local/core.py b/src/Python/hydra_image_processor/local/core.py new file mode 100644 index 0000000..13f99d5 --- /dev/null +++ b/src/Python/hydra_image_processor/local/core.py @@ -0,0 +1,369 @@ +""" +CPU-based fallback implementations for Hydra Image Processor. + +This module provides CPU implementations using NumPy and SciPy. Currently +these are placeholder stubs that will be implemented in the future. +""" + +import numpy as np +from typing import Union, List, Tuple + +# ============================================================================== +# Neighborhood Filter Functions +# ============================================================================== + +def mean_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of mean filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of mean_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def max_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of max filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of max_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def min_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of min filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of min_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def median_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of median filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of median_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def std_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of std filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of std_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def var_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of variance filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of var_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +# ============================================================================== +# Gaussian-Based Filter Functions +# ============================================================================== + +def gaussian( + image: np.ndarray, + sigmas: Union[List[float], Tuple[float, ...], np.ndarray], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of Gaussian filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of gaussian is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def LoG( + image: np.ndarray, + sigmas: Union[List[float], Tuple[float, ...], np.ndarray], + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of Laplacian of Gaussian (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of LoG is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def high_pass_filter( + image: np.ndarray, + sigmas: Union[List[float], Tuple[float, ...], np.ndarray], + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of high-pass filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of high_pass_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +# ============================================================================== +# Morphological Operations +# ============================================================================== + +def closure( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of morphological closing (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of closure is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def opener( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of morphological opening (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of opener is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +# ============================================================================== +# Advanced Filter Functions +# ============================================================================== + +def entropy_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of entropy filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of entropy_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def wiener_filter( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + noise_variance: float, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of Wiener filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of wiener_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def nlmeans( + image: np.ndarray, + h: float, + search_window_radius: int, + neighborhood_radius: int, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of Non-Local Means (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of nlmeans is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +# ============================================================================== +# Utility Operations +# ============================================================================== + +def multiply_sum( + image: np.ndarray, + kernel: Union[np.ndarray, List[int], Tuple[int, ...]], + num_iterations: int = 1, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of convolution (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of multiply_sum is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def element_wise_difference( + image1: np.ndarray, + image2: np.ndarray, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of element-wise difference (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of element_wise_difference is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +# ============================================================================== +# Reduction Operations +# ============================================================================== + +def sum( + image: np.ndarray, + device: Union[int, None] = None +) -> float: + """ + CPU implementation of sum (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of sum is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +def get_min_max( + image: np.ndarray, + device: Union[int, None] = None +) -> Tuple[float, float]: + """ + CPU implementation of get_min_max (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of get_min_max is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) + + +# ============================================================================== +# Identity/Test Functions +# ============================================================================== + +def identity_filter( + image: np.ndarray, + device: Union[int, None] = None +) -> np.ndarray: + """ + CPU implementation of identity filter (placeholder). + + This function is not yet implemented. Use the CUDA version or contribute + a NumPy/SciPy implementation. + """ + raise NotImplementedError( + "CPU implementation of identity_filter is not yet available. " + "Please use a CUDA-capable device or contribute a NumPy/SciPy implementation." + ) diff --git a/src/Python/hydra_image_processor/utils/__init__.py b/src/Python/hydra_image_processor/utils/__init__.py new file mode 100644 index 0000000..22cdfe3 --- /dev/null +++ b/src/Python/hydra_image_processor/utils/__init__.py @@ -0,0 +1,16 @@ +""" +Utility functions for Hydra Image Processor. + +This module provides helper functions for creating structuring elements, +masks, and other utilities for image processing operations. +""" + +from .masks import ( + make_ball_mask, + make_ellipsoid_mask, +) + +__all__ = [ + 'make_ball_mask', + 'make_ellipsoid_mask', +] diff --git a/src/Python/hydra_image_processor/utils/masks.py b/src/Python/hydra_image_processor/utils/masks.py new file mode 100644 index 0000000..032df1e --- /dev/null +++ b/src/Python/hydra_image_processor/utils/masks.py @@ -0,0 +1,152 @@ +""" +Mask and structuring element creation utilities. + +This module provides functions to create common structuring elements and masks +for use with morphological operations and neighborhood filters. +""" + +import numpy as np +from typing import Union, Tuple, List + + +def make_ball_mask(radius: int) -> np.ndarray: + """ + Create a 3D spherical (ball) structuring element. + + Creates a boolean array where True values form a sphere of the specified + radius. This is useful for morphological operations and neighborhood filters + with spherical neighborhoods. + + Parameters + ---------- + radius : int + Radius of the ball in pixels. Must be positive. + + Returns + ------- + np.ndarray + 3D boolean array of shape (2*radius+1, 2*radius+1, 2*radius+1) where + True values indicate positions inside the sphere. + + Examples + -------- + >>> import hydra_image_processor as HIP + >>> mask = HIP.make_ball_mask(3) + >>> mask.shape + (7, 7, 7) + >>> mask.sum() # Number of voxels in the ball + 123 + + Notes + ----- + The ball is centered in the array, with radius measured from the center + voxel. The function uses Euclidean distance to determine inclusion. + """ + if radius <= 0: + raise ValueError("Radius must be positive") + + size = 2 * radius + 1 + shape_element = np.zeros((size, size, size), dtype=bool) + + # Create coordinate grids + x, y, z = np.mgrid[0:size, 0:size, 0:size] + + # Calculate distance from center + center = radius + distances_squared = ( + (x - center) ** 2 + + (y - center) ** 2 + + (z - center) ** 2 + ) + + # Set True for positions inside the ball + shape_element[distances_squared <= radius ** 2] = True + + return shape_element + + +def make_ellipsoid_mask( + axes_radius: Union[Tuple[float, float, float], List[float], np.ndarray] +) -> np.ndarray: + """ + Create a 3D ellipsoidal structuring element. + + Creates a boolean array where True values form an ellipsoid with the + specified radii along each axis. This is useful for anisotropic + morphological operations and neighborhood filters. + + Parameters + ---------- + axes_radius : array-like of 3 floats + Radii along each axis [radius_x, radius_y, radius_z]. Values must be + non-negative. Use 0 for a flat disk in that dimension. + + Returns + ------- + np.ndarray + 3D boolean array where True values indicate positions inside the + ellipsoid. Array size is determined by the radii to fully contain + the ellipsoid. + + Raises + ------ + ValueError + If axes_radius does not contain exactly 3 values or contains + negative values. + + Examples + -------- + >>> import hydra_image_processor as HIP + >>> # Create an ellipsoid with different radii + >>> mask = HIP.make_ellipsoid_mask([5, 3, 2]) + >>> mask.shape + (11, 7, 5) + + >>> # Create a disk (flat in Z) + >>> disk = HIP.make_ellipsoid_mask([5, 5, 0]) + >>> disk.shape[2] + 1 + + Notes + ----- + The ellipsoid is centered in the array. The inclusion criterion uses + the standard ellipsoid equation: (x/rx)² + (y/ry)² + (z/rz)² ≤ 1 + """ + axes_radius = np.asarray(axes_radius, dtype=float) + + if axes_radius.size != 3: + raise ValueError( + f"axes_radius must contain exactly 3 values (got {axes_radius.size}). " + "Specify radii for X, Y, and Z axes." + ) + + if np.any(axes_radius < 0): + raise ValueError("All radius values must be non-negative") + + # Calculate volume size (ensure at least size 1 in each dimension) + vol_size = np.maximum(np.ceil(axes_radius * 2) + 1, 1).astype(int) + + shape_element = np.zeros(vol_size, dtype=bool) + + # Create coordinate grids + x, y, z = np.mgrid[0:vol_size[0], 0:vol_size[1], 0:vol_size[2]] + + # Shift origin to center + x = x - vol_size[0] / 2 - 0.5 + y = y - vol_size[1] / 2 - 0.5 + z = z - vol_size[2] / 2 - 0.5 + + # Avoid division by zero for zero radii + axes_radius = np.maximum(axes_radius, 1e-10) + + # Calculate normalized squared distances + normalized_distances = ( + (x ** 2 / axes_radius[0] ** 2) + + (y ** 2 / axes_radius[1] ** 2) + + (z ** 2 / axes_radius[2] ** 2) + ) + + # Set True for positions inside the ellipsoid + shape_element[normalized_distances <= 1] = True + + return shape_element diff --git a/src/Python/pyproject.toml b/src/Python/pyproject.toml new file mode 100644 index 0000000..1753bf4 --- /dev/null +++ b/src/Python/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "hydra-image-processor" +version = "0.1.0" +description = "A high-performance image processing library with Python bindings." +readme = "README.md" +requires-python = ">=3.9" +license = "BSD-3-Clause" +authors = [ + {name = "Eric Wait", email = "info@ericwait.com"} +] +keywords = ["image processing", "python bindings", "high performance", "C++", "CUDA"] + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +include = ["hydra_image_processor*"] +exclude = ["Test*"] From 06c687fa3bc2bbd0bfa5d6258eb659b44c872c27 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:12:27 -0600 Subject: [PATCH 12/40] ci: add github actions workflow for automated conda builds --- .github/workflows/conda-build.yml | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/conda-build.yml diff --git a/.github/workflows/conda-build.yml b/.github/workflows/conda-build.yml new file mode 100644 index 0000000..2bb76fc --- /dev/null +++ b/.github/workflows/conda-build.yml @@ -0,0 +1,58 @@ +name: Conda Build + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + workflow_dispatch: + +jobs: + build-conda: + name: Build Conda Package (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest] + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install CUDA Toolkit + uses: Jimver/cuda-toolkit@v0.2.14 + id: cuda-toolkit + with: + cuda: '12.4.1' + + - name: Set up Conda + uses: conda-incubator/setup-miniconda@v3 + with: + auto-update-conda: true + python-version: ${{ matrix.python-version }} + channels: conda-forge, defaults + + - name: Install Conda Build Tools + shell: bash -l {0} + run: | + conda install -y conda-build conda-verify anaconda-client + + - name: Build Conda Package + shell: bash -l {0} + run: | + conda build recipe --python ${{ matrix.python-version }} + + - name: Upload to Anaconda (Optional) + # Only upload on main branch pushes, not PRs + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + shell: bash -l {0} + env: + ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} + run: | + if [ -n "$ANACONDA_TOKEN" ]; then + anaconda -t $ANACONDA_TOKEN upload --user hydra-image-processor $(conda build recipe --output) + else + echo "ANACONDA_TOKEN not found, skipping upload." + fi From 9d4bffb76bbad71c11d8e3fdbba8e0adb8b7101d Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:12:35 -0600 Subject: [PATCH 13/40] docs: add GEMINI context and CI-specific documentation --- GEMINI.md | 76 +++++++++++++++++++++ ci-readme.md | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 GEMINI.md create mode 100644 ci-readme.md diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..d3c2d60 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,76 @@ +# Hydra Image Processor + +## Project Overview + +Hydra Image Processor (Hydra) is a high-performance library for signal filters and image analysis, capable of handling 1-5 dimensional data (x, y, z, channels, time). Its core feature is the ability to efficiently process data larger than GPU memory by optimally chunking it and distributing higher-dimensional chunks across multiple GPUs, while ensuring energy-insulated boundary conditions to prevent edge artifacts. + +The project consists of a core C++/CUDA library with bindings for: +* **Python**: A Conda-installable package `hydra-image-processor`. +* **MATLAB**: A toolbox interface. + +## Key Technologies + +* **Languages**: C++, CUDA, Python, MATLAB. +* **Build System**: CMake, Ninja. +* **Package Managers**: Conda, Pip. +* **CI/CD**: GitHub Actions. + +## Directory Structure + +* `src/c/Cuda`: Core C++/CUDA implementation of image processing kernels and chunking logic. +* `src/c/Python`: C++ source for the Python extension module (`Hydra.pyd`/`Hydra.so`). +* `src/c/Mex`: C++ source for the MATLAB MEX interface. +* `src/Python`: Source code for the Python package (`hydra_image_processor`). +* `src/MATLAB`: MATLAB toolbox code and wrappers. +* `recipe/`: Conda build recipe (`meta.yaml`, `build.sh`, `bld.bat`). +* `.github/workflows`: CI/CD workflows for building and publishing. + +## Building and Running + +### Prerequisites + +* NVIDIA GPU with CUDA support. +* CUDA Toolkit (11.x or newer recommended). +* CMake (3.22+). +* C++ Compiler (MSVC on Windows, GCC/Clang on Linux). +* Python 3.9+. + +### Python Build (Local Development) + +To build and install the Python package locally: + +```bash +# Navigate to the project root +cd E:\programming\hydra-image-processor + +# Install in editable mode (requires scikit-build-core or manual cmake invocation usually, +# but this project uses a standard setup.py/cmake flow or pip install .) +pip install . +``` + +Alternatively, using the provided CMake presets: + +```bash +cmake --preset dev +cmake --build build --config Release --target HydraPy +``` + +### Conda Package Build + +To build the Conda package: + +```bash +conda install conda-build +conda build recipe +``` + +### MATLAB Build + +The MATLAB toolbox is built using the `.prj` file: +`src/MATLAB/HydraImageProcessor.prj`. + +## Development Conventions + +* **Hybrid Implementation**: The Python package uses a "GPU-first, CPU-fallback" pattern. Wrappers in `src/Python/hydra_image_processor/core.py` try to call the compiled CUDA module and catch exceptions to fall back to local CPU implementations. +* **Chunking Strategy**: Large images are automatically split into chunks that fit in GPU memory. This logic is handled in the C++ core (`ImageChunk.cpp`). +* **Packaging**: The Python package includes the compiled extension binary (`.pyd` or `.so`) and required DLLs (like `libomp`) to ensure portability. diff --git a/ci-readme.md b/ci-readme.md new file mode 100644 index 0000000..783dc7f --- /dev/null +++ b/ci-readme.md @@ -0,0 +1,184 @@ +# CI/CD Setup for Hydra Image Processor + +This document describes how to build and distribute the Hydra Python extensions using GitHub Actions. + +## Files to Add to Your Repository + +1. **`.github/workflows/build-python-artifacts.yml`** - Main CI workflow that builds the Python extensions +2. **`scripts/download_artifacts.py`** - Helper script to download artifacts from GitHub Actions +3. **`test_hydra_module.py`** - Test script to verify the built module works correctly +4. **Update `CMakeLists.txt`** - Change `HYDRA_MODULE_NAME` from "HIP" to "Hydra" + +## Build Process + +### Step 1: Update Your CMakeLists.txt + +Change the module name in your root `CMakeLists.txt`: +```cmake +set(HYDRA_MODULE_NAME "Hydra") # Changed from "HIP" +``` + +### Step 2: Commit and Push the Workflow + +```bash +# Create the workflow directory +mkdir -p .github/workflows + +# Copy the workflow file +cp build-python-artifacts.yml .github/workflows/ + +# Create scripts directory +mkdir -p scripts +cp download_artifacts.py scripts/ + +# Commit and push +git add .github/workflows/build-python-artifacts.yml +git add scripts/download_artifacts.py +git add test_hydra_module.py +git commit -m "Add CI/CD for Python extensions with Hydra module name" +git push +``` + +### Step 3: Monitor the Build + +1. Go to your GitHub repository +2. Click on the "Actions" tab +3. Watch the workflow run +4. Builds will create artifacts for: + - Linux (`.so` files) with CUDA 12.2 and 12.3 + - Windows (`.pyd` files) with CUDA 12.2 and 12.3 + - Python versions 3.9, 3.10, and 3.11 + +## Downloading and Testing Artifacts + +### Method 1: Via GitHub Web Interface + +1. Go to Actions tab +2. Click on a completed workflow run +3. Scroll down to "Artifacts" +4. Download the artifacts you need + +### Method 2: Using the Download Script + +```bash +# Find your run ID from the GitHub Actions page +python scripts/download_artifacts.py --run-id + +# The script will download all artifacts and create a test script +cd downloaded_artifacts +python test_import.py +``` + +### Method 3: Using GitHub CLI + +```bash +# List recent workflow runs +gh run list --workflow=build-python-artifacts.yml + +# Download all artifacts from a specific run +gh run download +``` + +## Testing the Module + +### Quick Test +```python +import sys +sys.path.insert(0, '/path/to/artifact') +import Hydra +print(Hydra.Info()) +``` + +### Comprehensive Test +```bash +python test_hydra_module.py +``` + +## Using in Your Project + +### For [[home-media-ai]] or Other Projects + +1. Download the appropriate artifact for your platform: + - Linux: `Hydra-linux-cuda12.2-py3.10/Hydra.so` + - Windows: `Hydra-windows-cuda1220-py3.10/Hydra.pyd` + +2. Copy to your project: +```bash +# Linux +cp downloaded_artifacts/Hydra-linux-cuda12.2-py3.10/Hydra.so ~/home-media-ai/ + +# Windows +copy downloaded_artifacts\Hydra-windows-cuda1220-py3.10\Hydra.pyd C:\projects\home-media-ai\ +``` + +3. Import in Python: +```python +import Hydra + +# Check available functions +info = Hydra.Info() +for cmd in info: + print(cmd.command) + +# Use the module +import numpy as np +image = np.random.rand(512, 512).astype(np.float32) +filtered = Hydra.Gaussian(image, sigma=[2.0, 2.0]) +``` + +## Artifact Naming Convention + +Artifacts are named with the pattern: +``` +Hydra-{platform}-cuda{version}-py{python_version} +``` + +Examples: +- `Hydra-linux-cuda12.2-py3.10` +- `Hydra-windows-cuda1220-py3.11` + +## Troubleshooting + +### Import Errors + +If you get import errors, check: +1. **CUDA Runtime**: Ensure CUDA 12.x is installed +2. **Python Version**: Match the Python version of the artifact +3. **Dependencies**: Install numpy: `pip install numpy` +4. **Library Path**: On Linux, you may need to set `LD_LIBRARY_PATH` + +### CUDA Not Found + +The module will import but operations may fail if CUDA is not available. +This is normal for testing on systems without GPUs. + +### Windows Specific + +On Windows, you may need: +- Visual C++ Redistributables +- CUDA Toolkit 12.x +- Ensure `.pyd` file is in a directory on your Python path + +## Next Steps + +Once testing is successful: + +1. **Create Wheels**: Package as `.whl` files for pip installation +2. **Conda Package**: Create conda recipe for conda-forge submission +3. **Release Automation**: Auto-publish on GitHub releases + +## Conda-forge Submission (Future) + +After successful testing, we'll: +1. Fork conda-forge/staged-recipes +2. Add recipe in `recipes/hydra-image-processor/` +3. Submit PR for review +4. Once accepted, get a feedstock repository +5. Automatic builds on each release + +## Support + +For issues or questions: +- Check the [Actions logs](https://github.com/ericwait/hydra-image-processor/actions) +- Open an issue on the repository +- Check the [Hydra website](https://www.hydraimageprocessor.com) \ No newline at end of file From 107dfb02ad818af18383cdb95982850f8ad3ffea Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:12:55 -0600 Subject: [PATCH 14/40] test: update python examples and test suite to use hydra namespace --- src/Python/HIP.so | 3 - src/Python/download-script.py | 146 ++++++++++++++++++++++++ src/Python/example_usage.py | 203 +++++++++++++++++++++++++++++++++ src/Python/test-hydra.py | 137 ++++++++++++++++++++++ src/Python/test_import.py | 49 ++++++++ src/Python/test_nonchunking.py | 22 ++-- src/Python/test_package.py | 176 ++++++++++++++++++++++++++++ 7 files changed, 722 insertions(+), 14 deletions(-) delete mode 100755 src/Python/HIP.so create mode 100644 src/Python/download-script.py create mode 100644 src/Python/example_usage.py create mode 100644 src/Python/test-hydra.py create mode 100644 src/Python/test_import.py create mode 100644 src/Python/test_package.py diff --git a/src/Python/HIP.so b/src/Python/HIP.so deleted file mode 100755 index 5739cee..0000000 --- a/src/Python/HIP.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e48fde196e8b7cabaf4086bdb4a5a0e488b129015c1061819419c8fe9537becf -size 16810640 diff --git a/src/Python/download-script.py b/src/Python/download-script.py new file mode 100644 index 0000000..cce88cf --- /dev/null +++ b/src/Python/download-script.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +Download GitHub Actions artifacts for local testing. + +Usage: + python scripts/download_artifacts.py --run-id +""" + +import argparse +import json +import os +import subprocess +import sys +import zipfile +from pathlib import Path +from urllib.request import urlretrieve, urlopen + + +def get_artifacts(repo, run_id, token=None): + """Get list of artifacts from a workflow run.""" + url = f"https://api.github.com/repos/{repo}/actions/runs/{run_id}/artifacts" + + headers = {} + if token: + headers["Authorization"] = f"token {token}" + + req = urlopen(url) + data = json.loads(req.read()) + return data["artifacts"] + + +def download_artifact(artifact, output_dir, token=None): + """Download a single artifact.""" + name = artifact["name"] + url = artifact["archive_download_url"] + + output_path = Path(output_dir) / f"{name}.zip" + + print(f"Downloading {name}...") + + # Use gh CLI if available for authenticated downloads + if token or subprocess.run(["gh", "--version"], capture_output=True).returncode == 0: + subprocess.run([ + "gh", "api", url, + "-H", "Accept: application/vnd.github+json", + "-H", "X-GitHub-Api-Version: 2022-11-28", + "--output", str(output_path) + ]) + else: + urlretrieve(url, output_path) + + # Extract + extract_dir = Path(output_dir) / name + extract_dir.mkdir(exist_ok=True) + + with zipfile.ZipFile(output_path, 'r') as zf: + zf.extractall(extract_dir) + + output_path.unlink() # Delete zip file + print(f" Extracted to {extract_dir}") + + return extract_dir + + +def main(): + parser = argparse.ArgumentParser(description="Download GitHub Actions artifacts") + parser.add_argument("--repo", default="ericwait/hydra-image-processor", + help="GitHub repository (owner/name)") + parser.add_argument("--run-id", required=True, + help="Workflow run ID") + parser.add_argument("--output-dir", default="./downloaded_artifacts", + help="Output directory for artifacts") + parser.add_argument("--token", + help="GitHub personal access token (optional)") + parser.add_argument("--filter", + help="Filter artifacts by name pattern") + + args = parser.parse_args() + + # Create output directory + output_dir = Path(args.output_dir) + output_dir.mkdir(exist_ok=True) + + # Get artifacts + artifacts = get_artifacts(args.repo, args.run_id, args.token) + + if not artifacts: + print("No artifacts found for this run") + return 1 + + print(f"Found {len(artifacts)} artifacts:") + for a in artifacts: + print(f" - {a['name']}") + + # Filter if requested + if args.filter: + artifacts = [a for a in artifacts if args.filter in a["name"]] + print(f"\nFiltered to {len(artifacts)} artifacts") + + # Download artifacts + print("\nDownloading artifacts...") + for artifact in artifacts: + download_artifact(artifact, output_dir, args.token) + + print(f"\nAll artifacts downloaded to {output_dir}") + + # Create test script + test_script = output_dir / "test_import.py" + test_script.write_text(""" +import sys +import os +from pathlib import Path + +# Add each artifact directory to path and try to import +artifact_dirs = [d for d in Path('.').iterdir() if d.is_dir()] + +for dir in artifact_dirs: + print(f"\\nTesting {dir.name}...") + sys.path.insert(0, str(dir)) + + try: + import Hydra + print(f" ✓ Successfully imported Hydra from {dir.name}") + + # Try to call a basic function (will fail if no CUDA) + try: + # info = Hydra.DeviceCount() + print(f" ✓ Module functional") + except Exception as e: + print(f" ⚠ Module imported but CUDA not available: {e}") + + del sys.modules['Hydra'] + except ImportError as e: + print(f" ✗ Failed to import: {e}") + finally: + sys.path.pop(0) +""") + + print(f"\nCreated test script: {test_script}") + print("Run it with: python downloaded_artifacts/test_import.py") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/src/Python/example_usage.py b/src/Python/example_usage.py new file mode 100644 index 0000000..9565028 --- /dev/null +++ b/src/Python/example_usage.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +Example usage of the hydra_image_processor package. + +This script demonstrates various image processing operations available +in the Hydra Image Processor Python package. +""" + +import sys +import numpy as np + +# Add package to path for development +sys.path.insert(0, '.') + +import hydra_image_processor as HIP + + +def print_section(title): + """Print a formatted section header.""" + print("\n" + "=" * 60) + print(title) + print("=" * 60) + + +def example_device_info(): + """Display information about available CUDA devices.""" + print_section("Device Information") + + count = HIP.device_count() + print(f"Number of CUDA devices: {count}") + + if count > 0: + stats = HIP.device_stats() + for i, stat in enumerate(stats): + total_gb = stat['total'] / (1024**3) + avail_gb = stat['available'] / (1024**3) + print(f"\nDevice {i}:") + print(f" Total memory: {total_gb:.2f} GB") + print(f" Available memory: {avail_gb:.2f} GB") + + +def example_basic_filtering(): + """Demonstrate basic filtering operations.""" + print_section("Basic Filtering Operations") + + # Create a test image + print("\nCreating test image (100x100x50)...") + image = np.random.rand(100, 100, 50).astype(np.float32) + print(f"Image shape: {image.shape}, dtype: {image.dtype}") + + # Gaussian smoothing + print("\n1. Applying Gaussian smoothing (sigma=2.0)...") + smoothed = HIP.gaussian(image, sigmas=[2.0, 2.0, 1.0]) + print(f" Output shape: {smoothed.shape}") + + # Mean filter + print("\n2. Applying mean filter (3x3x3 kernel)...") + kernel = np.ones((3, 3, 3), dtype=np.uint8) + mean_result = HIP.mean_filter(image, kernel) + print(f" Output shape: {mean_result.shape}") + + # Median filter + print("\n3. Applying median filter with ball mask (radius=2)...") + ball = HIP.make_ball_mask(radius=2) + print(f" Ball mask shape: {ball.shape}, active voxels: {ball.sum()}") + median_result = HIP.median_filter(image, ball) + print(f" Output shape: {median_result.shape}") + + +def example_morphology(): + """Demonstrate morphological operations.""" + print_section("Morphological Operations") + + # Create binary image + print("\nCreating binary test image (80x80x40)...") + binary = (np.random.rand(80, 80, 40) > 0.6).astype(np.float32) + print(f"Image shape: {binary.shape}") + print(f"Foreground voxels: {binary.sum():.0f} ({100*binary.mean():.1f}%)") + + # Create structuring element + print("\nCreating ellipsoid structuring element...") + se = HIP.make_ellipsoid_mask([3, 3, 2]) + print(f"SE shape: {se.shape}, active elements: {se.sum()}") + + # Morphological closing + print("\nApplying morphological closing...") + closed = HIP.closure(binary, se) + print(f"Result shape: {closed.shape}") + print(f"Foreground after closing: {closed.sum():.0f} ({100*closed.mean():.1f}%)") + + # Morphological opening + print("\nApplying morphological opening...") + opened = HIP.opener(binary, se) + print(f"Result shape: {opened.shape}") + print(f"Foreground after opening: {opened.sum():.0f} ({100*opened.mean():.1f}%)") + + +def example_edge_detection(): + """Demonstrate edge detection operations.""" + print_section("Edge Detection") + + # Create test image with some structure + print("\nCreating structured test image (128x128)...") + x = np.linspace(-5, 5, 128) + y = np.linspace(-5, 5, 128) + X, Y = np.meshgrid(x, y) + image = (np.sin(X) * np.cos(Y)).astype(np.float32) + print(f"Image shape: {image.shape}, dtype: {image.dtype}") + + # Laplacian of Gaussian + print("\nApplying Laplacian of Gaussian (sigma=2.0)...") + edges = HIP.LoG(image, sigmas=[2.0, 2.0, 0.0]) + print(f"Edge map shape: {edges.shape}") + + # High-pass filter + print("\nApplying high-pass filter (sigma=3.0)...") + high_freq = HIP.high_pass_filter(image, sigmas=[3.0, 3.0, 0.0]) + print(f"High-freq shape: {high_freq.shape}") + + +def example_statistics(): + """Demonstrate statistical operations.""" + print_section("Image Statistics") + + # Create test image + print("\nCreating test image...") + image = np.random.rand(50, 50, 25).astype(np.float32) * 100 + print(f"Image shape: {image.shape}") + + # Get min/max + min_val, max_val = HIP.get_min_max(image) + print(f"\nMin value: {min_val:.4f}") + print(f"Max value: {max_val:.4f}") + + # Get sum + total = HIP.sum_array(image) + mean_val = total / image.size + print(f"\nSum: {total:.2f}") + print(f"Mean (calculated): {mean_val:.4f}") + + # Standard deviation filter + print("\nApplying std filter (5x5x5 kernel)...") + kernel = np.ones((5, 5, 5), dtype=np.uint8) + std_result = HIP.std_filter(image, kernel) + local_std_mean = HIP.sum_array(std_result) / std_result.size + print(f"Mean local std: {local_std_mean:.4f}") + + +def example_mask_creation(): + """Demonstrate mask/structuring element creation.""" + print_section("Mask Creation Utilities") + + # Ball mask + print("\nCreating ball masks of various radii...") + for radius in [2, 5, 10]: + ball = HIP.make_ball_mask(radius) + expected_size = 2 * radius + 1 + print(f" Radius {radius}: shape={ball.shape}, " + f"voxels={ball.sum()}, expected_dim={expected_size}") + + # Ellipsoid masks + print("\nCreating ellipsoid masks with different aspect ratios...") + configs = [ + ([5, 5, 5], "Sphere"), + ([10, 5, 2], "Elongated"), + ([5, 5, 0], "Disk (flat in Z)"), + ] + + for axes, description in configs: + mask = HIP.make_ellipsoid_mask(axes) + print(f" {description} {axes}: shape={mask.shape}, voxels={mask.sum()}") + + +def main(): + """Run all examples.""" + print("\n" + "=" * 60) + print(" " * 15 + "Hydra Image Processor") + print(" " * 19 + "Example Usage") + print("=" * 60) + + try: + example_device_info() + example_mask_creation() + example_basic_filtering() + example_morphology() + example_edge_detection() + example_statistics() + + print("\n" + "=" * 60) + print("All examples completed successfully!") + print("=" * 60 + "\n") + + return 0 + + except Exception as e: + print(f"\n[ERROR] Example failed: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/Python/test-hydra.py b/src/Python/test-hydra.py new file mode 100644 index 0000000..fd3c3d3 --- /dev/null +++ b/src/Python/test-hydra.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Test script for the Hydra Image Processor Python module. + +This script tests: +1. Module import +2. Basic CUDA functionality +3. Simple image processing operations +""" + +import sys +import os +import numpy as np + +# Add the directory containing the Hydra module to Python path +module_dir = os.path.dirname(os.path.abspath(__file__)) +if module_dir not in sys.path: + sys.path.insert(0, module_dir) + + +def test_import(): + """Test that the Hydra module can be imported.""" + try: + import Hydra + print("[SUCCESS] Successfully imported Hydra module") + return True + except ImportError as e: + print(f"[FAILED] Failed to import Hydra: {e}") + return False + + +def test_device_info(): + """Test CUDA device detection.""" + import Hydra + + try: + # Get device count + device_count = Hydra.DeviceCount() + print(f"[SUCCESS] Found {device_count} CUDA device(s)") + + # Get device stats + if device_count > 0: + stats = Hydra.DeviceStats() + print(" Device memory info available") + + return True + except Exception as e: + print(f"[WARNING] CUDA not available or error accessing devices: {e}") + return False + + +def test_basic_operation(): + """Test a basic image processing operation.""" + import Hydra + + try: + # Create a simple test image + test_image = np.random.rand(100, 100).astype(np.float32) + + # Try a simple operation (e.g., Gaussian filter) + # Parameters may vary based on your actual API + result = Hydra.Gaussian(test_image, sigma=[2.0, 2.0]) + + print("[SUCCESS] Successfully performed Gaussian filtering") + print(f" Input shape: {test_image.shape}, Output shape: {result.shape}") + + return True + except Exception as e: + print(f"[WARNING] Could not perform image processing: {e}") + return False + + +def test_info(): + """Display available Hydra commands.""" + import Hydra + + try: + info = Hydra.Info() + print(f"[SUCCESS] Hydra module has {len(info)} available commands") + + # Print first few commands as examples + print(" Sample commands:") + for cmd in info[:5]: + if hasattr(cmd, 'command'): + print(f" - {cmd.command}") + + return True + except Exception as e: + print(f"[WARNING] Could not get module info: {e}") + return False + + +def main(): + """ + Run all tests. + """ + print("=" * 60) + print("Hydra Image Processor Module Test") + print("=" * 60) + + # Track test results + results = [] + + # Test 1: Import + print("\n1. Testing module import...") + if not test_import(): + print("\nModule import failed. Cannot continue with other tests.") + return 1 + results.append(True) + + # Test 2: Device info + print("\n2. Testing CUDA device detection...") + results.append(test_device_info()) + + # Test 3: Module info + print("\n3. Testing module information...") + results.append(test_info()) + + # Test 4: Basic operation + print("\n4. Testing basic image processing...") + results.append(test_basic_operation()) + + # Summary + print("\n" + "=" * 60) + print("Test Summary:") + print(f" Passed: {sum(results)}/{len(results)} tests") + + if all(results): + print("\n[SUCCESS] All tests passed successfully!") + else: + print("\n[WARNING] Some tests failed or had warnings.") + print(" This may be expected if CUDA is not available on this system.") + + return 0 if all(results) else 1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/Python/test_import.py b/src/Python/test_import.py new file mode 100644 index 0000000..bf705bd --- /dev/null +++ b/src/Python/test_import.py @@ -0,0 +1,49 @@ +""" +Test script to verify Hydra module imports correctly with bundled OpenMP DLL. + +This demonstrates that the module is portable and can be distributed +with just the Hydra.pyd and libomp140.x86_64.dll files together. +""" + +import os +import sys + + +def test_hydra_import(): + """Test that Hydra imports successfully.""" + print("Testing Hydra import...") + print(f"Python version: {sys.version}") + print(f"Current directory: {os.getcwd()}") + + # Check required files exist + required_files = ['Hydra.pyd', 'libomp140.x86_64.dll'] + print("\nChecking required files:") + for filename in required_files: + exists = os.path.exists(filename) + status = "[OK]" if exists else "[MISSING]" + print(f" {status} {filename}: {exists}") + + # Import Hydra + print("\nImporting Hydra module...") + try: + import Hydra + print("[OK] Successfully imported Hydra!") + print(f" Module location: {Hydra.__file__}") + + # List available functions + funcs = [name for name in dir(Hydra) if not name.startswith('_')] + print(f"\n Available functions ({len(funcs)} total):") + for func in funcs[:5]: + print(f" - {func}") + if len(funcs) > 5: + print(f" ... and {len(funcs) - 5} more") + + return True + except ImportError as e: + print(f"[FAILED] Failed to import Hydra: {e}") + return False + + +if __name__ == "__main__": + success = test_hydra_import() + sys.exit(0 if success else 1) diff --git a/src/Python/test_nonchunking.py b/src/Python/test_nonchunking.py index 7363b60..7202840 100644 --- a/src/Python/test_nonchunking.py +++ b/src/Python/test_nonchunking.py @@ -3,28 +3,28 @@ from skimage import filters import numpy as np -import HIP +import Hydra + -#%% # im = np.random.random((128,128,12,1,1)) -#im = np.random.rand(128, 512) -im = np.zeros((128,256)) -im[20:50,:] = 1 +# im = np.random.rand(128, 512) +im = np.zeros((128, 256)) +im[20:50, :] = 1 print(im.shape) -#%% -#imS = HIP.IdentityFilter(im, 0) -imS = HIP.Gaussian(im, [5,0,0],1,0) + +# imS = Hydra.IdentityFilter(im, 0) +imS = Hydra.Gaussian(im, [5, 0, 0], 1, 0) print(imS.shape) -#%% + fig, axes = plt.subplots(ncols=2, figsize=(8, 2.5)) ax = axes.ravel() ax[0] = plt.subplot(1, 2, 1) ax[1] = plt.subplot(1, 2, 2) -ax[0].imshow(im[:,:], cmap=get_cmap("gray"), interpolation='nearest') -ax[1].imshow(imS[:,:,0,0,0], cmap=get_cmap("gray"), interpolation='nearest') +ax[0].imshow(im[:, :], cmap=get_cmap("gray"), interpolation='nearest') +ax[1].imshow(imS[:, :, 0, 0, 0], cmap=get_cmap("gray"), interpolation='nearest') plt.show() diff --git a/src/Python/test_package.py b/src/Python/test_package.py new file mode 100644 index 0000000..4b47813 --- /dev/null +++ b/src/Python/test_package.py @@ -0,0 +1,176 @@ +""" +Test script for the hydra_image_processor package. + +This script demonstrates basic usage and tests key functionality of the +Pythonic wrapper around Hydra Image Processor. +""" + +import sys +import numpy as np + +# Add current directory to path for testing +sys.path.insert(0, '.') + +import hydra_image_processor as HIP + + +def test_import(): + """Test that the package imports correctly.""" + print("=" * 60) + print("Test 1: Package Import") + print("=" * 60) + print("[OK] Package imported successfully") + print(f" Version: {HIP.__version__}") + print(f" Author: {HIP.__author__}") + print(f" Available functions: {len(HIP.__all__)}") + print() + return True + + +def test_device_info(): + """Test device information functions.""" + print("=" * 60) + print("Test 2: Device Information") + print("=" * 60) + try: + count = HIP.device_count() + print(f"[OK] Found {count} CUDA device(s)") + + if count > 0: + stats = HIP.device_stats() + print(f" Device statistics available for {len(stats)} device(s)") + + config = HIP.check_config() + print(" Library configuration retrieved") + + commands = HIP.info() + print(f" Available commands: {len(commands)}") + + return True + except Exception as e: + print(f"[FAILED] Device info test failed: {e}") + return False + finally: + print() + + +def test_utility_functions(): + """Test utility functions like mask creation.""" + print("=" * 60) + print("Test 3: Utility Functions") + print("=" * 60) + try: + # Test ball mask creation + ball = HIP.make_ball_mask(radius=5) + print(f"[OK] Created ball mask: shape={ball.shape}, dtype={ball.dtype}") + print(f" Voxels in ball: {ball.sum()}") + + # Test ellipsoid mask creation + ellipsoid = HIP.make_ellipsoid_mask([5, 3, 2]) + print(f"[OK] Created ellipsoid mask: shape={ellipsoid.shape}, dtype={ellipsoid.dtype}") + print(f" Voxels in ellipsoid: {ellipsoid.sum()}") + + return True + except Exception as e: + print(f"[FAILED] Utility functions test failed: {e}") + import traceback + traceback.print_exc() + return False + finally: + print() + + +def test_image_processing(): + """Test basic image processing operations.""" + print("=" * 60) + print("Test 4: Image Processing Operations") + print("=" * 60) + try: + # Create test image + image = np.random.rand(50, 50, 20).astype(np.float32) + print(f"Created test image: shape={image.shape}, dtype={image.dtype}") + + # Test Gaussian filtering + print("Testing Gaussian filter...") + smoothed = HIP.gaussian(image, sigmas=[2.0, 2.0, 1.0]) + print(f"[OK] Gaussian filter: input {image.shape} -> output {smoothed.shape}") + + # Test mean filter with small kernel + print("Testing mean filter...") + kernel = np.ones((3, 3, 3), dtype=np.uint8) + mean_filtered = HIP.mean_filter(image, kernel, num_iterations=1) + print(f"[OK] Mean filter: input {image.shape} -> output {mean_filtered.shape}") + + # Test identity filter (simple passthrough) + print("Testing identity filter...") + identity = HIP.identity_filter(image) + print(f"[OK] Identity filter: input {image.shape} -> output {identity.shape}") + + return True + except Exception as e: + print(f"[FAILED] Image processing test failed: {e}") + import traceback + traceback.print_exc() + return False + finally: + print() + + +def test_help_system(): + """Test the help system.""" + print("=" * 60) + print("Test 5: Help System") + print("=" * 60) + try: + # Test getting help for a specific command + # Note: Help() prints directly to stdout and returns None + print("Calling HIP.help('Gaussian')...") + print("-" * 60) + HIP.help('Gaussian') + print("-" * 60) + print("[OK] Help function executed successfully") + print(" (Help text printed above)") + + return True + except Exception as e: + print(f"[FAILED] Help system test failed: {e}") + return False + finally: + print() + + +def main(): + """Run all tests.""" + print("\n") + print("=" * 60) + print(" " * 10 + "Hydra Image Processor Package Test") + print("=" * 60) + print() + + results = [] + + # Run tests + results.append(test_import()) + results.append(test_device_info()) + results.append(test_utility_functions()) + results.append(test_image_processing()) + results.append(test_help_system()) + + # Summary + print("=" * 60) + print("Test Summary") + print("=" * 60) + passed = sum(results) + total = len(results) + print(f"Passed: {passed}/{total} tests") + + if all(results): + print("\n[OK] All tests passed successfully!") + return 0 + else: + print("\n[FAILED] Some tests failed.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) From 8d3d0b8140fa3513d51317fefd58f41a8c4885ae Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:13:05 -0600 Subject: [PATCH 15/40] build: add cmake presets and dll dependency helper script --- CMakePresets.json | 27 +++++++++++++++++++++++++++ check_dll_deps.ps1 | 3 +++ 2 files changed, 30 insertions(+) create mode 100644 CMakePresets.json create mode 100644 check_dll_deps.ps1 diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..85e7432 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,27 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "VS64", + "displayName": "Visual Studio Community 2022 Release - amd64", + "description": "Using compilers for Visual Studio 17 2022 (x64 architecture)", + "generator": "Visual Studio 17 2022", + "toolset": "host=x64", + "architecture": "x64", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe" + } + } + ], + "buildPresets": [ + { + "name": "VS64-debug", + "displayName": "Visual Studio Community 2022 Release - amd64 - Debug", + "configurePreset": "VS64", + "configuration": "Debug" + } + ] +} \ No newline at end of file diff --git a/check_dll_deps.ps1 b/check_dll_deps.ps1 new file mode 100644 index 0000000..3645f39 --- /dev/null +++ b/check_dll_deps.ps1 @@ -0,0 +1,3 @@ +$dll = "e:\programming\hydra-image-processor\src\Python\Hydra.pyd" +$result = & "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x64\dumpbin.exe" /DEPENDENTS $dll +Write-Output $result From 7119c0bcecd825a8a2fb6c3766b65181f2f8937f Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:16:13 -0600 Subject: [PATCH 16/40] feat(matlab): add new +Hydra package wrapper --- src/MATLAB/+Hydra/@Cuda/CheckConfig.m | 7 +++++ src/MATLAB/+Hydra/@Cuda/Closure.m | 22 +++++++++++++ src/MATLAB/+Hydra/@Cuda/Cuda.m | 31 +++++++++++++++++++ src/MATLAB/+Hydra/@Cuda/DeviceCount.m | 9 ++++++ src/MATLAB/+Hydra/@Cuda/DeviceStats.m | 7 +++++ .../+Hydra/@Cuda/ElementWiseDifference.m | 18 +++++++++++ src/MATLAB/+Hydra/@Cuda/EntropyFilter.m | 19 ++++++++++++ src/MATLAB/+Hydra/@Cuda/Gaussian.m | 22 +++++++++++++ src/MATLAB/+Hydra/@Cuda/GetMinMax.m | 14 +++++++++ src/MATLAB/+Hydra/@Cuda/Help.m | 5 +++ src/MATLAB/+Hydra/@Cuda/HighPassFilter.m | 18 +++++++++++ src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 | 3 ++ src/MATLAB/+Hydra/@Cuda/IdentityFilter.m | 15 +++++++++ src/MATLAB/+Hydra/@Cuda/Info.m | 11 +++++++ src/MATLAB/+Hydra/@Cuda/LoG.m | 18 +++++++++++ src/MATLAB/+Hydra/@Cuda/MaxFilter.m | 23 ++++++++++++++ src/MATLAB/+Hydra/@Cuda/MeanFilter.m | 23 ++++++++++++++ src/MATLAB/+Hydra/@Cuda/MedianFilter.m | 23 ++++++++++++++ src/MATLAB/+Hydra/@Cuda/MinFilter.m | 23 ++++++++++++++ src/MATLAB/+Hydra/@Cuda/MultiplySum.m | 23 ++++++++++++++ src/MATLAB/+Hydra/@Cuda/NLMeans.m | 15 +++++++++ src/MATLAB/+Hydra/@Cuda/Opener.m | 23 ++++++++++++++ src/MATLAB/+Hydra/@Cuda/StdFilter.m | 23 ++++++++++++++ src/MATLAB/+Hydra/@Cuda/Sum.m | 15 +++++++++ src/MATLAB/+Hydra/@Cuda/VarFilter.m | 23 ++++++++++++++ src/MATLAB/+Hydra/@Cuda/WienerFilter.m | 23 ++++++++++++++ src/MATLAB/+Hydra/CheckConfig.m | 12 +++++++ src/MATLAB/+Hydra/Closure.m | 27 ++++++++++++++++ src/MATLAB/+Hydra/ElementWiseDifference.m | 23 ++++++++++++++ src/MATLAB/+Hydra/EntropyFilter.m | 24 ++++++++++++++ src/MATLAB/+Hydra/Gaussian.m | 27 ++++++++++++++++ src/MATLAB/+Hydra/GetMinMax.m | 19 ++++++++++++ src/MATLAB/+Hydra/Help.m | 5 +++ src/MATLAB/+Hydra/HighPassFilter.m | 23 ++++++++++++++ src/MATLAB/+Hydra/IdentityFilter.m | 20 ++++++++++++ src/MATLAB/+Hydra/Info.m | 16 ++++++++++ src/MATLAB/+Hydra/LoG.m | 23 ++++++++++++++ src/MATLAB/+Hydra/MaxFilter.m | 28 +++++++++++++++++ src/MATLAB/+Hydra/MeanFilter.m | 28 +++++++++++++++++ src/MATLAB/+Hydra/MedianFilter.m | 28 +++++++++++++++++ src/MATLAB/+Hydra/MinFilter.m | 28 +++++++++++++++++ src/MATLAB/+Hydra/MultiplySum.m | 28 +++++++++++++++++ src/MATLAB/+Hydra/NLMeans.m | 20 ++++++++++++ src/MATLAB/+Hydra/Opener.m | 28 +++++++++++++++++ src/MATLAB/+Hydra/StdFilter.m | 28 +++++++++++++++++ src/MATLAB/+Hydra/Sum.m | 20 ++++++++++++ src/MATLAB/+Hydra/VarFilter.m | 28 +++++++++++++++++ src/MATLAB/+Hydra/WienerFilter.m | 28 +++++++++++++++++ 48 files changed, 967 insertions(+) create mode 100644 src/MATLAB/+Hydra/@Cuda/CheckConfig.m create mode 100644 src/MATLAB/+Hydra/@Cuda/Closure.m create mode 100644 src/MATLAB/+Hydra/@Cuda/Cuda.m create mode 100644 src/MATLAB/+Hydra/@Cuda/DeviceCount.m create mode 100644 src/MATLAB/+Hydra/@Cuda/DeviceStats.m create mode 100644 src/MATLAB/+Hydra/@Cuda/ElementWiseDifference.m create mode 100644 src/MATLAB/+Hydra/@Cuda/EntropyFilter.m create mode 100644 src/MATLAB/+Hydra/@Cuda/Gaussian.m create mode 100644 src/MATLAB/+Hydra/@Cuda/GetMinMax.m create mode 100644 src/MATLAB/+Hydra/@Cuda/Help.m create mode 100644 src/MATLAB/+Hydra/@Cuda/HighPassFilter.m create mode 100644 src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 create mode 100644 src/MATLAB/+Hydra/@Cuda/IdentityFilter.m create mode 100644 src/MATLAB/+Hydra/@Cuda/Info.m create mode 100644 src/MATLAB/+Hydra/@Cuda/LoG.m create mode 100644 src/MATLAB/+Hydra/@Cuda/MaxFilter.m create mode 100644 src/MATLAB/+Hydra/@Cuda/MeanFilter.m create mode 100644 src/MATLAB/+Hydra/@Cuda/MedianFilter.m create mode 100644 src/MATLAB/+Hydra/@Cuda/MinFilter.m create mode 100644 src/MATLAB/+Hydra/@Cuda/MultiplySum.m create mode 100644 src/MATLAB/+Hydra/@Cuda/NLMeans.m create mode 100644 src/MATLAB/+Hydra/@Cuda/Opener.m create mode 100644 src/MATLAB/+Hydra/@Cuda/StdFilter.m create mode 100644 src/MATLAB/+Hydra/@Cuda/Sum.m create mode 100644 src/MATLAB/+Hydra/@Cuda/VarFilter.m create mode 100644 src/MATLAB/+Hydra/@Cuda/WienerFilter.m create mode 100644 src/MATLAB/+Hydra/CheckConfig.m create mode 100644 src/MATLAB/+Hydra/Closure.m create mode 100644 src/MATLAB/+Hydra/ElementWiseDifference.m create mode 100644 src/MATLAB/+Hydra/EntropyFilter.m create mode 100644 src/MATLAB/+Hydra/Gaussian.m create mode 100644 src/MATLAB/+Hydra/GetMinMax.m create mode 100644 src/MATLAB/+Hydra/Help.m create mode 100644 src/MATLAB/+Hydra/HighPassFilter.m create mode 100644 src/MATLAB/+Hydra/IdentityFilter.m create mode 100644 src/MATLAB/+Hydra/Info.m create mode 100644 src/MATLAB/+Hydra/LoG.m create mode 100644 src/MATLAB/+Hydra/MaxFilter.m create mode 100644 src/MATLAB/+Hydra/MeanFilter.m create mode 100644 src/MATLAB/+Hydra/MedianFilter.m create mode 100644 src/MATLAB/+Hydra/MinFilter.m create mode 100644 src/MATLAB/+Hydra/MultiplySum.m create mode 100644 src/MATLAB/+Hydra/NLMeans.m create mode 100644 src/MATLAB/+Hydra/Opener.m create mode 100644 src/MATLAB/+Hydra/StdFilter.m create mode 100644 src/MATLAB/+Hydra/Sum.m create mode 100644 src/MATLAB/+Hydra/VarFilter.m create mode 100644 src/MATLAB/+Hydra/WienerFilter.m diff --git a/src/MATLAB/+Hydra/@Cuda/CheckConfig.m b/src/MATLAB/+Hydra/@Cuda/CheckConfig.m new file mode 100644 index 0000000..473d05c --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/CheckConfig.m @@ -0,0 +1,7 @@ +% CheckConfig - Get Hydra library configuration information. +% [hydraConfig] = Hydra.Cuda.CheckConfig() +% Returns hydraConfig structure with configuration information. +% +function [hydraConfig] = CheckConfig() + [hydraConfig] = Hydra.Cuda.Hydra('CheckConfig'); +end diff --git a/src/MATLAB/+Hydra/@Cuda/Closure.m b/src/MATLAB/+Hydra/@Cuda/Closure.m new file mode 100644 index 0000000..4e967ca --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/Closure.m @@ -0,0 +1,22 @@ +% Closure - This kernel will apply a dilation followed by an erosion. +% [imageOut] = Hydra.Cuda.Closure(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +function [imageOut] = Closure(imageIn,kernel,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('Closure',imageIn,kernel,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/Cuda.m b/src/MATLAB/+Hydra/@Cuda/Cuda.m new file mode 100644 index 0000000..05993d9 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/Cuda.m @@ -0,0 +1,31 @@ +classdef (Abstract,Sealed) Cuda +methods (Static) + [hydraConfig] = CheckConfig() + [imageOut] = Closure(imageIn,kernel,numIterations,device) + [numCudaDevices,memStats] = DeviceCount() + [deviceStatsArray] = DeviceStats() + [imageOut] = ElementWiseDifference(image1In,image2In,device) + [imageOut] = EntropyFilter(imageIn,kernel,device) + [imageOut] = Gaussian(imageIn,sigmas,numIterations,device) + [minVal,maxVal] = GetMinMax(imageIn,device) + Help(command) + [imageOut] = HighPassFilter(imageIn,sigmas,device) + [imageOut] = IdentityFilter(imageIn,device) + [cmdInfo] = Info() + [imageOut] = LoG(imageIn,sigmas,device) + [imageOut] = MaxFilter(imageIn,kernel,numIterations,device) + [imageOut] = MeanFilter(imageIn,kernel,numIterations,device) + [imageOut] = MedianFilter(imageIn,kernel,numIterations,device) + [imageOut] = MinFilter(imageIn,kernel,numIterations,device) + [imageOut] = MultiplySum(imageIn,kernel,numIterations,device) + [imageOut] = NLMeans(imageIn,h,searchWindowRadius,nhoodRadius,device) + [imageOut] = Opener(imageIn,kernel,numIterations,device) + [imageOut] = StdFilter(imageIn,kernel,numIterations,device) + [imageOut] = Sum(imageIn,device) + [imageOut] = VarFilter(imageIn,kernel,numIterations,device) + [imageOut] = WienerFilter(imageIn,kernel,noiseVariance,device) +end +methods (Static, Access = private) + varargout = Hydra(command, varargin) +end +end diff --git a/src/MATLAB/+Hydra/@Cuda/DeviceCount.m b/src/MATLAB/+Hydra/@Cuda/DeviceCount.m new file mode 100644 index 0000000..3522ebc --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/DeviceCount.m @@ -0,0 +1,9 @@ +% DeviceCount - This will return the number of Cuda devices available, and their memory. +% [numCudaDevices,memStats] = Hydra.Cuda.DeviceCount() +% NumCudaDevices -- this is the number of Cuda devices available. +% MemoryStats -- this is an array of structures where each entry corresponds to a Cuda device. +% The memory structure contains the total memory on the device and the memory available for a Cuda call. +% +function [numCudaDevices,memStats] = DeviceCount() + [numCudaDevices,memStats] = Hydra.Cuda.Hydra('DeviceCount'); +end diff --git a/src/MATLAB/+Hydra/@Cuda/DeviceStats.m b/src/MATLAB/+Hydra/@Cuda/DeviceStats.m new file mode 100644 index 0000000..37f7d2d --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/DeviceStats.m @@ -0,0 +1,7 @@ +% DeviceStats - This will return the statistics of each Cuda capable device installed. +% [deviceStatsArray] = Hydra.Cuda.DeviceStats() +% DeviceStatsArray -- this is an array of structs, one struct per device. +% The struct has these fields: name, major, minor, constMem, sharedMem, totalMem, tccDriver, mpCount, threadsPerMP, warpSize, maxThreads. +function [deviceStatsArray] = DeviceStats() + [deviceStatsArray] = Hydra.Cuda.Hydra('DeviceStats'); +end diff --git a/src/MATLAB/+Hydra/@Cuda/ElementWiseDifference.m b/src/MATLAB/+Hydra/@Cuda/ElementWiseDifference.m new file mode 100644 index 0000000..a4a4b95 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/ElementWiseDifference.m @@ -0,0 +1,18 @@ +% ElementWiseDifference - This subtracts the second array from the first, element by element (A-B). +% [imageOut] = Hydra.Cuda.ElementWiseDifference(image1In,image2In,[device]) +% image1In = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% image2In = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +function [imageOut] = ElementWiseDifference(image1In,image2In,device) + [imageOut] = Hydra.Cuda.Hydra('ElementWiseDifference',image1In,image2In,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/EntropyFilter.m b/src/MATLAB/+Hydra/@Cuda/EntropyFilter.m new file mode 100644 index 0000000..cf2d7f1 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/EntropyFilter.m @@ -0,0 +1,19 @@ +% EntropyFilter - This calculates the entropy within the neighborhood given by the kernel. +% [imageOut] = Hydra.Cuda.EntropyFilter(imageIn,kernel,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = EntropyFilter(imageIn,kernel,device) + [imageOut] = Hydra.Cuda.Hydra('EntropyFilter',imageIn,kernel,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/Gaussian.m b/src/MATLAB/+Hydra/@Cuda/Gaussian.m new file mode 100644 index 0000000..adbfbfc --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/Gaussian.m @@ -0,0 +1,22 @@ +% Gaussian - Gaussian smoothing. +% [imageOut] = Hydra.Cuda.Gaussian(imageIn,sigmas,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% Sigmas = This should be an array of three positive values that represent the standard deviation of a Gaussian curve. +% Zeros (0) in this array will not smooth in that direction. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = Gaussian(imageIn,sigmas,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('Gaussian',imageIn,sigmas,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/GetMinMax.m b/src/MATLAB/+Hydra/@Cuda/GetMinMax.m new file mode 100644 index 0000000..ea33b06 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/GetMinMax.m @@ -0,0 +1,14 @@ +% GetMinMax - This function finds the lowest and highest value in the array that is passed in. +% [minVal,maxVal] = Hydra.Cuda.GetMinMax(imageIn,[device]) +% imageIn = This is a one to five dimensional array. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% minValue = This is the lowest value found in the array. +% maxValue = This is the highest value found in the array. +% +function [minVal,maxVal] = GetMinMax(imageIn,device) + [minVal,maxVal] = Hydra.Cuda.Hydra('GetMinMax',imageIn,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/Help.m b/src/MATLAB/+Hydra/@Cuda/Help.m new file mode 100644 index 0000000..af64f9a --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/Help.m @@ -0,0 +1,5 @@ +% Help - Print detailed usage information for the specified command. +% Hydra.Cuda.Help([command]) +function Help(command) + Hydra.Cuda.Hydra('Help',command); +end diff --git a/src/MATLAB/+Hydra/@Cuda/HighPassFilter.m b/src/MATLAB/+Hydra/@Cuda/HighPassFilter.m new file mode 100644 index 0000000..c31a295 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/HighPassFilter.m @@ -0,0 +1,18 @@ +% HighPassFilter - Filters out low frequency by subtracting a Gaussian blurred version of the input based on the sigmas provided. +% [imageOut] = Hydra.Cuda.HighPassFilter(imageIn,sigmas,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% Sigmas = This should be an array of three positive values that represent the standard deviation of a Gaussian curve. +% Zeros (0) in this array will not smooth in that direction. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = HighPassFilter(imageIn,sigmas,device) + [imageOut] = Hydra.Cuda.Hydra('HighPassFilter',imageIn,sigmas,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 b/src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 new file mode 100644 index 0000000..3dc7331 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/Hydra.mexw64 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c81574d8a5edca96aa6555166f863d5c4007c30b552c243ba7a30ec759b0a46 +size 13505024 diff --git a/src/MATLAB/+Hydra/@Cuda/IdentityFilter.m b/src/MATLAB/+Hydra/@Cuda/IdentityFilter.m new file mode 100644 index 0000000..609b1e3 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/IdentityFilter.m @@ -0,0 +1,15 @@ +% IdentityFilter - Identity Filter for testing. Copies image data to GPU memory and back into output image. +% [imageOut] = Hydra.Cuda.IdentityFilter(imageIn,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = IdentityFilter(imageIn,device) + [imageOut] = Hydra.Cuda.Hydra('IdentityFilter',imageIn,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/Info.m b/src/MATLAB/+Hydra/@Cuda/Info.m new file mode 100644 index 0000000..9015fe6 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/Info.m @@ -0,0 +1,11 @@ +% Info - Get information on all available mex commands. +% [cmdInfo] = Hydra.Cuda.Info() +% Returns commandInfo structure array containing information on all mex commands. +% commandInfo.command - Command string +% commandInfo.outArgs - Comma-delimited string list of output arguments +% commandInfo.inArgs - Comma-delimited string list of input arguments +% commandInfo.helpLines - Help string +% +function [cmdInfo] = Info() + [cmdInfo] = Hydra.Cuda.Hydra('Info'); +end diff --git a/src/MATLAB/+Hydra/@Cuda/LoG.m b/src/MATLAB/+Hydra/@Cuda/LoG.m new file mode 100644 index 0000000..55cdd3a --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/LoG.m @@ -0,0 +1,18 @@ +% LoG - Apply a Lapplacian of Gaussian filter with the given sigmas. +% [imageOut] = Hydra.Cuda.LoG(imageIn,sigmas,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% Sigmas = This should be an array of three positive values that represent the standard deviation of a Gaussian curve. +% Zeros (0) in this array will not smooth in that direction. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = LoG(imageIn,sigmas,device) + [imageOut] = Hydra.Cuda.Hydra('LoG',imageIn,sigmas,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/MaxFilter.m b/src/MATLAB/+Hydra/@Cuda/MaxFilter.m new file mode 100644 index 0000000..462a228 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/MaxFilter.m @@ -0,0 +1,23 @@ +% MaxFilter - This will set each pixel/voxel to the max value of the neighborhood defined by the given kernel. +% [imageOut] = Hydra.Cuda.MaxFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MaxFilter(imageIn,kernel,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('MaxFilter',imageIn,kernel,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/MeanFilter.m b/src/MATLAB/+Hydra/@Cuda/MeanFilter.m new file mode 100644 index 0000000..fb75f9a --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/MeanFilter.m @@ -0,0 +1,23 @@ +% MeanFilter - This will take the mean of the given neighborhood. +% [imageOut] = Hydra.Cuda.MeanFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MeanFilter(imageIn,kernel,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('MeanFilter',imageIn,kernel,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/MedianFilter.m b/src/MATLAB/+Hydra/@Cuda/MedianFilter.m new file mode 100644 index 0000000..65eb726 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/MedianFilter.m @@ -0,0 +1,23 @@ +% MedianFilter - This will calculate the median for each neighborhood defined by the kernel. +% [imageOut] = Hydra.Cuda.MedianFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MedianFilter(imageIn,kernel,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('MedianFilter',imageIn,kernel,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/MinFilter.m b/src/MATLAB/+Hydra/@Cuda/MinFilter.m new file mode 100644 index 0000000..db56ef9 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/MinFilter.m @@ -0,0 +1,23 @@ +% MinFilter - This will set each pixel/voxel to the max value of the neighborhood defined by the given kernel. +% [imageOut] = Hydra.Cuda.MinFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MinFilter(imageIn,kernel,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('MinFilter',imageIn,kernel,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/MultiplySum.m b/src/MATLAB/+Hydra/@Cuda/MultiplySum.m new file mode 100644 index 0000000..56cabb1 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/MultiplySum.m @@ -0,0 +1,23 @@ +% MultiplySum - Multiplies the kernel with the neighboring values and sums these new values. +% [imageOut] = Hydra.Cuda.MultiplySum(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MultiplySum(imageIn,kernel,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('MultiplySum',imageIn,kernel,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/NLMeans.m b/src/MATLAB/+Hydra/@Cuda/NLMeans.m new file mode 100644 index 0000000..c97224c --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/NLMeans.m @@ -0,0 +1,15 @@ +% NLMeans - Apply an approximate non-local means filter using patch mean and covariance with Fisher discrminant distance +% [imageOut] = Hydra.Cuda.NLMeans(imageIn,h,[searchWindowRadius],[nhoodRadius],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% h = weighting applied to patch difference function. typically e.g. 0.05-0.1. controls the amount of smoothing. +% +% searchWindowRadius = radius of region to locate patches at. +% +% nhoodRadius = radius of patch size (comparison window). +% +function [imageOut] = NLMeans(imageIn,h,searchWindowRadius,nhoodRadius,device) + [imageOut] = Hydra.Cuda.Hydra('NLMeans',imageIn,h,searchWindowRadius,nhoodRadius,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/Opener.m b/src/MATLAB/+Hydra/@Cuda/Opener.m new file mode 100644 index 0000000..17862e3 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/Opener.m @@ -0,0 +1,23 @@ +% Opener - This kernel will erode follow by a dilation. +% [imageOut] = Hydra.Cuda.Opener(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = Opener(imageIn,kernel,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('Opener',imageIn,kernel,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/StdFilter.m b/src/MATLAB/+Hydra/@Cuda/StdFilter.m new file mode 100644 index 0000000..e685d22 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/StdFilter.m @@ -0,0 +1,23 @@ +% StdFilter - This will take the standard deviation of the given neighborhood. +% [imageOut] = Hydra.Cuda.StdFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = StdFilter(imageIn,kernel,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('StdFilter',imageIn,kernel,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/Sum.m b/src/MATLAB/+Hydra/@Cuda/Sum.m new file mode 100644 index 0000000..eb401fe --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/Sum.m @@ -0,0 +1,15 @@ +% Sum - This sums up the entire array in. +% [imageOut] = Hydra.Cuda.Sum(imageIn,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% valueOut = This is the summation of the entire array. +% +function [imageOut] = Sum(imageIn,device) + [imageOut] = Hydra.Cuda.Hydra('Sum',imageIn,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/VarFilter.m b/src/MATLAB/+Hydra/@Cuda/VarFilter.m new file mode 100644 index 0000000..2c2d955 --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/VarFilter.m @@ -0,0 +1,23 @@ +% VarFilter - This will take the variance deviation of the given neighborhood. +% [imageOut] = Hydra.Cuda.VarFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = VarFilter(imageIn,kernel,numIterations,device) + [imageOut] = Hydra.Cuda.Hydra('VarFilter',imageIn,kernel,numIterations,device); +end diff --git a/src/MATLAB/+Hydra/@Cuda/WienerFilter.m b/src/MATLAB/+Hydra/@Cuda/WienerFilter.m new file mode 100644 index 0000000..720499d --- /dev/null +++ b/src/MATLAB/+Hydra/@Cuda/WienerFilter.m @@ -0,0 +1,23 @@ +% WienerFilter - A Wiener filter aims to denoise an image in a linear fashion. +% [imageOut] = Hydra.Cuda.WienerFilter(imageIn,kernel,[noiseVariance],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel (optional) = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the neighborhood. +% This can be an empty array [] and which will use a 3x3x3 neighborhood (or equivalent given input dimension). +% +% noiseVariance (optional) = This is the expected variance of the noise. +% This should be a scalar value or an empty array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = WienerFilter(imageIn,kernel,noiseVariance,device) + [imageOut] = Hydra.Cuda.Hydra('WienerFilter',imageIn,kernel,noiseVariance,device); +end diff --git a/src/MATLAB/+Hydra/CheckConfig.m b/src/MATLAB/+Hydra/CheckConfig.m new file mode 100644 index 0000000..7d480f5 --- /dev/null +++ b/src/MATLAB/+Hydra/CheckConfig.m @@ -0,0 +1,12 @@ +% CheckConfig - Get Hydra library configuration information. +% [hydraConfig] = Hydra.CheckConfig() +% Returns hydraConfig structure with configuration information. +% +function [hydraConfig] = CheckConfig() + try + [hydraConfig] = HIP.Cuda.CheckConfig(); + catch errMsg + warning(errMsg.message); + [hydraConfig] = HIP.Local.CheckConfig(); + end +end diff --git a/src/MATLAB/+Hydra/Closure.m b/src/MATLAB/+Hydra/Closure.m new file mode 100644 index 0000000..6e4adb3 --- /dev/null +++ b/src/MATLAB/+Hydra/Closure.m @@ -0,0 +1,27 @@ +% Closure - This kernel will apply a dilation followed by an erosion. +% [imageOut] = Hydra.Closure(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +function [imageOut] = Closure(imageIn,kernel,numIterations,device) + try + [imageOut] = HIP.Cuda.Closure(imageIn,kernel,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.Closure(imageIn,kernel,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/ElementWiseDifference.m b/src/MATLAB/+Hydra/ElementWiseDifference.m new file mode 100644 index 0000000..c0e7355 --- /dev/null +++ b/src/MATLAB/+Hydra/ElementWiseDifference.m @@ -0,0 +1,23 @@ +% ElementWiseDifference - This subtracts the second array from the first, element by element (A-B). +% [imageOut] = Hydra.ElementWiseDifference(image1In,image2In,[device]) +% image1In = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% image2In = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +function [imageOut] = ElementWiseDifference(image1In,image2In,device) + try + [imageOut] = HIP.Cuda.ElementWiseDifference(image1In,image2In,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.ElementWiseDifference(image1In,image2In,device); + end +end diff --git a/src/MATLAB/+Hydra/EntropyFilter.m b/src/MATLAB/+Hydra/EntropyFilter.m new file mode 100644 index 0000000..08f26a8 --- /dev/null +++ b/src/MATLAB/+Hydra/EntropyFilter.m @@ -0,0 +1,24 @@ +% EntropyFilter - This calculates the entropy within the neighborhood given by the kernel. +% [imageOut] = Hydra.EntropyFilter(imageIn,kernel,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = EntropyFilter(imageIn,kernel,device) + try + [imageOut] = HIP.Cuda.EntropyFilter(imageIn,kernel,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.EntropyFilter(imageIn,kernel,device); + end +end diff --git a/src/MATLAB/+Hydra/Gaussian.m b/src/MATLAB/+Hydra/Gaussian.m new file mode 100644 index 0000000..67cdd5b --- /dev/null +++ b/src/MATLAB/+Hydra/Gaussian.m @@ -0,0 +1,27 @@ +% Gaussian - Gaussian smoothing. +% [imageOut] = Hydra.Gaussian(imageIn,sigmas,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% Sigmas = This should be an array of three positive values that represent the standard deviation of a Gaussian curve. +% Zeros (0) in this array will not smooth in that direction. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = Gaussian(imageIn,sigmas,numIterations,device) + try + [imageOut] = HIP.Cuda.Gaussian(imageIn,sigmas,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.Gaussian(imageIn,sigmas,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/GetMinMax.m b/src/MATLAB/+Hydra/GetMinMax.m new file mode 100644 index 0000000..04efe04 --- /dev/null +++ b/src/MATLAB/+Hydra/GetMinMax.m @@ -0,0 +1,19 @@ +% GetMinMax - This function finds the lowest and highest value in the array that is passed in. +% [minVal,maxVal] = Hydra.GetMinMax(imageIn,[device]) +% imageIn = This is a one to five dimensional array. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% minValue = This is the lowest value found in the array. +% maxValue = This is the highest value found in the array. +% +function [minVal,maxVal] = GetMinMax(imageIn,device) + try + [minVal,maxVal] = HIP.Cuda.GetMinMax(imageIn,device); + catch errMsg + warning(errMsg.message); + [minVal,maxVal] = HIP.Local.GetMinMax(imageIn,device); + end +end diff --git a/src/MATLAB/+Hydra/Help.m b/src/MATLAB/+Hydra/Help.m new file mode 100644 index 0000000..6428639 --- /dev/null +++ b/src/MATLAB/+Hydra/Help.m @@ -0,0 +1,5 @@ +% Help - Print detailed usage information for the specified command. +% Hydra.Help([command]) +function Help(command) + Hydra.Cuda.Hydra('Help',command); +end diff --git a/src/MATLAB/+Hydra/HighPassFilter.m b/src/MATLAB/+Hydra/HighPassFilter.m new file mode 100644 index 0000000..a3cf864 --- /dev/null +++ b/src/MATLAB/+Hydra/HighPassFilter.m @@ -0,0 +1,23 @@ +% HighPassFilter - Filters out low frequency by subtracting a Gaussian blurred version of the input based on the sigmas provided. +% [imageOut] = Hydra.HighPassFilter(imageIn,sigmas,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% Sigmas = This should be an array of three positive values that represent the standard deviation of a Gaussian curve. +% Zeros (0) in this array will not smooth in that direction. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = HighPassFilter(imageIn,sigmas,device) + try + [imageOut] = HIP.Cuda.HighPassFilter(imageIn,sigmas,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.HighPassFilter(imageIn,sigmas,device); + end +end diff --git a/src/MATLAB/+Hydra/IdentityFilter.m b/src/MATLAB/+Hydra/IdentityFilter.m new file mode 100644 index 0000000..ce6b9d8 --- /dev/null +++ b/src/MATLAB/+Hydra/IdentityFilter.m @@ -0,0 +1,20 @@ +% IdentityFilter - Identity Filter for testing. Copies image data to GPU memory and back into output image. +% [imageOut] = Hydra.IdentityFilter(imageIn,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = IdentityFilter(imageIn,device) + try + [imageOut] = HIP.Cuda.IdentityFilter(imageIn,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.IdentityFilter(imageIn,device); + end +end diff --git a/src/MATLAB/+Hydra/Info.m b/src/MATLAB/+Hydra/Info.m new file mode 100644 index 0000000..c459403 --- /dev/null +++ b/src/MATLAB/+Hydra/Info.m @@ -0,0 +1,16 @@ +% Info - Get information on all available mex commands. +% [cmdInfo] = Hydra.Info() +% Returns commandInfo structure array containing information on all mex commands. +% commandInfo.command - Command string +% commandInfo.outArgs - Comma-delimited string list of output arguments +% commandInfo.inArgs - Comma-delimited string list of input arguments +% commandInfo.helpLines - Help string +% +function [cmdInfo] = Info() + try + [cmdInfo] = HIP.Cuda.Info(); + catch errMsg + warning(errMsg.message); + [cmdInfo] = HIP.Local.Info(); + end +end diff --git a/src/MATLAB/+Hydra/LoG.m b/src/MATLAB/+Hydra/LoG.m new file mode 100644 index 0000000..3267f9b --- /dev/null +++ b/src/MATLAB/+Hydra/LoG.m @@ -0,0 +1,23 @@ +% LoG - Apply a Lapplacian of Gaussian filter with the given sigmas. +% [imageOut] = Hydra.LoG(imageIn,sigmas,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% Sigmas = This should be an array of three positive values that represent the standard deviation of a Gaussian curve. +% Zeros (0) in this array will not smooth in that direction. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = LoG(imageIn,sigmas,device) + try + [imageOut] = HIP.Cuda.LoG(imageIn,sigmas,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.LoG(imageIn,sigmas,device); + end +end diff --git a/src/MATLAB/+Hydra/MaxFilter.m b/src/MATLAB/+Hydra/MaxFilter.m new file mode 100644 index 0000000..ff3cce1 --- /dev/null +++ b/src/MATLAB/+Hydra/MaxFilter.m @@ -0,0 +1,28 @@ +% MaxFilter - This will set each pixel/voxel to the max value of the neighborhood defined by the given kernel. +% [imageOut] = Hydra.MaxFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MaxFilter(imageIn,kernel,numIterations,device) + try + [imageOut] = HIP.Cuda.MaxFilter(imageIn,kernel,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.MaxFilter(imageIn,kernel,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/MeanFilter.m b/src/MATLAB/+Hydra/MeanFilter.m new file mode 100644 index 0000000..7687af0 --- /dev/null +++ b/src/MATLAB/+Hydra/MeanFilter.m @@ -0,0 +1,28 @@ +% MeanFilter - This will take the mean of the given neighborhood. +% [imageOut] = Hydra.MeanFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MeanFilter(imageIn,kernel,numIterations,device) + try + [imageOut] = HIP.Cuda.MeanFilter(imageIn,kernel,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.MeanFilter(imageIn,kernel,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/MedianFilter.m b/src/MATLAB/+Hydra/MedianFilter.m new file mode 100644 index 0000000..abb8715 --- /dev/null +++ b/src/MATLAB/+Hydra/MedianFilter.m @@ -0,0 +1,28 @@ +% MedianFilter - This will calculate the median for each neighborhood defined by the kernel. +% [imageOut] = Hydra.MedianFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MedianFilter(imageIn,kernel,numIterations,device) + try + [imageOut] = HIP.Cuda.MedianFilter(imageIn,kernel,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.MedianFilter(imageIn,kernel,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/MinFilter.m b/src/MATLAB/+Hydra/MinFilter.m new file mode 100644 index 0000000..a98005d --- /dev/null +++ b/src/MATLAB/+Hydra/MinFilter.m @@ -0,0 +1,28 @@ +% MinFilter - This will set each pixel/voxel to the max value of the neighborhood defined by the given kernel. +% [imageOut] = Hydra.MinFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MinFilter(imageIn,kernel,numIterations,device) + try + [imageOut] = HIP.Cuda.MinFilter(imageIn,kernel,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.MinFilter(imageIn,kernel,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/MultiplySum.m b/src/MATLAB/+Hydra/MultiplySum.m new file mode 100644 index 0000000..5fefbc9 --- /dev/null +++ b/src/MATLAB/+Hydra/MultiplySum.m @@ -0,0 +1,28 @@ +% MultiplySum - Multiplies the kernel with the neighboring values and sums these new values. +% [imageOut] = Hydra.MultiplySum(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = MultiplySum(imageIn,kernel,numIterations,device) + try + [imageOut] = HIP.Cuda.MultiplySum(imageIn,kernel,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.MultiplySum(imageIn,kernel,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/NLMeans.m b/src/MATLAB/+Hydra/NLMeans.m new file mode 100644 index 0000000..e319124 --- /dev/null +++ b/src/MATLAB/+Hydra/NLMeans.m @@ -0,0 +1,20 @@ +% NLMeans - Apply an approximate non-local means filter using patch mean and covariance with Fisher discrminant distance +% [imageOut] = Hydra.NLMeans(imageIn,h,[searchWindowRadius],[nhoodRadius],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% h = weighting applied to patch difference function. typically e.g. 0.05-0.1. controls the amount of smoothing. +% +% searchWindowRadius = radius of region to locate patches at. +% +% nhoodRadius = radius of patch size (comparison window). +% +function [imageOut] = NLMeans(imageIn,h,searchWindowRadius,nhoodRadius,device) + try + [imageOut] = HIP.Cuda.NLMeans(imageIn,h,searchWindowRadius,nhoodRadius,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.NLMeans(imageIn,h,searchWindowRadius,nhoodRadius,device); + end +end diff --git a/src/MATLAB/+Hydra/Opener.m b/src/MATLAB/+Hydra/Opener.m new file mode 100644 index 0000000..7b1ab7c --- /dev/null +++ b/src/MATLAB/+Hydra/Opener.m @@ -0,0 +1,28 @@ +% Opener - This kernel will erode follow by a dilation. +% [imageOut] = Hydra.Opener(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = Opener(imageIn,kernel,numIterations,device) + try + [imageOut] = HIP.Cuda.Opener(imageIn,kernel,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.Opener(imageIn,kernel,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/StdFilter.m b/src/MATLAB/+Hydra/StdFilter.m new file mode 100644 index 0000000..9fd2c5f --- /dev/null +++ b/src/MATLAB/+Hydra/StdFilter.m @@ -0,0 +1,28 @@ +% StdFilter - This will take the standard deviation of the given neighborhood. +% [imageOut] = Hydra.StdFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = StdFilter(imageIn,kernel,numIterations,device) + try + [imageOut] = HIP.Cuda.StdFilter(imageIn,kernel,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.StdFilter(imageIn,kernel,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/Sum.m b/src/MATLAB/+Hydra/Sum.m new file mode 100644 index 0000000..1218a58 --- /dev/null +++ b/src/MATLAB/+Hydra/Sum.m @@ -0,0 +1,20 @@ +% Sum - This sums up the entire array in. +% [imageOut] = Hydra.Sum(imageIn,[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% valueOut = This is the summation of the entire array. +% +function [imageOut] = Sum(imageIn,device) + try + [imageOut] = HIP.Cuda.Sum(imageIn,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.Sum(imageIn,device); + end +end diff --git a/src/MATLAB/+Hydra/VarFilter.m b/src/MATLAB/+Hydra/VarFilter.m new file mode 100644 index 0000000..620082e --- /dev/null +++ b/src/MATLAB/+Hydra/VarFilter.m @@ -0,0 +1,28 @@ +% VarFilter - This will take the variance deviation of the given neighborhood. +% [imageOut] = Hydra.VarFilter(imageIn,kernel,[numIterations],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the max neighborhood. +% +% numIterations (optional) = This is the number of iterations to run the max filter for a given position. +% This is useful for growing regions by the shape of the structuring element or for very large neighborhoods. +% Can be empty an array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = VarFilter(imageIn,kernel,numIterations,device) + try + [imageOut] = HIP.Cuda.VarFilter(imageIn,kernel,numIterations,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.VarFilter(imageIn,kernel,numIterations,device); + end +end diff --git a/src/MATLAB/+Hydra/WienerFilter.m b/src/MATLAB/+Hydra/WienerFilter.m new file mode 100644 index 0000000..55e5a64 --- /dev/null +++ b/src/MATLAB/+Hydra/WienerFilter.m @@ -0,0 +1,28 @@ +% WienerFilter - A Wiener filter aims to denoise an image in a linear fashion. +% [imageOut] = Hydra.WienerFilter(imageIn,kernel,[noiseVariance],[device]) +% imageIn = This is a one to five dimensional array. The first three dimensions are treated as spatial. +% The spatial dimensions will have the kernel applied. The last two dimensions will determine +% how to stride or jump to the next spatial block. +% +% kernel (optional) = This is a one to three dimensional array that will be used to determine neighborhood operations. +% In this case, the positions in the kernel that do not equal zeros will be evaluated. +% In other words, this can be viewed as a structuring element for the neighborhood. +% This can be an empty array [] and which will use a 3x3x3 neighborhood (or equivalent given input dimension). +% +% noiseVariance (optional) = This is the expected variance of the noise. +% This should be a scalar value or an empty array []. +% +% device (optional) = Use this if you have multiple devices and want to select one explicitly. +% Setting this to [] allows the algorithm to either pick the best device and/or will try to split +% the data across multiple devices. +% +% imageOut = This will be an array of the same type and shape as the input array. +% +function [imageOut] = WienerFilter(imageIn,kernel,noiseVariance,device) + try + [imageOut] = HIP.Cuda.WienerFilter(imageIn,kernel,noiseVariance,device); + catch errMsg + warning(errMsg.message); + [imageOut] = HIP.Local.WienerFilter(imageIn,kernel,noiseVariance,device); + end +end From c215d312361cc3ea280026627f01b4022bc0733b Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:16:51 -0600 Subject: [PATCH 17/40] build: ignore agent config and conda build artifacts --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index faa1ca8..cbb5af0 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,7 @@ out .vs CMakeSettings.json src/Python/hydra_image_processor.egg-info + +# Agent and Build artifacts +.claude/ +conda-build-dir/ From 5a241b46de07bf6b058521487473b41b6a016cc8 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Mon, 9 Feb 2026 17:20:07 -0600 Subject: [PATCH 18/40] docs: add code of conduct, security policy, and PR template --- .github/PULL_REQUEST_TEMPLATE.md | 37 +++++++++ CODE_OF_CONDUCT.md | 131 +++++++++++++++++++++++++++++++ SECURITY.md | 23 ++++++ 3 files changed, 191 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 SECURITY.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..cb28132 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,37 @@ +## Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +## How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. + +- [ ] Test A +- [ ] Test B + +**Test Configuration**: +* Firmware version: +* Hardware: +* SDK: + +## Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4bd8d6c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,131 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and unwelcome sexual attention + or advances +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@ericwait.com. All complaints will be reviewed and investigated promptly and +fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited contact with those +enforcing the Code of Conduct, for a specified period of time. This includes +avoiding interactions in community spaces as well as external channels like +social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No online or +offline interaction with the people involved, including unsolicited contact +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..393942f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Supported Versions + +The following versions of Hydra Image Processor are currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 3.x | :white_check_mark: | +| < 3.0 | :x: | + +## Reporting a Vulnerability + +We take security seriously. If you discover a security vulnerability within this project, please report it privately rather than opening a public issue. + +You can report vulnerabilities by emailing **info@ericwait.com**. + +Please include the following in your report: +* A description of the vulnerability. +* Steps to reproduce the issue. +* Potential impact. + +We will acknowledge receipt of your report within 48 hours and provide a timeline for resolution. From 313057a55e66dc18bf7d2550ddb6785f1b124e18 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Tue, 10 Feb 2026 16:55:04 -0600 Subject: [PATCH 19/40] build: add tif file support to LFS in .gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index cef6a61..038e2da 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ *.mexa64 filter=lfs diff=lfs merge=lfs -text *.pyd filter=lfs diff=lfs merge=lfs -text *.so filter=lfs diff=lfs merge=lfs -text +*.tif filter=lfs diff=lfs merge=lfs -text From b20596c1e631e67207ad5bcb5f64c0743073c8e6 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Tue, 10 Feb 2026 17:01:17 -0600 Subject: [PATCH 20/40] feat: add new brightMIP images for various filters and operations --- docs/images/Closure_c1_5-5-2_brightMIP.png | 3 +++ docs/images/Closure_c2_5-5-2_brightMIP.png | 3 +++ docs/images/ElementWiseDifference_c1_reverse_brightMIP.png | 3 +++ docs/images/ElementWiseDifference_c2_reverse_brightMIP.png | 3 +++ docs/images/EntropyFilter_c1_5-5-2_brightMIP.png | 3 +++ docs/images/EntropyFilter_c2_5-5-2_brightMIP.png | 3 +++ docs/images/Gaussian_c1_19-19-9_brightMIP.png | 3 +++ docs/images/Gaussian_c2_19-19-9_brightMIP.png | 3 +++ docs/images/HighPassFilter_c1_19-19-9_brightMIP.png | 3 +++ docs/images/HighPassFilter_c2_19-19-9_brightMIP.png | 3 +++ docs/images/LoG_c1_4-4-2_brightMIP.png | 3 +++ docs/images/LoG_c2_4-4-2_brightMIP.png | 3 +++ docs/images/MaxFilter_c1_5-5-2_brightMIP.png | 3 +++ docs/images/MaxFilter_c2_5-5-2_brightMIP.png | 3 +++ docs/images/MeanFilter_c1_5-5-2_brightMIP.png | 3 +++ docs/images/MeanFilter_c2_5-5-2_brightMIP.png | 3 +++ docs/images/MedianFilter_c1_5-5-2_brightMIP.png | 3 +++ docs/images/MedianFilter_c2_5-5-2_brightMIP.png | 3 +++ docs/images/MinFilter_c1_5-5-2_brightMIP.png | 3 +++ docs/images/MinFilter_c2_5-5-2_brightMIP.png | 3 +++ docs/images/MultiplySum_c1_15-15-6_brightMIP.png | 3 +++ docs/images/MultiplySum_c2_15-15-6_brightMIP.png | 3 +++ docs/images/Opener_c1_5-5-2_brightMIP.png | 3 +++ docs/images/Opener_c2_5-5-2_brightMIP.png | 3 +++ docs/images/StdFilter_c1_5-5-2_brightMIP.png | 3 +++ docs/images/StdFilter_c2_5-5-2_brightMIP.png | 3 +++ docs/images/VarFilter_c1_5-5-2_brightMIP.png | 3 +++ docs/images/VarFilter_c2_5-5-2_brightMIP.png | 3 +++ docs/images/test_c1__brightMIP.png | 3 +++ docs/images/test_c2__brightMIP.png | 3 +++ 30 files changed, 90 insertions(+) create mode 100644 docs/images/Closure_c1_5-5-2_brightMIP.png create mode 100644 docs/images/Closure_c2_5-5-2_brightMIP.png create mode 100644 docs/images/ElementWiseDifference_c1_reverse_brightMIP.png create mode 100644 docs/images/ElementWiseDifference_c2_reverse_brightMIP.png create mode 100644 docs/images/EntropyFilter_c1_5-5-2_brightMIP.png create mode 100644 docs/images/EntropyFilter_c2_5-5-2_brightMIP.png create mode 100644 docs/images/Gaussian_c1_19-19-9_brightMIP.png create mode 100644 docs/images/Gaussian_c2_19-19-9_brightMIP.png create mode 100644 docs/images/HighPassFilter_c1_19-19-9_brightMIP.png create mode 100644 docs/images/HighPassFilter_c2_19-19-9_brightMIP.png create mode 100644 docs/images/LoG_c1_4-4-2_brightMIP.png create mode 100644 docs/images/LoG_c2_4-4-2_brightMIP.png create mode 100644 docs/images/MaxFilter_c1_5-5-2_brightMIP.png create mode 100644 docs/images/MaxFilter_c2_5-5-2_brightMIP.png create mode 100644 docs/images/MeanFilter_c1_5-5-2_brightMIP.png create mode 100644 docs/images/MeanFilter_c2_5-5-2_brightMIP.png create mode 100644 docs/images/MedianFilter_c1_5-5-2_brightMIP.png create mode 100644 docs/images/MedianFilter_c2_5-5-2_brightMIP.png create mode 100644 docs/images/MinFilter_c1_5-5-2_brightMIP.png create mode 100644 docs/images/MinFilter_c2_5-5-2_brightMIP.png create mode 100644 docs/images/MultiplySum_c1_15-15-6_brightMIP.png create mode 100644 docs/images/MultiplySum_c2_15-15-6_brightMIP.png create mode 100644 docs/images/Opener_c1_5-5-2_brightMIP.png create mode 100644 docs/images/Opener_c2_5-5-2_brightMIP.png create mode 100644 docs/images/StdFilter_c1_5-5-2_brightMIP.png create mode 100644 docs/images/StdFilter_c2_5-5-2_brightMIP.png create mode 100644 docs/images/VarFilter_c1_5-5-2_brightMIP.png create mode 100644 docs/images/VarFilter_c2_5-5-2_brightMIP.png create mode 100644 docs/images/test_c1__brightMIP.png create mode 100644 docs/images/test_c2__brightMIP.png diff --git a/docs/images/Closure_c1_5-5-2_brightMIP.png b/docs/images/Closure_c1_5-5-2_brightMIP.png new file mode 100644 index 0000000..8f64ae9 --- /dev/null +++ b/docs/images/Closure_c1_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80bd950671e1ce86f2600b4d9b3bc0bb90051c6aaa4005efb7393b197bc683a7 +size 67224 diff --git a/docs/images/Closure_c2_5-5-2_brightMIP.png b/docs/images/Closure_c2_5-5-2_brightMIP.png new file mode 100644 index 0000000..862a4e1 --- /dev/null +++ b/docs/images/Closure_c2_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66c870718bbd3e9091d18bb1ddebc338efe62f7f7ed2ecf2552781ecc498ce9c +size 67474 diff --git a/docs/images/ElementWiseDifference_c1_reverse_brightMIP.png b/docs/images/ElementWiseDifference_c1_reverse_brightMIP.png new file mode 100644 index 0000000..0357a64 --- /dev/null +++ b/docs/images/ElementWiseDifference_c1_reverse_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c7160db32282cae2c677771013b8b35d9af8a47da9c1698379864341a31c015 +size 76797 diff --git a/docs/images/ElementWiseDifference_c2_reverse_brightMIP.png b/docs/images/ElementWiseDifference_c2_reverse_brightMIP.png new file mode 100644 index 0000000..47a8e4d --- /dev/null +++ b/docs/images/ElementWiseDifference_c2_reverse_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6556455bb25d545640a7ebfa607ea9bb9b86210acc897afa4bfd15b2ee983d2c +size 78305 diff --git a/docs/images/EntropyFilter_c1_5-5-2_brightMIP.png b/docs/images/EntropyFilter_c1_5-5-2_brightMIP.png new file mode 100644 index 0000000..9d3ef90 --- /dev/null +++ b/docs/images/EntropyFilter_c1_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbedb3a3f7b4a5561ac48b985f8b54639a8fa18ba7a79c12c44db97516ba5117 +size 39560 diff --git a/docs/images/EntropyFilter_c2_5-5-2_brightMIP.png b/docs/images/EntropyFilter_c2_5-5-2_brightMIP.png new file mode 100644 index 0000000..7a5dea5 --- /dev/null +++ b/docs/images/EntropyFilter_c2_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11a80c3bff4afb6628dbf0ac1b5bc3e6589b0dde3df36231376f8b493c3fafc2 +size 36810 diff --git a/docs/images/Gaussian_c1_19-19-9_brightMIP.png b/docs/images/Gaussian_c1_19-19-9_brightMIP.png new file mode 100644 index 0000000..0c390d9 --- /dev/null +++ b/docs/images/Gaussian_c1_19-19-9_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1d5247a75cbc031774d5bbcb243c6c2215c173ff3e7d9d029e59d1afb1f16e5 +size 16612 diff --git a/docs/images/Gaussian_c2_19-19-9_brightMIP.png b/docs/images/Gaussian_c2_19-19-9_brightMIP.png new file mode 100644 index 0000000..7863841 --- /dev/null +++ b/docs/images/Gaussian_c2_19-19-9_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7524e584145ce626a4e9878ab9b08506acdf4f352bc37b5014a29a951f60b21 +size 22950 diff --git a/docs/images/HighPassFilter_c1_19-19-9_brightMIP.png b/docs/images/HighPassFilter_c1_19-19-9_brightMIP.png new file mode 100644 index 0000000..b031a78 --- /dev/null +++ b/docs/images/HighPassFilter_c1_19-19-9_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eefdf1ef407e86cca6ab6a20d98a072ca4e12da6b3b4b7b5f78e4e3d6f068de5 +size 78341 diff --git a/docs/images/HighPassFilter_c2_19-19-9_brightMIP.png b/docs/images/HighPassFilter_c2_19-19-9_brightMIP.png new file mode 100644 index 0000000..c678947 --- /dev/null +++ b/docs/images/HighPassFilter_c2_19-19-9_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc25b5b938c11e6edd3470c9bea51decd33e289f5d67b01c9c66b6d9fdb021b1 +size 81750 diff --git a/docs/images/LoG_c1_4-4-2_brightMIP.png b/docs/images/LoG_c1_4-4-2_brightMIP.png new file mode 100644 index 0000000..72afd57 --- /dev/null +++ b/docs/images/LoG_c1_4-4-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ca9f6dec53e7e72d5d930c050194ed81079d364e0c47fc0e4d44cb483db357d +size 58041 diff --git a/docs/images/LoG_c2_4-4-2_brightMIP.png b/docs/images/LoG_c2_4-4-2_brightMIP.png new file mode 100644 index 0000000..7768c47 --- /dev/null +++ b/docs/images/LoG_c2_4-4-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20db2d9d46eb55957c147bc1c2c8dd2a5d1d5a5053f515a3fb5581139173692b +size 62694 diff --git a/docs/images/MaxFilter_c1_5-5-2_brightMIP.png b/docs/images/MaxFilter_c1_5-5-2_brightMIP.png new file mode 100644 index 0000000..c0792df --- /dev/null +++ b/docs/images/MaxFilter_c1_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3534fa1b27bca0a6daa59ea378b83ad3fd8535f1a4223a24366fe49bb8186c6e +size 40225 diff --git a/docs/images/MaxFilter_c2_5-5-2_brightMIP.png b/docs/images/MaxFilter_c2_5-5-2_brightMIP.png new file mode 100644 index 0000000..e1fae51 --- /dev/null +++ b/docs/images/MaxFilter_c2_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b855f47d26879cb4c716bf87260540a85c649d6c16fe456fef1641e3d3246527 +size 46886 diff --git a/docs/images/MeanFilter_c1_5-5-2_brightMIP.png b/docs/images/MeanFilter_c1_5-5-2_brightMIP.png new file mode 100644 index 0000000..25936a3 --- /dev/null +++ b/docs/images/MeanFilter_c1_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffece380780eca2a17b935d9474b22515925e5bb2e00b11f86d1b9a85b393fb7 +size 44362 diff --git a/docs/images/MeanFilter_c2_5-5-2_brightMIP.png b/docs/images/MeanFilter_c2_5-5-2_brightMIP.png new file mode 100644 index 0000000..c338fda --- /dev/null +++ b/docs/images/MeanFilter_c2_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6412b9c6d65155575d5482489fe74eb8c34a259bf5af5376011c410e020c3a98 +size 47819 diff --git a/docs/images/MedianFilter_c1_5-5-2_brightMIP.png b/docs/images/MedianFilter_c1_5-5-2_brightMIP.png new file mode 100644 index 0000000..617b4d3 --- /dev/null +++ b/docs/images/MedianFilter_c1_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b779c701104414dbe78dec91d64424ab3450ca23e87ea6d513cfb4eb59d90b5c +size 45737 diff --git a/docs/images/MedianFilter_c2_5-5-2_brightMIP.png b/docs/images/MedianFilter_c2_5-5-2_brightMIP.png new file mode 100644 index 0000000..775578d --- /dev/null +++ b/docs/images/MedianFilter_c2_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04bd9a0efcbcd2b05c37fd44f2c49b1bc8e7d437b9a60333308cfd55e0e75b18 +size 50901 diff --git a/docs/images/MinFilter_c1_5-5-2_brightMIP.png b/docs/images/MinFilter_c1_5-5-2_brightMIP.png new file mode 100644 index 0000000..50bc76c --- /dev/null +++ b/docs/images/MinFilter_c1_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e11bee8bfab60255f4297f28c963b9e884cac93124c2381bfffa957dc516c1d2 +size 41817 diff --git a/docs/images/MinFilter_c2_5-5-2_brightMIP.png b/docs/images/MinFilter_c2_5-5-2_brightMIP.png new file mode 100644 index 0000000..94f5341 --- /dev/null +++ b/docs/images/MinFilter_c2_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:542c77ac6bfabeaf99a72ff94230e17b0289d15ee334cf4a2fddba5409e551d3 +size 53610 diff --git a/docs/images/MultiplySum_c1_15-15-6_brightMIP.png b/docs/images/MultiplySum_c1_15-15-6_brightMIP.png new file mode 100644 index 0000000..bd429b7 --- /dev/null +++ b/docs/images/MultiplySum_c1_15-15-6_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47508375518769a72a6ec87e16bf176723ac69a88915206464277bd8e4a30981 +size 44362 diff --git a/docs/images/MultiplySum_c2_15-15-6_brightMIP.png b/docs/images/MultiplySum_c2_15-15-6_brightMIP.png new file mode 100644 index 0000000..672395e --- /dev/null +++ b/docs/images/MultiplySum_c2_15-15-6_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26636e3e41a7d0ea67310db8169a7e6e796a5cc63fb6bad45d9701cfec531c2a +size 47819 diff --git a/docs/images/Opener_c1_5-5-2_brightMIP.png b/docs/images/Opener_c1_5-5-2_brightMIP.png new file mode 100644 index 0000000..7324b58 --- /dev/null +++ b/docs/images/Opener_c1_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9717659350e818f35950320aef1a9b2ed55c0129e4dd2339126fa1f2b2393df +size 30645 diff --git a/docs/images/Opener_c2_5-5-2_brightMIP.png b/docs/images/Opener_c2_5-5-2_brightMIP.png new file mode 100644 index 0000000..8fad0fb --- /dev/null +++ b/docs/images/Opener_c2_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f2dd3ffbc85fcbed5fc4c547b972593442d3275bef61b62c0a0377525d8a8a6 +size 40711 diff --git a/docs/images/StdFilter_c1_5-5-2_brightMIP.png b/docs/images/StdFilter_c1_5-5-2_brightMIP.png new file mode 100644 index 0000000..db01c15 --- /dev/null +++ b/docs/images/StdFilter_c1_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ccd43a9d9f881988b69f80b96e226e5519b5beefc93c3bd63658829eb747ba2 +size 45457 diff --git a/docs/images/StdFilter_c2_5-5-2_brightMIP.png b/docs/images/StdFilter_c2_5-5-2_brightMIP.png new file mode 100644 index 0000000..7a19521 --- /dev/null +++ b/docs/images/StdFilter_c2_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c9f3c4d017561ee7ab1e321fd076f79a8ca2c7d1cc04f779de1941d267e4067 +size 52717 diff --git a/docs/images/VarFilter_c1_5-5-2_brightMIP.png b/docs/images/VarFilter_c1_5-5-2_brightMIP.png new file mode 100644 index 0000000..352f117 --- /dev/null +++ b/docs/images/VarFilter_c1_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f472b44ec0f8f2a125caf70466467b3906744cfeb65321ffc11866f30d3b864f +size 52263 diff --git a/docs/images/VarFilter_c2_5-5-2_brightMIP.png b/docs/images/VarFilter_c2_5-5-2_brightMIP.png new file mode 100644 index 0000000..6aaaf58 --- /dev/null +++ b/docs/images/VarFilter_c2_5-5-2_brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5224f89d000d47f3c7f418ccb3d277eeb32038c08b23c0f677836a304004584a +size 57118 diff --git a/docs/images/test_c1__brightMIP.png b/docs/images/test_c1__brightMIP.png new file mode 100644 index 0000000..44bd4f9 --- /dev/null +++ b/docs/images/test_c1__brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9a005675a0727ae19c4ce916dd7c0d296f6d85b82c7079fb71929bc3809509a +size 76788 diff --git a/docs/images/test_c2__brightMIP.png b/docs/images/test_c2__brightMIP.png new file mode 100644 index 0000000..6cdcc61 --- /dev/null +++ b/docs/images/test_c2__brightMIP.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73c1e3162f021febf630027b079fdff6cc015cb1df27d3179b007c0cb6c778bc +size 76895 From 4ea46814f5362199e2988bc2c65abd87b5f9c071 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Tue, 10 Feb 2026 17:01:37 -0600 Subject: [PATCH 21/40] feat: add new TIFF test images for various filters and operations --- test_data/Closure_c1_5-5-2.tif | 3 +++ test_data/Closure_c2_5-5-2.tif | 3 +++ test_data/ElementWiseDifference_c1_reverse.tif | 3 +++ test_data/ElementWiseDifference_c2_reverse.tif | 3 +++ test_data/EntropyFilter_c1_5-5-2.tif | 3 +++ test_data/EntropyFilter_c2_5-5-2.tif | 3 +++ test_data/Gaussian_c1_19-19-9.tif | 3 +++ test_data/Gaussian_c2_19-19-9.tif | 3 +++ test_data/HighPassFilter_c1_19-19-9.tif | 3 +++ test_data/HighPassFilter_c2_19-19-9.tif | 3 +++ test_data/LoG_c1_4-4-2.tif | 3 +++ test_data/LoG_c2_4-4-2.tif | 3 +++ test_data/MaxFilter_c1_5-5-2.tif | 3 +++ test_data/MaxFilter_c2_5-5-2.tif | 3 +++ test_data/MeanFilter_c1_5-5-2.tif | 3 +++ test_data/MeanFilter_c2_5-5-2.tif | 3 +++ test_data/MedianFilter_c1_5-5-2.tif | 3 +++ test_data/MedianFilter_c2_5-5-2.tif | 3 +++ test_data/MinFilter_c1_5-5-2.tif | 3 +++ test_data/MinFilter_c2_5-5-2.tif | 3 +++ test_data/MultiplySum_c1_15-15-6.tif | 3 +++ test_data/MultiplySum_c2_15-15-6.tif | 3 +++ test_data/Opener_c1_5-5-2.tif | 3 +++ test_data/Opener_c2_5-5-2.tif | 3 +++ test_data/StdFilter_c1_5-5-2.tif | 3 +++ test_data/StdFilter_c2_5-5-2.tif | 3 +++ test_data/VarFilter_c1_5-5-2.tif | 3 +++ test_data/VarFilter_c2_5-5-2.tif | 3 +++ test_data/test_c0.tif | 3 +++ test_data/test_c1.tif | 3 +++ test_data/test_c1_.tif | 3 +++ test_data/test_c2_.tif | 3 +++ 32 files changed, 96 insertions(+) create mode 100644 test_data/Closure_c1_5-5-2.tif create mode 100644 test_data/Closure_c2_5-5-2.tif create mode 100644 test_data/ElementWiseDifference_c1_reverse.tif create mode 100644 test_data/ElementWiseDifference_c2_reverse.tif create mode 100644 test_data/EntropyFilter_c1_5-5-2.tif create mode 100644 test_data/EntropyFilter_c2_5-5-2.tif create mode 100644 test_data/Gaussian_c1_19-19-9.tif create mode 100644 test_data/Gaussian_c2_19-19-9.tif create mode 100644 test_data/HighPassFilter_c1_19-19-9.tif create mode 100644 test_data/HighPassFilter_c2_19-19-9.tif create mode 100644 test_data/LoG_c1_4-4-2.tif create mode 100644 test_data/LoG_c2_4-4-2.tif create mode 100644 test_data/MaxFilter_c1_5-5-2.tif create mode 100644 test_data/MaxFilter_c2_5-5-2.tif create mode 100644 test_data/MeanFilter_c1_5-5-2.tif create mode 100644 test_data/MeanFilter_c2_5-5-2.tif create mode 100644 test_data/MedianFilter_c1_5-5-2.tif create mode 100644 test_data/MedianFilter_c2_5-5-2.tif create mode 100644 test_data/MinFilter_c1_5-5-2.tif create mode 100644 test_data/MinFilter_c2_5-5-2.tif create mode 100644 test_data/MultiplySum_c1_15-15-6.tif create mode 100644 test_data/MultiplySum_c2_15-15-6.tif create mode 100644 test_data/Opener_c1_5-5-2.tif create mode 100644 test_data/Opener_c2_5-5-2.tif create mode 100644 test_data/StdFilter_c1_5-5-2.tif create mode 100644 test_data/StdFilter_c2_5-5-2.tif create mode 100644 test_data/VarFilter_c1_5-5-2.tif create mode 100644 test_data/VarFilter_c2_5-5-2.tif create mode 100644 test_data/test_c0.tif create mode 100644 test_data/test_c1.tif create mode 100644 test_data/test_c1_.tif create mode 100644 test_data/test_c2_.tif diff --git a/test_data/Closure_c1_5-5-2.tif b/test_data/Closure_c1_5-5-2.tif new file mode 100644 index 0000000..ae07cf8 --- /dev/null +++ b/test_data/Closure_c1_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37167aa92da2b01426735c7da8b880047c1c6aee6bfd7270d61ed4df3f3d0514 +size 7858317 diff --git a/test_data/Closure_c2_5-5-2.tif b/test_data/Closure_c2_5-5-2.tif new file mode 100644 index 0000000..7f738a6 --- /dev/null +++ b/test_data/Closure_c2_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a072dca8558b3a4597f8d491c1a3e6d0a23315be0a1842f723352ac19137aa5d +size 10789855 diff --git a/test_data/ElementWiseDifference_c1_reverse.tif b/test_data/ElementWiseDifference_c1_reverse.tif new file mode 100644 index 0000000..cce6a55 --- /dev/null +++ b/test_data/ElementWiseDifference_c1_reverse.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c72fa075ed86c000754572e60d0a147b62c66ebf612b414aa80f6186eedc721 +size 2013137 diff --git a/test_data/ElementWiseDifference_c2_reverse.tif b/test_data/ElementWiseDifference_c2_reverse.tif new file mode 100644 index 0000000..000fcbd --- /dev/null +++ b/test_data/ElementWiseDifference_c2_reverse.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fc0ad8c836d317bc536dba72b4ee7e2e4f9e2831010a58e0b29772514ecc168 +size 15809015 diff --git a/test_data/EntropyFilter_c1_5-5-2.tif b/test_data/EntropyFilter_c1_5-5-2.tif new file mode 100644 index 0000000..0031dac --- /dev/null +++ b/test_data/EntropyFilter_c1_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1a9fe393e761a8ce8c14a7956c541f813ef05882032fb9396bb0878b0b71080 +size 32060003 diff --git a/test_data/EntropyFilter_c2_5-5-2.tif b/test_data/EntropyFilter_c2_5-5-2.tif new file mode 100644 index 0000000..a6e76f1 --- /dev/null +++ b/test_data/EntropyFilter_c2_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:644c8fc968ebf79032af7e96695a6ca7f9050581018738705e73b93a0381a1ca +size 34174993 diff --git a/test_data/Gaussian_c1_19-19-9.tif b/test_data/Gaussian_c1_19-19-9.tif new file mode 100644 index 0000000..e6336a3 --- /dev/null +++ b/test_data/Gaussian_c1_19-19-9.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:baf5d8790014655fd0fa94aa91987cb7068bb04084b0b65e6788aea5eab5474a +size 4834921 diff --git a/test_data/Gaussian_c2_19-19-9.tif b/test_data/Gaussian_c2_19-19-9.tif new file mode 100644 index 0000000..0afb5cf --- /dev/null +++ b/test_data/Gaussian_c2_19-19-9.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dde7adc7f3133c602ad971999c8632b4fa11209baa7726b9f1c03c4673054ac3 +size 8900707 diff --git a/test_data/HighPassFilter_c1_19-19-9.tif b/test_data/HighPassFilter_c1_19-19-9.tif new file mode 100644 index 0000000..741e40d --- /dev/null +++ b/test_data/HighPassFilter_c1_19-19-9.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0857dc5e091d6be05ff577533fadfe6c75f75fb587066b1dd4df4fd2608133d +size 5153179 diff --git a/test_data/HighPassFilter_c2_19-19-9.tif b/test_data/HighPassFilter_c2_19-19-9.tif new file mode 100644 index 0000000..8c63dc8 --- /dev/null +++ b/test_data/HighPassFilter_c2_19-19-9.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e13d48d81c9707a9eeff040f73af089d3bb5b6f0f6649b8de69d953010bb2496 +size 6725009 diff --git a/test_data/LoG_c1_4-4-2.tif b/test_data/LoG_c1_4-4-2.tif new file mode 100644 index 0000000..6c66a08 --- /dev/null +++ b/test_data/LoG_c1_4-4-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6a75f4f8784be3071a6e0cafef94b9a6e5b80fdf3924c4d9c97043ab8742964 +size 58689575 diff --git a/test_data/LoG_c2_4-4-2.tif b/test_data/LoG_c2_4-4-2.tif new file mode 100644 index 0000000..2b11da8 --- /dev/null +++ b/test_data/LoG_c2_4-4-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c887ce5b1b6ca634783dc69dd490c74f4c4865454b1262e137c2b4e62adb570 +size 50979213 diff --git a/test_data/MaxFilter_c1_5-5-2.tif b/test_data/MaxFilter_c1_5-5-2.tif new file mode 100644 index 0000000..aae5e22 --- /dev/null +++ b/test_data/MaxFilter_c1_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a221d0c33501bfa6ff09aca3b6e498147b72d3bf60ecccb54301e198ae7d1ba +size 9080799 diff --git a/test_data/MaxFilter_c2_5-5-2.tif b/test_data/MaxFilter_c2_5-5-2.tif new file mode 100644 index 0000000..8a9142d --- /dev/null +++ b/test_data/MaxFilter_c2_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c45cf851adc33f2d804f3d315bd4e2eead27ade6d4983fc65ecf3fa2a36d595 +size 11523169 diff --git a/test_data/MeanFilter_c1_5-5-2.tif b/test_data/MeanFilter_c1_5-5-2.tif new file mode 100644 index 0000000..4f4c316 --- /dev/null +++ b/test_data/MeanFilter_c1_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22c9e01299d1cd211455dfa762f3cd746b6ba3ae81a868b656bf42deb5b8e4e4 +size 7708939 diff --git a/test_data/MeanFilter_c2_5-5-2.tif b/test_data/MeanFilter_c2_5-5-2.tif new file mode 100644 index 0000000..72f12ba --- /dev/null +++ b/test_data/MeanFilter_c2_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1d47bb66f0fb52564b663fb8bdaa41f031e5644da41c038e3fafa29ea9dc649 +size 12730251 diff --git a/test_data/MedianFilter_c1_5-5-2.tif b/test_data/MedianFilter_c1_5-5-2.tif new file mode 100644 index 0000000..f2df95d --- /dev/null +++ b/test_data/MedianFilter_c1_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7eb7162b0204adf4d0f2d7e51bacdb44fd9112cf95911d610aa6773a03686b2c +size 7048811 diff --git a/test_data/MedianFilter_c2_5-5-2.tif b/test_data/MedianFilter_c2_5-5-2.tif new file mode 100644 index 0000000..3b7d9cd --- /dev/null +++ b/test_data/MedianFilter_c2_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df34ba251c9ac671ee4a1a5fa6200a9b73c1c0b2c8802ba3e7456d6a14c11e1a +size 12016737 diff --git a/test_data/MinFilter_c1_5-5-2.tif b/test_data/MinFilter_c1_5-5-2.tif new file mode 100644 index 0000000..78e3f31 --- /dev/null +++ b/test_data/MinFilter_c1_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bcc814638c993cbe73f6416313058d8c2460a8b34e45a33f10c4164142ab641 +size 2503287 diff --git a/test_data/MinFilter_c2_5-5-2.tif b/test_data/MinFilter_c2_5-5-2.tif new file mode 100644 index 0000000..846a6c0 --- /dev/null +++ b/test_data/MinFilter_c2_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f9515effda64a2fa07f00a8c8510b871a4a1fda7a411307716cea91f51b5eae +size 6797349 diff --git a/test_data/MultiplySum_c1_15-15-6.tif b/test_data/MultiplySum_c1_15-15-6.tif new file mode 100644 index 0000000..4f4c316 --- /dev/null +++ b/test_data/MultiplySum_c1_15-15-6.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22c9e01299d1cd211455dfa762f3cd746b6ba3ae81a868b656bf42deb5b8e4e4 +size 7708939 diff --git a/test_data/MultiplySum_c2_15-15-6.tif b/test_data/MultiplySum_c2_15-15-6.tif new file mode 100644 index 0000000..72f12ba --- /dev/null +++ b/test_data/MultiplySum_c2_15-15-6.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1d47bb66f0fb52564b663fb8bdaa41f031e5644da41c038e3fafa29ea9dc649 +size 12730251 diff --git a/test_data/Opener_c1_5-5-2.tif b/test_data/Opener_c1_5-5-2.tif new file mode 100644 index 0000000..a52577a --- /dev/null +++ b/test_data/Opener_c1_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f453bbf15a959e1deac193870bd8baaaaa84cebf3a543c7a3a9aaad079edae62 +size 3545941 diff --git a/test_data/Opener_c2_5-5-2.tif b/test_data/Opener_c2_5-5-2.tif new file mode 100644 index 0000000..fac0748 --- /dev/null +++ b/test_data/Opener_c2_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1de6c7cc04f0abcd6ad162f5877fe4243599c7d84ffe4c2eb16995ba79bd92cd +size 8004425 diff --git a/test_data/StdFilter_c1_5-5-2.tif b/test_data/StdFilter_c1_5-5-2.tif new file mode 100644 index 0000000..efb8783 --- /dev/null +++ b/test_data/StdFilter_c1_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3706725ab93f56ecb4a03683e52c8affc1a52dd21cf0d6db7150446d8b513df3 +size 6434777 diff --git a/test_data/StdFilter_c2_5-5-2.tif b/test_data/StdFilter_c2_5-5-2.tif new file mode 100644 index 0000000..196b97d --- /dev/null +++ b/test_data/StdFilter_c2_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12003e06f802f47bb201f2da1f4b386e14a4379a6552ee7eda3eb088df6fbe2e +size 9268607 diff --git a/test_data/VarFilter_c1_5-5-2.tif b/test_data/VarFilter_c1_5-5-2.tif new file mode 100644 index 0000000..be60614 --- /dev/null +++ b/test_data/VarFilter_c1_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:427636b998a9ef08ae86e2afba564117c8ff165088c6d2ac530ac1e7978fb67e +size 17593641 diff --git a/test_data/VarFilter_c2_5-5-2.tif b/test_data/VarFilter_c2_5-5-2.tif new file mode 100644 index 0000000..a2fb403 --- /dev/null +++ b/test_data/VarFilter_c2_5-5-2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f1ff110cdcfeb22d960bdb9c15b706944637bb727a194274ac82cbe33f9d6b8 +size 24346655 diff --git a/test_data/test_c0.tif b/test_data/test_c0.tif new file mode 100644 index 0000000..3a5fe62 --- /dev/null +++ b/test_data/test_c0.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0641e8049489833e01fbdfc74ba015da399042c584b520557b45ffe0983c4034 +size 12662511 diff --git a/test_data/test_c1.tif b/test_data/test_c1.tif new file mode 100644 index 0000000..2107be3 --- /dev/null +++ b/test_data/test_c1.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f246ac2c61c7f88e8d8290717745d92dfd3762e711932c9b6527e8d201aed769 +size 16729167 diff --git a/test_data/test_c1_.tif b/test_data/test_c1_.tif new file mode 100644 index 0000000..3a5fe62 --- /dev/null +++ b/test_data/test_c1_.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0641e8049489833e01fbdfc74ba015da399042c584b520557b45ffe0983c4034 +size 12662511 diff --git a/test_data/test_c2_.tif b/test_data/test_c2_.tif new file mode 100644 index 0000000..2107be3 --- /dev/null +++ b/test_data/test_c2_.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f246ac2c61c7f88e8d8290717745d92dfd3762e711932c9b6527e8d201aed769 +size 16729167 From c7f1f0714c70ce8a54e656083d3e4f73c013183c Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Sat, 28 Feb 2026 14:33:30 -0600 Subject: [PATCH 22/40] feat: implement multi-platform build workflow and add C++ accuracy tests --- .github/workflows/matlab-multibuild.yml | 155 ++++++++++++++++++++ recipe/bld.bat | 10 ++ recipe/meta.yaml | 9 ++ src/MATLAB/+Test/AccuracyTest.m | 181 ++++++++++++++++++++++++ src/c/test_back/CMakeLists.txt | 19 ++- src/c/test_back/test_accuracy.cpp | 123 ++++++++++++++++ 6 files changed, 495 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/matlab-multibuild.yml create mode 100644 src/MATLAB/+Test/AccuracyTest.m create mode 100644 src/c/test_back/test_accuracy.cpp diff --git a/.github/workflows/matlab-multibuild.yml b/.github/workflows/matlab-multibuild.yml new file mode 100644 index 0000000..c2e5929 --- /dev/null +++ b/.github/workflows/matlab-multibuild.yml @@ -0,0 +1,155 @@ +name: Multi-Platform Toolbox Build + +on: + push: + branches: [ "main", "master" ] + workflow_dispatch: + +jobs: + build-mex-windows: + name: Build MEX (Windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Install CUDA Toolkit + uses: Jimver/cuda-toolkit@v0.2.14 + with: + cuda: '12.4.1' + + - name: Setup MATLAB + uses: matlab-actions/setup-matlab@v2 + + - name: Configure CMake + run: | + cmake -S . -B build -G "Visual Studio 17 2022" -DHYDRA_MODULE_NAME=HIP + # Matlab_ROOT is set by setup-matlab and detected by FindMatlab module usually + # If not, we might need -DMatlab_ROOT_DIR=$env:MATLAB_ROOT + + - name: Build MEX + run: | + cmake --build build --config Release --target HydraMex + + - name: Verify Output + shell: powershell + run: | + # Verify the mex file exists in the source tree (copied by autoInstallMex) + if (Test-Path "src/MATLAB/+HIP/@Cuda/HIP.mexw64") { + Write-Host "MEX file found." + } else { + Write-Error "MEX file not found in src/MATLAB/+HIP/@Cuda/HIP.mexw64" + # Check build dir if autoInstall failed + Get-ChildItem -Recurse -Filter HIP.mexw64 + exit 1 + } + + - uses: actions/upload-artifact@v4 + with: + name: mex-windows + path: src/MATLAB/+HIP/@Cuda/HIP.mexw64 + + build-mex-linux: + name: Build MEX (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install CUDA Toolkit + uses: Jimver/cuda-toolkit@v0.2.14 + with: + cuda: '12.4.1' + + - name: Setup MATLAB + uses: matlab-actions/setup-matlab@v2 + + - name: Configure CMake + run: | + cmake -S . -B build -DHYDRA_MODULE_NAME=HIP + + - name: Build MEX + run: | + cmake --build build --config Release --target HydraMex + + - name: Verify Output + run: | + if [ -f "src/MATLAB/+HIP/@Cuda/HIP.mexa64" ]; then + echo "MEX file found." + else + echo "MEX file not found in src/MATLAB/+HIP/@Cuda/HIP.mexa64" + find . -name "HIP.mexa64" + exit 1 + fi + + - uses: actions/upload-artifact@v4 + with: + name: mex-linux + path: src/MATLAB/+HIP/@Cuda/HIP.mexa64 + + package-toolbox: + name: Package Toolbox + needs: [build-mex-windows, build-mex-linux] + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup MATLAB + uses: matlab-actions/setup-matlab@v2 + + - name: Download Windows MEX + uses: actions/download-artifact@v4 + with: + name: mex-windows + path: src/MATLAB/+HIP/@Cuda + + - name: Download Linux MEX + uses: actions/download-artifact@v4 + with: + name: mex-linux + path: src/MATLAB/+HIP/@Cuda + + - name: Patch PRJ File + shell: python + run: | + import os + + prj_path = r'src/MATLAB/HydraImageProcessor.prj' + with open(prj_path, 'r') as f: + content = f.read() + + # Replace absolute paths with ${PROJECT_ROOT} if they look like the user's local path + # The user's path was D:\Users\ewait\git\programming\hydra-image-processor + # We'll just genericize any absolute path pattern if possible, or just the specific one. + # Easier: Update the section to use ${PROJECT_ROOT} + + # Since we don't know the exact string that might be in the repo vs what was pasted, + # we will try to be smart. + # But actually, verify if the repo version has absolute paths. + # If it uses ${PROJECT_ROOT} mostly, we are good. + # The snippet showed absolute path in . + + import re + # Regex to find ANYTHING\Hydra Image Processor.mltbx inside + # and replace it with ${PROJECT_ROOT}\Hydra Image Processor.mltbx + + new_content = re.sub( + r'(\s*]*>).*?(\Hydra Image Processor\.mltbx)', + r'\1${PROJECT_ROOT}\2', + content, + flags=re.DOTALL + ) + + with open(prj_path, 'w') as f: + f.write(new_content) + + print("Patched .prj file.") + + - name: Package Toolbox + uses: matlab-actions/run-command@v2 + with: + command: | + matlab.addons.toolbox.packageToolbox('src/MATLAB/HydraImageProcessor.prj'); + + - uses: actions/upload-artifact@v4 + with: + name: HydraImageProcessor.mltbx + path: src/MATLAB/Hydra Image Processor.mltbx diff --git a/recipe/bld.bat b/recipe/bld.bat index 9c4fd08..f99be18 100644 --- a/recipe/bld.bat +++ b/recipe/bld.bat @@ -18,6 +18,16 @@ if errorlevel 1 exit 1 cmake --build . --config Release --target HydraPy if errorlevel 1 exit 1 +:: Build the C++ tests +cmake --build . --config Release --target test_accuracy +if errorlevel 1 exit 1 + +:: Run the C++ tests +:: Ninja places the executable in src/c/test_back/ +echo Running C++ Accuracy Tests... +src\c\test_back\test_accuracy.exe +if errorlevel 1 exit 1 + :: The build should have placed Hydra.pyd into src/Python/hydra_image_processor :: Verify it exists (optional but good for debugging) if not exist "%SRC_DIR%\src\Python\hydra_image_processor\Hydra.pyd" ( diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 9c58b41..0a397c7 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -30,12 +30,21 @@ requirements: - cuda-version >=12.4 test: + source_files: + - test_data + - src/Python/Test imports: - hydra_image_processor - hydra_image_processor.cuda - hydra_image_processor.local + requires: + - tifffile commands: - python -c "import hydra_image_processor; print(hydra_image_processor.__version__)" + # Run accuracy tests (will skip if no GPU) + # Note: source_files copies test_data to {test_work_dir}/test_data and src/Python/Test to {test_work_dir}/src/Python/Test + # We need to run the test script. + - python -m unittest src/Python/Test/test_accuracy.py about: home: https://github.com/zfphil/hydra-image-processor diff --git a/src/MATLAB/+Test/AccuracyTest.m b/src/MATLAB/+Test/AccuracyTest.m new file mode 100644 index 0000000..f0a346b --- /dev/null +++ b/src/MATLAB/+Test/AccuracyTest.m @@ -0,0 +1,181 @@ +classdef AccuracyTest < matlab.unittest.TestCase + + properties + TestDataDir + Im + ImNorm + Kernel + Sigmas + end + + methods(TestClassSetup) + function setupData(testCase) + % Check for GPU availability + try + devCount = Hydra.DeviceCount(); + if isstruct(devCount) || iscell(devCount) + % Sometimes it returns stats or tuple, handle loosely + % In Python wrapper it returns (count, stats) or count + % Let's assume if it runs, we check count + if iscell(devCount) + devCount = devCount{1}; + end + end + + % If devCount is struct/stats, maybe count is length? + % Let's rely on documentation/header: SCR_CMD_NOPROC(DeviceCount, SCR_PARAMS(SCR_OUTPUT(SCR_SCALAR(int32_t), numCudaDevices), SCR_OUTPUT(SCR_STRUCT, memStats))) + % Mex returns [num, stats]. + catch + devCount = 0; + end + + testCase.assumeTrue(isnumeric(devCount) && devCount > 0, ... + 'No CUDA devices found. Skipping accuracy tests.'); + + % Locate test data relative to this file + % File is in src/MATLAB/+Test/AccuracyTest.m + % Data is in test_data/ (root) + + % Get path of this file + currentFile = mfilename('fullpath'); + [testDir, ~, ~] = fileparts(currentFile); + % Go up from +Test -> MATLAB -> src -> root -> test_data + testCase.TestDataDir = fullfile(testDir, '..', '..', '..', 'test_data'); + + testCase.assumeTrue(isfolder(testCase.TestDataDir), ... + ['Test data directory not found at: ' testCase.TestDataDir]); + + im0Path = fullfile(testCase.TestDataDir, 'test_c0.tif'); + im1Path = fullfile(testCase.TestDataDir, 'test_c1.tif'); + + testCase.assumeTrue(isfile(im0Path) && isfile(im1Path), ... + 'Test source images missing'); + + im0 = MicroscopeData.LoadTif(im0Path); + im1 = MicroscopeData.LoadTif(im1Path); + testCase.Im = cat(4, im0, im1); + testCase.ImNorm = ImUtils.ConvertType(testCase.Im, 'single', true); + + testCase.Sigmas = [19, 19, 9]; + radius = [5, 5, 2]; + testCase.Kernel = HIP.MakeEllipsoidMask(radius); + end + end + + methods + function verifyImage(testCase, actual, expectedFile, msg) + expectedPath = fullfile(testCase.TestDataDir, expectedFile); + if ~isfile(expectedPath) + % Soft warning/skip if ground truth missing, or fail? + % Standard unit tests usually fail if resources missing unless assumed. + % We will assume it exists for "Test" to pass, but print warning if not? + % Let's use assumption so it marks as 'Incomplete' if missing, not Failed. + testCase.assumeTrue(isfile(expectedPath), ['Missing ground truth: ' expectedFile]); + return; + end + + expected = MicroscopeData.LoadTif(expectedPath); + + if contains(expectedFile, '_c1_') + actualChannel = actual(:,:,:,1); + elseif contains(expectedFile, '_c2_') + actualChannel = actual(:,:,:,2); + else + actualChannel = actual; + end + + testCase.verifyEqual(size(actualChannel), size(expected), [msg ' (Shape)']); + + if isfloat(actualChannel) + diff = abs(actualChannel - expected); + maxDiff = max(diff(:)); + testCase.verifyLessThan(maxDiff, 1e-4, [msg ' (Value mismatch)']); + else + testCase.verifyEqual(actualChannel, expected, [msg ' (Value mismatch)']); + end + end + end + + methods(Test) + function testGaussian(testCase) + imOut = Hydra.Gaussian(testCase.Im, testCase.Sigmas, 1, []); + testCase.verifyImage(imOut, 'Gaussian_c1_19-19-9.tif', 'Gaussian c1'); + testCase.verifyImage(imOut, 'Gaussian_c2_19-19-9.tif', 'Gaussian c2'); + end + + function testHighPassFilter(testCase) + imOut = Hydra.HighPassFilter(testCase.Im, testCase.Sigmas, []); + testCase.verifyImage(imOut, 'HighPassFilter_c1_19-19-9.tif', 'HighPassFilter c1'); + testCase.verifyImage(imOut, 'HighPassFilter_c2_19-19-9.tif', 'HighPassFilter c2'); + end + + function testLoG(testCase) + sigmas_log = [4, 4, 2]; + imOut = Hydra.LoG(testCase.ImNorm, sigmas_log, []); + imOut(imOut > 0.001) = 0; + imOut = abs(imOut); + testCase.verifyImage(imOut, 'LoG_c1_4-4-2.tif', 'LoG c1'); + testCase.verifyImage(imOut, 'LoG_c2_4-4-2.tif', 'LoG c2'); + end + + function testClosure(testCase) + imOut = Hydra.Closure(testCase.Im, testCase.Kernel, 1, []); + testCase.verifyImage(imOut, 'Closure_c1_5-5-2.tif', 'Closure c1'); + testCase.verifyImage(imOut, 'Closure_c2_5-5-2.tif', 'Closure c2'); + end + + function testMaxFilter(testCase) + imOut = Hydra.MaxFilter(testCase.Im, testCase.Kernel, 1, []); + testCase.verifyImage(imOut, 'MaxFilter_c1_5-5-2.tif', 'MaxFilter c1'); + testCase.verifyImage(imOut, 'MaxFilter_c2_5-5-2.tif', 'MaxFilter c2'); + end + + function testMeanFilter(testCase) + imOut = Hydra.MeanFilter(testCase.Im, testCase.Kernel, 1, []); + testCase.verifyImage(imOut, 'MeanFilter_c1_5-5-2.tif', 'MeanFilter c1'); + testCase.verifyImage(imOut, 'MeanFilter_c2_5-5-2.tif', 'MeanFilter c2'); + end + + function testMedianFilter(testCase) + imOut = Hydra.MedianFilter(testCase.Im, testCase.Kernel, 1, []); + testCase.verifyImage(imOut, 'MedianFilter_c1_5-5-2.tif', 'MedianFilter c1'); + testCase.verifyImage(imOut, 'MedianFilter_c2_5-5-2.tif', 'MedianFilter c2'); + end + + function testMinFilter(testCase) + imOut = Hydra.MinFilter(testCase.Im, testCase.Kernel, 1, []); + testCase.verifyImage(imOut, 'MinFilter_c1_5-5-2.tif', 'MinFilter c1'); + testCase.verifyImage(imOut, 'MinFilter_c2_5-5-2.tif', 'MinFilter c2'); + end + + function testOpener(testCase) + imOut = Hydra.Opener(testCase.Im, testCase.Kernel, 1, []); + testCase.verifyImage(imOut, 'Opener_c1_5-5-2.tif', 'Opener c1'); + testCase.verifyImage(imOut, 'Opener_c2_5-5-2.tif', 'Opener c2'); + end + + function testStdFilter(testCase) + imOut = Hydra.StdFilter(testCase.Im, testCase.Kernel, 1, []); + testCase.verifyImage(imOut, 'StdFilter_c1_5-5-2.tif', 'StdFilter c1'); + testCase.verifyImage(imOut, 'StdFilter_c2_5-5-2.tif', 'StdFilter c2'); + end + + function testVarFilter(testCase) + imOut = Hydra.VarFilter(testCase.Im, testCase.Kernel, 1, []); + testCase.verifyImage(imOut, 'VarFilter_c1_5-5-2.tif', 'VarFilter c1'); + testCase.verifyImage(imOut, 'VarFilter_c2_5-5-2.tif', 'VarFilter c2'); + end + + function testMultiplySum(testCase) + imOut = Hydra.MultiplySum(testCase.Im, testCase.Kernel.*3, 1, []); + testCase.verifyImage(imOut, 'MultiplySum_c1_15-15-6.tif', 'MultiplySum c1'); + testCase.verifyImage(imOut, 'MultiplySum_c2_15-15-6.tif', 'MultiplySum c2'); + end + + function testElementWiseDifference(testCase) + imOut = Hydra.ElementWiseDifference(testCase.Im, testCase.Im(:,:,:,end:-1:1), []); + testCase.verifyImage(imOut, 'ElementWiseDifference_c1_reverse.tif', 'ElementWiseDifference c1'); + testCase.verifyImage(imOut, 'ElementWiseDifference_c2_reverse.tif', 'ElementWiseDifference c2'); + end + end +end diff --git a/src/c/test_back/CMakeLists.txt b/src/c/test_back/CMakeLists.txt index 2b87f21..9828442 100644 --- a/src/c/test_back/CMakeLists.txt +++ b/src/c/test_back/CMakeLists.txt @@ -1,7 +1,22 @@ cmake_minimum_required(VERSION 3.22) add_executable(test_back test.cpp) - target_link_libraries(test_back PRIVATE HydraCudaStatic) target_include_directories(test_back PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}../Cuda) -target_compile_features(test_back PRIVATE cxx_std_17) \ No newline at end of file +target_compile_features(test_back PRIVATE cxx_std_17) + +add_executable(test_accuracy test_accuracy.cpp) + +target_link_libraries(test_accuracy PRIVATE HydraCudaStatic) + +target_include_directories(test_accuracy PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}../Cuda) + +target_compile_features(test_accuracy PRIVATE cxx_std_17) + + + +# Register tests with CTest + +enable_testing() + +add_test(NAME CppAccuracyTest COMMAND test_accuracy) diff --git a/src/c/test_back/test_accuracy.cpp b/src/c/test_back/test_accuracy.cpp new file mode 100644 index 0000000..e5f8baf --- /dev/null +++ b/src/c/test_back/test_accuracy.cpp @@ -0,0 +1,123 @@ +#include "CWrappers.h" +#include "ImageView.h" +#include "Vec.h" + +#include +#include +#include +#include +#include +#include + +// Simple minimal unit testing framework +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + printf("[FAILED] %s:%d: Assertion failed: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + } while(0) + +#define TEST_ASSERT_MSG(cond, msg) \ + do { \ + if (!(cond)) { \ + printf("[FAILED] %s:%d: %s\n", __FILE__, __LINE__, msg); \ + return false; \ + } \ + } while(0) + +#define RUN_TEST(func) \ + do { \ + printf("Running %s... ", #func); \ + if (func()) { \ + printf("[OK]\n"); \ + } else { \ + allPassed = false; \ + } \ + } while(0) + +bool test_gaussian() +{ + Vec dims = {64, 64, 10}; + ImageOwner imIn(0, dims, 1, 1); + ImageOwner imOut(0, dims, 1, 1); + + // Fill with a impulse in the middle + std::fill(imIn.getPtr(), imIn.getPtr() + imIn.getNumElements(), 0.0f); + imIn.getPtr()[imIn.getLinearAddress({32, 32, 5, 0, 0})] = 100.0f; + + float sigmas[3] = {2.0f, 2.0f, 2.0f}; + CudaCall_Gaussian::run(imIn, imOut, sigmas, 1, 0); + + // Check if it smoothed (sum should be preserved approximately) + double sumIn = 0; + double sumOut = 0; + for(size_t i=0; i dims = {32, 32, 10}; + ImageOwner imIn(0, dims, 1, 1); + + // Pattern that covers range + for(size_t i=0; i Date: Fri, 3 Jul 2026 23:31:57 -0500 Subject: [PATCH 23/40] feat: add CLAUDE.md for project guidance and build instructions --- CLAUDE.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f327ee0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,85 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Hydra Image Processor is a CUDA-accelerated image processing library for 1–5D data (x, y, z, channel, time). Its signature feature is automatic chunking of images larger than GPU memory across one or more GPUs, with halo regions to avoid edge artifacts. A single C++/CUDA backend is exposed identically to Python (`hydra_image_processor` package wrapping a compiled `Hydra` extension) and MATLAB (MEX + generated `.m` wrappers). + +An NVIDIA GPU + CUDA Toolkit (12.x in CI) are required to build; tests skip gracefully when no CUDA device is present, but the build itself needs `nvcc`. + +## Build Commands + +```bash +# Configure + build (Linux/macOS-style; CUDA toolkit must be on PATH) +cmake -S . -B build +cmake --build build --config Release --target HydraPy # Python extension +cmake --build build --config Release --target HydraMex # MATLAB MEX (needs MATLAB) +cmake --build build --config Release --target test_accuracy # C++ accuracy tests + +# Windows/MSVC preset (the only preset defined) +cmake --preset VS64 +cmake --build --preset VS64-debug +``` + +- `-DHYDRA_MODULE_NAME=HIP` renames the output module (default `Hydra`; CI uses `HIP` for MATLAB builds). +- The `HydraPy` target writes `Hydra.pyd`/`Hydra.so` directly into `src/Python/hydra_image_processor/`; then install the Python package with `pip install .` from `src/Python/`. +- Building `HydraMex` triggers a post-build MATLAB step (`src/c/Mex/autoBuildMex.cmake`) that regenerates the `.m` wrapper files — never hand-edit generated wrappers in `src/MATLAB/+Hydra/@Cuda/`. +- Conda package: `conda build recipe --python 3.12` (recipe in `recipe/`; `bld.bat` for Windows, `build.sh` for Linux/macOS). +- `-DUSE_PROCESS_MUTEX=ON` enables a cross-process GPU mutex (boost interprocess, vendored in `src/c/external/`). + +## Tests + +```bash +# C++ accuracy tests (needs a CUDA device at runtime; skips otherwise) +cmake --build build --target test_accuracy +ctest --test-dir build # runs CppAccuracyTest +./build/src/c/test_back/test_accuracy # or run the binary directly + +# Python smoke tests (plain scripts, no pytest) +cd src/Python && python test_package.py + +# MATLAB accuracy tests (matlab.unittest; compares against test_data/*.tif) +matlab -batch "results = runtests('src/MATLAB/+Test/AccuracyTest.m')" +``` + +- C++ tests use a minimal custom framework (`TEST_ASSERT`/`RUN_TEST` in `src/c/test_back/test_accuracy.cpp`) and call the generated `CudaCall_::run(...)` entry points directly. To run a single test, comment out other `RUN_TEST` lines or add a new `RUN_TEST` — there is no filter flag. +- Ground-truth images live in `test_data/` named `_c_.tif`. They are **Git LFS** files (`.gitattributes` tracks `*.tif *.png *.mat *.pyd *.so *.mex*`) — run `git lfs pull` before running accuracy tests, and any new binary test assets go through LFS. + +## Architecture + +### Macro-driven command registration (the heart of the codebase) + +Every operation is declared **once** as an `SCR_CMD(Name, SCR_PARAMS(...), cudaFunc)` line in `src/c/ScriptCmds/ScriptCommands.h`. That table is re-included repeatedly by `src/c/ScriptCmds/GenCommands.h` under different `GENERATE_*` preprocessor passes, which synthesize: + +- an argument parser and command class per operation (`GENERATE_SCRIPT_COMMANDS`), +- concrete `CudaCall_::run(...)` stubs for each of the 8 supported pixel types (`GENERATE_PROC_STUBS`, driven from `src/c/Cuda/CWrapperAutogen.cu`), +- input→output pixel-type maps (`GENERATE_DEFAULT_IO_MAPPERS`, overridable via `SCR_DEFINE_IO_TYPE_MAP`), +- help strings and the runtime command map used by both frontends (`GENERATE_CONSTEXPR_MEM`, `GENERATE_COMMAND_MAP`). + +`src/c/ScriptCmds/ScriptCommandModule.h` is the instantiation point and must be included in **exactly one** `.cpp` per module (`src/c/Python/PyCommandModule.cpp`, `src/c/Mex/MexCommandModule.cpp`). Shared per-command runtime logic (arg conversion → pixel-type dispatch → output allocation → CUDA call) lives in `src/c/ScriptCmds/ScriptCommandImpl.h`. + +The 8 supported pixel types (`bool, uint8, uint16, int16, uint32, int32, float, double`) are hard-coded in three places that must stay in sync: the runtime dispatch in `ScriptCommandImpl.h`, the stub generators in `GenCommands.h`, and the type transforms in `src/c/ScriptCmds/LinkageTraitTfms.h`. + +### Frontends + +Python and MATLAB compile the same `HydraCudaStatic` backend and command framework; they differ only by a `PY_BUILD`/`MEX_BUILD` define and a language-specific ArgConverter (`src/c/ScriptCmds/PyArgConverter.h` / `MexArgConverter.h`). + +- **Python**: `src/c/Python/HydraPyModule.cpp` builds the `PyMethodDef` table by iterating the generated command map. The pure-Python layer (`src/Python/hydra_image_processor/`) follows a GPU-first/CPU-fallback pattern: `core.py` wraps `cuda/core.py` (calls the compiled extension) with fallbacks in `local/core.py`. +- **MATLAB**: `src/c/Mex/HydraMexModule.cpp` is a single `mexFunction` dispatching on a command-name string. User-facing `.m` files under `src/MATLAB/+Hydra/` are generated at build time by `src/MATLAB/build-scripts/BuildMexClass.m` + `autoInstallMex.m` from the C++ help strings (`+HIP` is the legacy package name). + +### CUDA core and chunking + +`src/c/Cuda/ImageChunk.cpp` computes chunks that fit the smallest GPU's free memory, including kernel-halo margins, and picks split axes to minimize overlap recomputation. Host drivers (e.g. `src/c/Cuda/CudaGaussian.cuh`) follow a standard pattern: build kernels → `calculateBuffers(...)` → OpenMP parallel with one thread per GPU → per chunk `sendROI` → launch `__global__` kernel(s) via ping-pong buffers (`CudaDeviceImages.cuh`) → `retriveROI`. Device-side images (`CudaImageContainer.cuh`) are passed by value into kernels; `KernelIterator.cuh` walks structuring-element neighborhoods. Host-side images are non-owning `ImageView` (`src/c/Cuda/ImageView.h`). + +### Adding a new operation + +1. Write `src/c/Cuda/CudaFoo.cuh` with the `__global__` kernel and a host driver `cFoo(ImageView ...)`; include it in `src/c/Cuda/CWrapperAutogen.cu`. +2. Add `src/c/ScriptCmds/Commands/ScrCmdFoo.h` (`SCR_COMMAND_CLASSDEF(Foo)` + `SCR_HELP_STRING`); include it in `ScriptCommandModule.h`. +3. Add one `SCR_CMD(Foo, SCR_PARAMS(...), cFoo)` line to `src/c/ScriptCmds/ScriptCommands.h`. + +Type dispatch, Python method registration, and MATLAB wrappers are all generated from that. Only the pretty-named Python wrappers in `src/Python/hydra_image_processor/` (`cuda/core.py`, `core.py`) are added by hand. + +## CI + +`.github/workflows/conda-build.yml` builds the conda package on Windows for Python 3.10–3.12 (CUDA 12.4.1). `.github/workflows/matlab-multibuild.yml` builds `HIP` MEX binaries on Windows + Linux and packages the MATLAB toolbox from `src/MATLAB/HydraImageProcessor.prj`. From 657b54c112fd741324e4062c0e09fc460d5f5209 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 3 Jul 2026 23:32:03 -0500 Subject: [PATCH 24/40] feat: add comprehensive roadmap for project vision and development phases --- ROADMAP.md | 332 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 ROADMAP.md diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..9ea5c53 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,332 @@ +# Hydra Image Processor Roadmap + +This document assesses how far the current implementation is from the project's ideals and lays out a dependency-ordered path to get there. +It was last grounded against the codebase in July 2026 (branch `conda-forge`, commit `c7f1f07`); file paths and claims reference that state. + +## Vision + +Hydra aims to be: + +1. A library of **fast, GPU-accelerated filters** usable from multiple languages (C/C++, MATLAB, Python, C#, ...). +2. **Energy-isolated at image boundaries**: kernels are renormalized at edges instead of padding/reflecting, + so no energy is invented or lost at borders. +3. **Runtime-hardware-aware**: images larger than GPU memory are chunked automatically, + and work is distributed across multiple GPUs when available. + +Parts of this vision are already real and load-bearing: + +- The energy-insulated boundary exists: `KernelIterator` (`src/c/Cuda/KernelIterator.cu`) clips the kernel window at image borders, + and `cudaMultiplySum` (`src/c/Cuda/CudaMultiplySum.cuh`) accumulates the in-bounds kernel weight and renormalizes. +- Chunking and multi-GPU distribution exist: `src/c/Cuda/ImageChunk.cpp` sizes chunks (including kernel-halo margins) + to the smallest GPU's free memory, and every host driver stripes chunks across one OpenMP thread per GPU. +- The multi-language architecture exists: every operation is declared once in `src/c/ScriptCmds/ScriptCommands.h`, + and X-macro passes in `GenCommands.h` generate the parsers, pixel-type dispatch, help text, + and command tables consumed by both the Python and MATLAB frontends. + +The gaps are in **distribution** (nothing is installable from a public registry today), +**reach** (no CPU path, no C ABI, NVIDIA-only), +and **throughput features** (no FFT, no cross-call GPU residency, no GPU-direct I/O). + +## Gap assessment + +| Goal | Current state | Distance | +| --- | --- | --- | +| vcpkg / conan | Unmerged `feature/vcpkg` branch has near-complete CMake install/export and a port skeleton with a `SHA512 0` placeholder. Mainline has zero `install()` rules, no `project(VERSION)`. | Medium — salvage and finish | +| conda / mamba / pixi | Working-ish recipe with Windows-only CI uploading to an anaconda.org channel. Not on conda-forge. Known bugs listed in Phase 0. | Medium | +| C# + NuGet | Nothing. No `extern "C"` API (only mangled C++ templates over `ImageView`), shared-lib target commented out, empty `.def` files. | Far — needs C ABI + shared lib | +| Non-NVIDIA GPU | Raw CUDA everywhere; zero OpenCL/SYCL/HIP references. The only backend seam is the generated `CudaCall_::run` stubs calling CUDA drivers directly. | Far — deliberately deferred (Phase 2) | +| CPU fallback | C++: none — the build fails without the CUDA toolkit. Python: 100% `NotImplementedError` stubs. MATLAB: ~14/19 real toolbox fallbacks, but with divergent boundary behavior. | Medium — the dispatch seam makes it tractable | +| Additional filters | 19 compute commands today. Several new filters are nearly free as compositions of existing ones. | Near for tier 1 | +| FFT filters | Zero FFT references anywhere. All convolution is direct spatial; Gaussian is separable spatial. | Medium | +| Pipelining (vRAM residency) | Every API call is a full upload-compute-download round trip. Composites (Closure, Gaussian, LoG) already ping-pong on-device within one call, but nothing survives across calls. | Medium-far | +| GPU-direct disk I/O | The C++ core has zero file I/O by design (in-memory `ImageView` only). No GDS/nvImageCodec/nvTIFF references. | Far — and of questionable value (Phase 5) | + +Cross-cutting problems the path below fixes along the way: + +- **Version chaos**: `pyproject.toml`, `recipe/meta.yaml`, and `vcpkg.json` say 0.1.0; the MATLAB `.prj` says 3.1.2; + git tags reach v3.15; CMake declares no version. + Decision: continue the 3.x lineage — the packaging-era relaunch is **v4.0.0**. +- **CI has never executed a filter**: all runners are GPU-less GitHub-hosted machines, so accuracy tests always skip. + The CPU backend fixes this permanently. +- **Stale `main`**: all recent work lives on the `conda-forge` dev branch; `origin/main` is 22 commits behind with nothing unique. +- **Test data weight**: `test_data/` is ~435 MB of Git-LFS TIFFs — a CI bandwidth hazard as jobs multiply. + +## The path + +Effort key (solo-maintainer scale): **S** = days, **M** = 1-3 weeks, **L** = 1-2 months, **XL** = a quarter or more. + +```text +Phase 0 (foundations, ~2-3 weeks) + ├──► Track A: CPU backend ─────────────► v4.0.0 release + ├──► Track B: packaging/install ───────► │ + │ ├──► Phase 2: C ABI + shared lib ──► C# + NuGet + │ ├──► Phase 2: device-apply refactor ──► Phase 3: pipelining ──► FFT deconvolution + │ ├──► Phase 4: FFT convolution/bandpass + │ └──► Phase 5: GPU-direct I/O (assess-only, last) + Ongoing: filter catalog (tier 1 unlocks right after Track A starts landing ops) +``` + +### Phase 0 — Foundations (~2-3 weeks total) + +Everything else builds on these. All items are S unless noted. + +- **Consolidate branches**: merge the `conda-forge` dev branch into `main` (main has nothing unique) and retire the branch name — + reserve `conda-forge` for the future feedstock. +- **Salvage `feature/vcpkg` by file, not by merge** (M). + Both branches diverged from the same commit and both modified the CMakeLists files, so a merge is all conflicts. + Take verbatim: `cmake/hydra-config.cmake.in`; `src/c/Version.h.in` (adapt `@GITVERSION_*@` placeholders to `@PROJECT_VERSION*@`); + `ports/hydra/vcpkg.json`, `ports/hydra/portfile.cmake`, and `scripts/update-vcpkg-files.py` (parked until Phase 1B). + Re-implement by hand on mainline: the install/export blocks — `GNUInstallDirs`, + `configure_package_config_file` + `write_basic_package_version_file`, `install(EXPORT HydraTargets NAMESPACE Hydra::)`, + and headers installed to `include/hydra`. + Skip `.gitversion.yml` and the self-hosted `hydra-ci.yml` (documented later as an option). +- **Single-source the version**: a plain-text `VERSION` file at repo root containing `4.0.0`. + Git-derived schemes (GitVersion, setuptools_scm) fail exactly where packaging needs them: + conda-forge and vcpkg build from GitHub tarballs that contain no `.git` directory. + Consumers: CMake reads it into `project(VERSION)` and configures `Version.h.in`; + `pyproject.toml` via scikit-build-core's regex metadata provider (Phase 1B); `meta.yaml` via Jinja `load_file_regex`; + the MATLAB `.prj` is patched by the release workflow. + A CI guard asserts git tag == `VERSION` on every tag push. +- **Fix the known recipe bugs**: add the missing backslash continuations in `recipe/build.sh` + (today only the first line of the cmake invocation runs on Linux); + replace the phantom `python -m unittest src/Python/Test/test_accuracy.py` test with import checks; + fix `about.home`/`dev_url` (they point at `zfphil`, not `ericwait`). +- **License/metadata cleanup**: keep `LICENSE.md` as canonical, delete the duplicate `license.txt`, + pick one author email everywhere, and add build-from-source instructions to `readme.md` + (required for registry reviews anyway). +- **Small code prep for Track A**: fix the `sum_array`/`sum` binding bug in `src/Python/hydra_image_processor/core.py` + (the fallback path raises `AttributeError` instead of the intended `NotImplementedError`); + reserve `HYDRA_DEVICE_CPU = -2` in `src/c/Cuda/Defines.h` (`-1` already means "all GPUs"); + add a `HYDRA_HOST_DEVICE` macro (`__host__ __device__` under `__CUDACC__`, empty otherwise). +- **Make the CUDA arch list a cache variable** (`HYDRA_CUDA_ARCHITECTURES`): + mainline pins `75;86;89;120` while `feature/vcpkg` changed it to `89;90;103;121` — + keep the mainline list as default and let packagers override. + +### Phase 1, Track A — CPU backend (near-term centerpiece; XL total; ships in v4.0.0) + +**Why first**: it fixes "build requires nvcc," gives all three frontends automatic fallback in one place, +makes the library usable on non-GPU machines, lets CI actually execute filters for the first time, +makes the conda-forge feedstock testable, and is the honest non-NVIDIA story until a second GPU backend is justified. + +- **Where the code lives**: new `src/c/Cpu/` mirroring the CUDA headers one-to-one (`CpuMultiplySum.h`, `CpuMaxFilter.h`, ...). + Each defines a host driver with the **same name and signature** as its CUDA counterpart, inside `namespace CpuBackend`. + Identical signatures keep the dispatch change to a two-line macro edit. +- **Dispatch, two layers**: + 1. CMake option `HYDRA_CPU_ONLY`: builds `src/c/Cpu/` only — no `LANGUAGES CUDA`, no `find_package(CUDAToolkit)`. + This is the packaging enabler. + 2. In dual builds, the generated stub body (`_SCR_GEN_TYPED_IMPL` in `src/c/ScriptCmds/GenCommands.h`, ~lines 182-186) becomes: + if `device == HYDRA_DEVICE_CPU` or `deviceCount() == 0`, call `CpuBackend::cFoo(...)`, else call the CUDA driver. + Fallback semantics move out of the Python/MATLAB wrappers and into C++, where every frontend inherits them. + A future HIP backend is another namespace and another branch — no rewrite. +- **Boundary-condition parity is non-negotiable — share the code, don't reimplement it**: + - Move `KernelIterator` method bodies into the header and decorate with `HYDRA_HOST_DEVICE`. + The class is pure `Vec` arithmetic with no CUDA intrinsics, + so a CPU loop using the same iterator performs **bit-identical per-pixel arithmetic** for neighborhood ops. + Bitwise equality — not tolerance matching — becomes the test target. + - Factor the fractional-coordinate trilinear accessor (`CudaImageContainer.cuh`, ~lines 46-100, hit by even-sized kernels) + into a shared `HYDRA_HOST_DEVICE` free function (e.g. `src/c/Cuda/ImageSample.h`) used by both backends. + This is exactly where silent divergence would hide. +- **Parallelization**: OpenMP `parallel for` over a flattened output-pixel index + (mirroring `GetThreadBlockCoordinate` on the GPU side). + Skip the `ImageChunk` machinery on CPU — host RAM is the ceiling. + Caveat: MSVC ships OpenMP 2.0, so use signed flattened index loops (or `/openmp:llvm`). +- **Op implementation waves** (each op is S once the infrastructure exists): + 1. MultiplySum (the archetype — validates the whole parity story), Sum, GetMinMax, ElementWiseDifference, IdentityFilter. + 2. MeanFilter, Gaussian (separable, reuses MultiplySum machinery), MinFilter, MaxFilter, MedianFilter. + 3. Closure, Opener, LoG, HighPassFilter — pure compositions of waves 1-2. + 4. StdFilter, VarFilter, EntropyFilter, WienerFilter. + 5. NLMeans (largest single kernel, M). +- **Retire the language-native fallbacks** once the C++ backend covers them: + delete the Python `local/core.py` stubs and the `_gpu_with_fallback` wrapper; + deprecate MATLAB `+HIP/+Local` for one release with a warning + (the toolbox functions pad/reflect at borders and therefore diverge from Hydra's boundary at edge pixels), then remove. +- **Testing and CI payoff** (M, woven through the waves): + - New `src/c/test_back/test_cpu_parity.cpp` (pattern cloned from `test_accuracy.cpp`): + CPU vs the `test_data/` ground-truth TIFFs on GitHub-hosted runners, + plus CPU-vs-GPU bitwise comparison (via `device=-2` vs default) where a GPU exists. + - Consolidate the loose Python scripts into a pytest suite under `src/Python/tests/`, parameterized over the 8 pixel types. + - New CPU-only CI workflow (linux/macos/windows matrix, `-DHYDRA_CPU_ONLY=ON`) — + the first CI in the project's history that computes anything. +- **Risk mitigations**: land the `GenCommands.h` macro edit with the CUDA path only and diff preprocessor output + before adding the CPU branch (an X-macro mistake breaks all 24 commands at once); + add a dedicated even-kernel interpolation parity test; + while in there, consider consolidating the 8-pixel-type list — + currently duplicated across `ScriptCommandImpl.h`, `GenCommands.h`, and `LinkageTraitTfms.h` — into one X-macro header. + Document clearly that CPU mode is a correctness/accessibility path, not a performance claim. + +### Phase 1, Track B — Packaging and install (parallel with Track A) + +- **Python build integration — scikit-build-core** (L). + Move `pyproject.toml` to the repo root and let `pip install .` drive the top-level CMake. + Add `install(TARGETS HydraPy ...)` and stop writing build products into the source tree + (today `src/c/Python/CMakeLists.txt` drops `Hydra.pyd/.so` into `src/Python/hydra_image_processor/` + and `MANIFEST.in` bundles whatever is lying around — + a `pip install .` without a prior manual CMake build yields an importable-but-nonfunctional package). + Standardize on `find_package(Python COMPONENTS Interpreter Development.Module NumPy)` + (scikit-build-core hints the `Python` find module, not `Python3`). + Once `HYDRA_CPU_ONLY` exists, auto-select it when no CUDA toolkit is found so `pip install .` works everywhere. +- **Wheel strategy**: **no CUDA wheels on PyPI.** + They are GB-class, ABI-fragile, and a maintenance treadmill for a solo maintainer. + Ship: sdist always; CPU-only wheels via cibuildwheel once Track A lands + (small, testable on GPU-less CI, gives every `pip install hydra-image-processor` a working baseline); + GPU binaries via conda. Document "GPU users: conda/mamba/pixi" prominently. +- **conda-forge feedstock** (M + review latency). + Requirements vs the current recipe: GitHub release tarball URL + `sha256` instead of `source: path: ..` + (needs the first v4.0.0 release); `{{ compiler('cuda') }}` / `{{ compiler('cxx') }}` / `{{ stdlib("c") }}` conventions; + imports-only tests for the GPU variant (conda-forge CI has no GPUs — building CUDA packages there is standard). + Submit linux-64 first, win-64 as a follow-up. + When Track A lands, add the CPU variant (`cuda_compiler_version: [None, 12.x]` matrix) — + that variant runs real filter tests on conda-forge CI. + Interim: keep the anaconda.org channel healthy (fix CI trigger branches, add ubuntu to the matrix once `build.sh` is fixed). + mamba and pixi consume conda-forge automatically; + optionally add a root `pixi.toml` with dev tasks as contributor convenience (S). +- **vcpkg** (M). Finish the parked port: + replace `SHA512 0` and `REF v${GITVERSION_SEMVER}` with literals written by `scripts/update-vcpkg-files.py`, + wired into the release workflow so every tag refreshes the port. + Add a `HYDRA_BUILD_BINDINGS=OFF` guard so the port build needs no Python/MATLAB. + Ship as an **overlay port / small self-hosted registry first**; + submit to the central microsoft/vcpkg registry after 2-3 stable releases + (central CI review of CUDA ports is slow, and every release needs a version-database PR). +- **Conan**: skip for now, vcpkg-first. + The Phase 0 install/export work is package-manager-agnostic, so a conanfile is cheap later if users ask. +- **Release automation** (M): one `release.yml` on `v*` tags — + guard (tag == `VERSION`), GitHub Release with notes, + conda build + anaconda.org upload (until the feedstock's bot takes over), + vcpkg port-update PR, `.mltbx` build with `.prj` version patched from `VERSION`; + later phases append sdist/CPU wheels (PyPI trusted publishing), C-ABI shared libs, and NuGet push. +- **LFS bandwidth**: default `lfs: false` in checkouts; + one dedicated data-test job restoring LFS objects from `actions/cache`; + longer term, move test data to a GitHub Release asset or Zenodo. + +### Phase 2 — Bridge: C ABI, shared library, backend seam hardening (post-4.0) + +- **C ABI + shared library** (L; design it early — other bindings benefit from targeting it): + - New `src/c/CApi/`: `HydraC.h` (pure C public header), `HydraCTypes.h`, `HydraC.cpp`. + - Shape: **one exported C function per operation** (~19 + ~5 utilities), + with pixel type carried as an enum inside a caller-owned image descriptor struct + (`dims[3]`, `channels`, `frames`, `pixel_type`, `data`) mirroring `ImageView`/`ImageDimensions`. + The 8-way type dispatch happens inside the shim, + generated from the same `GenCommands.h` X-macros so new ops get C entry points automatically. + (Rejected: per-op-per-type exports — ~150 brittle symbols; + a single string-dispatch entry — discards compile-time signature checking and needs a third arg-converter stack.) + - Errors: every function returns an `int32_t` status; `hydra_last_error_message()` with thread-local storage; + every call wrapped in `try/catch` — nothing throws across the ABI. + - Build: `hydra_c` as a SHARED library statically linking `HydraCudaStatic`, + `CXX_VISIBILITY_PRESET hidden` so only the `extern "C"` surface exports. + This sidesteps the C++ ABI tar-pit entirely — the templated surface never crosses a DLL boundary. + `CUDA_RESOLVE_DEVICE_SYMBOLS` and PIC are already set on the static target; + verify device-symbol resolution on both platforms. Delete the empty `.def` files. + - Build it against the CPU backend too, so the C ABI is testable in GPU-less CI. + This layer also serves Rust/Julia/Java/ctypes users, not just C#. +- **C# + NuGet** (M + M, after the C ABI): + - `bindings/csharp/`: netstandard2.0 class library, + `[DllImport("hydra_c")]` declarations mirroring `HydraC.h` (~24 functions — hand-written is fine), + an ergonomic `Span`-based layer, and tests running on the CPU backend so `dotnet test` needs no GPU. + - NuGet layout: managed lib in `lib/netstandard2.0/`, + natives in `runtimes/win-x64/native/` and `runtimes/linux-x64/native/`, pulled from release artifacts. + - CUDA redistribution: `cudart_static` is currently the only CUDA dependency, so packages stay small. + **Standing cross-check**: the FFT phase adding cuFFT (a large dynamic redistributable) would break this — + if that happens, split a CPU-only base package from a GPU add-on package. +- **Device-apply refactor** (L, mechanical, fully covered by the parity tests): + split each CUDA driver into `deviceApply(residentBuffers, params, chunk)` plus the existing transfer shell. + No behavior change; it is the prerequisite for pipelining and improves code health regardless. +- **Non-NVIDIA GPU assessment** (assessment only, no dates): **hipify/ROCm over SYCL/Kokkos** when the time comes. + The kernels are idiomatic CUDA (`__constant__` kernel memory, `cudaMemcpyToSymbol`, occupancy API) + with direct HIP equivalents, so `hipify` keeps the code in-dialect, + and the Track-A namespace seam accommodates a `HipBackend::` branch without redesign. + SYCL/Kokkos would mean rewriting the entire kernel layer and adopting a heavy dependency — + unjustified for a solo-maintained library whose non-GPU users already have the CPU backend. + Trigger to act: demonstrated user demand plus access to AMD test hardware. Effort if triggered: L-XL. + +### Phase 3 — Filter pipelining (XL; depends on the device-apply refactor) + +**Recommendation: a pipeline API ("declare the chain, execute once"), not exposed device handles.** + +- Rationale: an opaque GPU-array handle crossing the Python/MATLAB boundary drags in cross-language lifetime management, + multi-GPU placement, and error-state surface — + and is fundamentally incompatible with chunking (a chunked image cannot stay resident). + The pipeline generalizes what Closure/Gaussian/LoG already do internally: + per chunk, upload once, run every op in the chain via `CudaDeviceImages` ping-pong, download once. + It works whether or not the image fits in vRAM — residency handles do not. +- Backend: a `PipelineExecutor` consuming a list of op descriptors and calling the Phase-2 `deviceApply` entry points. + The chunk halo becomes the **sum** of per-op kernel halos across the chain (extend `ImageChunk.cpp`), + so interior pixels never see window clipping mid-chain + and boundary semantics are identical to running the ops as separate calls. + The CPU backend runs the same descriptor list trivially, keeping parity testable. +- Frontend: one new `SCR_CMD_NOPROC` command (e.g. `RunPipeline`) + taking the descriptor list through the existing struct/deferred-type machinery, + plus thin fluent builders in Python and MATLAB. +- Later, if profiling shows transfer-bound single-op workloads on fits-in-vRAM images, + a residency handle can be added *on top of* the executor — do not lead with it. +- Risks: halo-sum shrinks usable chunk size for long chains of large kernels + (document; mid-chain re-tiling is research-grade — defer); + descriptors must carry intermediate pixel-type promotion (the `OutMap` machinery). + +### Phase 4 — FFT filters (L for convolution/bandpass; + L for deconvolution, which wants Phase 3) + +- Ops that earn their keep: frequency-domain convolution for large kernels (direct cost explodes above roughly 15^3), + difference-of-Gaussians/bandpass, and **Richardson-Lucy deconvolution** — + the marquee microscopy feature, and iterative (convolve, divide, convolve, multiply per iteration), + which is exactly the shape the pipeline executor accelerates. Sequence R-L after Phase 3. +- Libraries: cuFFT on GPU (ships with the toolkit — no new dependency, but see the NuGet size cross-check above). + CPU: **pocketfft** (BSD-3, header-only, powers NumPy/SciPy). + FFTW is rejected on license (GPL); kissfft is BSD but slower. +- Chunking: overlap-add/overlap-save with kernel-sized padding per chunk — fits the existing `ImageChunk` margin model. + FFT plan and scratch memory must join the chunk-size budget calculation. +- Boundary story (be explicit in docs): circular convolution has no native equivalent of clipped-window renormalization. + Default to **normalized convolution** — divide the zero-padded convolution by the convolution of a ones-mask — + which reproduces the energy-insulated boundary exactly for one extra cacheable transform. + Offer plain pad modes as documented alternatives. + Parity tests against spatial MultiplySum validate the path + (tolerance-based, not bitwise — cuFFT and pocketfft round differently). + +### Phase 5 — GPU-direct disk I/O (assess-only; recommendation: defer) + +Honest assessment of the NVIDIA decoder/storage stack against this project's users +(1-5D microscopy volumes on lab workstations): + +- nvTIFF covers baseline TIFF and a subset of compressions — + OME-TIFF/BigTIFF multi-page hyperstacks with varied bit depths routinely exceed it. +- GPUDirect Storage (cuFile) requires supported filesystems and the `nvidia-fs` driver stack — + DGX-class deployments, not typical lab machines, and Linux-only. +- nvImageCodec targets JPEG-family codecs, not microscopy formats. + +The pragmatic 80% win needs none of that: **pinned-host-memory double-buffered prefetch** (M) — +e.g. Python-side `tifffile`/`zarr` readers filling `cudaHostRegister`-ed buffers that feed the Phase-3 pipeline executor, +overlapping disk I/O with compute using only existing dependencies. +If an I/O layer is ever built, it belongs in the Python package or a `hydra-io` companion — +never in `src/c/Cuda` (the core's zero-I/O design is a feature). +Revisit only after Phase 3 ships and profiling shows I/O-bound pipelines. + +### Ongoing — filter catalog (prioritized for microscopy) + +Current inventory (19 compute commands): Closure, ElementWiseDifference, EntropyFilter, Gaussian, GetMinMax, +HighPassFilter, IdentityFilter, LoG, MaxFilter, MeanFilter, MedianFilter, MinFilter, MultiplySum, NLMeans, +Opener, StdFilter, Sum, VarFilter, WienerFilter (plus 5 meta-commands). + +- **Tier 1 — nearly free compositions, chunking-compatible (S each, any time after Track A's waves land)**: + top-hat/bottom-hat (Opener/Closure + ElementWiseDifference), morphological gradient (Max − Min), + unsharp mask (HighPassFilter exists), difference-of-Gaussians. +- **Tier 2 — new local kernels, chunking-compatible (S-M each)**: + Niblack/Sauvola adaptive thresholding (Mean and Std already exist), Sobel/gradient magnitude, bilateral filter, + anisotropic diffusion (iterative-local — a natural pipeline citizen), + Otsu global threshold (a histogram is a mergeable reduction like GetMinMax, so it is chunk-safe despite being "global-valued"). +- **Tier 3 — fundamentally global: architecture warning.** + Watershed, connected components, distance transform, and morphological reconstruction + require global propagation passes that **conflict with the streaming-chunk architecture**; + correct chunked versions are research-grade border-merging problems. + Declare them out of scope for the chunked engine (or gate behind an explicit fits-on-one-GPU mode), + and document the intended workflow: Hydra for filtering, scikit-image/ITK for global segmentation steps. + Do not let these creep in without that decision. + +## Top risks + +1. **LFS bandwidth exhaustion** — 435 MB of test data multiplied by a growing CI matrix; address in Track B before adding jobs. +2. **conda-forge review latency for CUDA recipes** (weeks) — the anaconda.org channel is the hedge; + the CPU variant strengthens the submission. +3. **X-macro edits break all 24 commands at once** — mitigate with staged landing and preprocessor-output diffing (Track A). +4. **cuFFT vs the "cudart_static-only, small redistributables" assumption** that the conda and NuGet packaging rely on — + a standing cross-track check when Phase 4 starts. +5. **Version-drift regression** — the tag == `VERSION` CI guard is the enforcement; + without it the five-version chaos returns. +6. **CPU performance expectations** — document CPU mode as a correctness and accessibility path, not a performance claim. From a4f6daf6797c647d4d2761f4a6fc89662c6b1126 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 17:50:59 -0500 Subject: [PATCH 25/40] docs: restructure roadmap into five-step plan of record and add operation backlog Reorder the roadmap around the agreed sequence: (1) conda/mamba/pixi/uv packaging, (2) vcpkg/conan, (3) CPU fallback with warning + strict flag, (4) vRAM-resident filter pipelining with host-memory taps, (5) prioritized operation backlog. Document the develop-branch PR workflow and parallel lanes. Move C ABI/C#/NuGet, non-NVIDIA GPU, and GPU-direct I/O to later phases. Seed BACKLOG.md with a re-arrangeable priority table covering compositions, local kernels, the FFT track, and global ops with their chunking caveats. Co-Authored-By: Claude Fable 5 --- BACKLOG.md | 70 ++++++++++ ROADMAP.md | 381 +++++++++++++++++++++++++++-------------------------- 2 files changed, 261 insertions(+), 190 deletions(-) create mode 100644 BACKLOG.md diff --git a/BACKLOG.md b/BACKLOG.md new file mode 100644 index 0000000..95f5fc5 --- /dev/null +++ b/BACKLOG.md @@ -0,0 +1,70 @@ +# Operation Backlog + +The re-arrangeable, priority-ordered list of filters and operations to implement (Step 5 of [ROADMAP.md](ROADMAP.md)). + +How to use this file: + +- **Reordering rows within a priority band — or moving a row between bands — is reprioritizing.** Edit freely; PR to `develop`. +- When work starts on a row, open a GitHub issue, link it in the Status column, and move status to `in progress`. +- After Step 3 (CPU backend) lands, every new operation ships **both** backends (`Cuda*.cuh` + `Cpu*.h`) + plus a parity test against ground truth in `test_data/` — that is part of the definition of done. +- New operations follow the three-file pattern in [CLAUDE.md](CLAUDE.md): CUDA/CPU driver, `ScrCmd*.h`, one `SCR_CMD` line. + +Column notes — **Effort**: S = days, M = 1-3 weeks, L = 1-2 months (per backend where two exist). +**Chunk-safe**: whether the op fits the streaming-chunk architecture (local neighborhood or mergeable reduction). + +## Priority 1 — cheap wins (compositions of existing ops) + +| Operation | Kind | Effort | Chunk-safe | Depends on | Status | Notes | +| --- | --- | --- | --- | --- | --- | --- | +| Top-hat | Composition | S | yes | Opener, ElementWiseDifference | proposed | `image - open(image)`; bright feature extraction | +| Bottom-hat | Composition | S | yes | Closure, ElementWiseDifference | proposed | `close(image) - image`; dark feature extraction | +| Morphological gradient | Composition | S | yes | MaxFilter, MinFilter | proposed | `max(image) - min(image)`; edge strength | +| Difference of Gaussians | Composition | S | yes | Gaussian | proposed | Two sigmas, subtract; blob enhancement | +| Unsharp mask | Composition | S | yes | HighPassFilter | proposed | HighPass already exists; add amount parameter | + +## Priority 2 — new local kernels (chunking-compatible) + +| Operation | Kind | Effort | Chunk-safe | Depends on | Status | Notes | +| --- | --- | --- | --- | --- | --- | --- | +| Sobel / gradient magnitude | New kernel | S | yes | — | proposed | Separable derivative kernels | +| Niblack / Sauvola threshold | New kernel | S-M | yes | MeanFilter, StdFilter | proposed | Local adaptive thresholding; Mean/Std already exist | +| Otsu threshold | Reduction | S-M | yes | histogram reduction | proposed | Histogram is a mergeable reduction (like GetMinMax), so chunk-safe despite being global-valued | +| Bilateral filter | New kernel | M | yes | — | proposed | Edge-preserving smoothing; range+spatial weights | +| Anisotropic diffusion | Iterative kernel | M-L | yes | — | proposed | Perona-Malik; halo cost per iteration; natural Step 4 pipeline citizen | + +## Priority 3 — FFT track + +Shared prerequisites for this band: cuFFT (GPU) + vendored pocketfft (CPU, BSD-3); overlap-add/save chunking; +normalized-convolution boundary mode (see ROADMAP Step 5 notes). +Standing check: cuFFT is a large dynamic redistributable — coordinate with conda/NuGet packaging before landing. + +| Operation | Kind | Effort | Chunk-safe | Depends on | Status | Notes | +| --- | --- | --- | --- | --- | --- | --- | +| FFT convolution (large kernels) | FFT | L | yes (overlap-add) | FFT infrastructure | proposed | Wins over spatial above ~15^3 kernels; parity-test vs MultiplySum | +| FFT bandpass / DoG | FFT | S after FFT conv | yes | FFT convolution | proposed | Frequency-domain band selection | +| Richardson-Lucy deconvolution | FFT, iterative | L | yes | FFT convolution; Step 4 pipeline (strongly) | proposed | Marquee microscopy feature; iterative conv/divide/multiply loops | +| Wiener deconvolution | FFT | M | yes | FFT convolution | proposed | Non-iterative alternative to R-L; distinct from existing spatial WienerFilter | + +## Priority 4 — global operations (architecture caveat) + +These require global propagation/label passes that **conflict with the streaming-chunk architecture**. +Each row needs a design decision before implementation: either a **fits-on-one-GPU gate** +(error on images that would chunk) or a research-grade **border-merge design**. +Until then, the documented workflow is Hydra for filtering, scikit-image/ITK for global segmentation. + +| Operation | Kind | Effort | Chunk-safe | Depends on | Status | Notes | +| --- | --- | --- | --- | --- | --- | --- | +| Connected components | Global label | L | no — needs gate or border-merge | design decision | proposed | Union-find on GPU is well-studied for single-buffer images | +| Distance transform | Global sweep | L | no — needs gate or border-merge | design decision | proposed | Prerequisite for watershed seeding | +| Watershed | Global propagation | L-XL | no — needs gate or border-merge | connected components, distance transform | proposed | Most-requested segmentation op; hardest to chunk correctly | +| Morphological reconstruction | Global iteration | L | no — needs gate or border-merge | design decision | proposed | Enables h-maxima/h-minima, hole filling | + +## Priority 5 — ideas / unscheduled + +| Operation | Kind | Effort | Chunk-safe | Depends on | Status | Notes | +| --- | --- | --- | --- | --- | --- | --- | +| Hole filling | Composition of reconstruction | S after reconstruction | no | morphological reconstruction | proposed | | +| H-maxima / H-minima | Composition of reconstruction | S after reconstruction | no | morphological reconstruction | proposed | Seed detection for watershed | +| Local entropy-based threshold | New kernel | M | yes | EntropyFilter | proposed | | +| Structure tensor / orientation | New kernel | M | yes | Sobel | proposed | Fiber/orientation analysis | diff --git a/ROADMAP.md b/ROADMAP.md index 9ea5c53..50c13be 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,9 @@ # Hydra Image Processor Roadmap -This document assesses how far the current implementation is from the project's ideals and lays out a dependency-ordered path to get there. -It was last grounded against the codebase in July 2026 (branch `conda-forge`, commit `c7f1f07`); file paths and claims reference that state. +This document assesses how far the current implementation is from the project's ideals and lays out the agreed plan of record: +five ordered steps, plus later phases, executed on feature branches that PR into `develop`. +It was last grounded against the codebase in July 2026 (commit `c7f1f07`); file paths and claims reference that state. +The re-arrangeable operation backlog referenced by Step 5 lives in [BACKLOG.md](BACKLOG.md). ## Vision @@ -31,15 +33,14 @@ and **throughput features** (no FFT, no cross-call GPU residency, no GPU-direct | Goal | Current state | Distance | | --- | --- | --- | +| conda / mamba / pixi / uv | Working-ish recipe with Windows-only CI uploading to an anaconda.org channel. Not on conda-forge. Nothing on PyPI, so uv has nothing to install. Known bugs listed in Step 0. | Medium | | vcpkg / conan | Unmerged `feature/vcpkg` branch has near-complete CMake install/export and a port skeleton with a `SHA512 0` placeholder. Mainline has zero `install()` rules, no `project(VERSION)`. | Medium — salvage and finish | -| conda / mamba / pixi | Working-ish recipe with Windows-only CI uploading to an anaconda.org channel. Not on conda-forge. Known bugs listed in Phase 0. | Medium | -| C# + NuGet | Nothing. No `extern "C"` API (only mangled C++ templates over `ImageView`), shared-lib target commented out, empty `.def` files. | Far — needs C ABI + shared lib | -| Non-NVIDIA GPU | Raw CUDA everywhere; zero OpenCL/SYCL/HIP references. The only backend seam is the generated `CudaCall_::run` stubs calling CUDA drivers directly. | Far — deliberately deferred (Phase 2) | | CPU fallback | C++: none — the build fails without the CUDA toolkit. Python: 100% `NotImplementedError` stubs. MATLAB: ~14/19 real toolbox fallbacks, but with divergent boundary behavior. | Medium — the dispatch seam makes it tractable | -| Additional filters | 19 compute commands today. Several new filters are nearly free as compositions of existing ones. | Near for tier 1 | -| FFT filters | Zero FFT references anywhere. All convolution is direct spatial; Gaussian is separable spatial. | Medium | | Pipelining (vRAM residency) | Every API call is a full upload-compute-download round trip. Composites (Closure, Gaussian, LoG) already ping-pong on-device within one call, but nothing survives across calls. | Medium-far | -| GPU-direct disk I/O | The C++ core has zero file I/O by design (in-memory `ImageView` only). No GDS/nvImageCodec/nvTIFF references. | Far — and of questionable value (Phase 5) | +| Additional filters / FFT | 19 compute commands today; zero FFT references; global ops (watershed, connected components) absent. Full candidate list now lives in [BACKLOG.md](BACKLOG.md). | Near for compositions, far for global ops | +| C# + NuGet | Nothing. No `extern "C"` API (only mangled C++ templates over `ImageView`), shared-lib target commented out, empty `.def` files. | Far — needs C ABI + shared lib (later phase) | +| Non-NVIDIA GPU | Raw CUDA everywhere; zero OpenCL/SYCL/HIP references. The only backend seam is the generated `CudaCall_::run` stubs calling CUDA drivers directly. | Far — deliberately deferred (later phase) | +| GPU-direct disk I/O | The C++ core has zero file I/O by design (in-memory `ImageView` only). No GDS/nvImageCodec/nvTIFF references. | Far — and of questionable value (later phase) | Cross-cutting problems the path below fixes along the way: @@ -47,36 +48,50 @@ Cross-cutting problems the path below fixes along the way: git tags reach v3.15; CMake declares no version. Decision: continue the 3.x lineage — the packaging-era relaunch is **v4.0.0**. - **CI has never executed a filter**: all runners are GPU-less GitHub-hosted machines, so accuracy tests always skip. - The CPU backend fixes this permanently. -- **Stale `main`**: all recent work lives on the `conda-forge` dev branch; `origin/main` is 22 commits behind with nothing unique. + The CPU backend (Step 3) fixes this permanently. - **Test data weight**: `test_data/` is ~435 MB of Git-LFS TIFFs — a CI bandwidth hazard as jobs multiply. -## The path +## Development workflow -Effort key (solo-maintainer scale): **S** = days, **M** = 1-3 weeks, **L** = 1-2 months, **XL** = a quarter or more. +- **`develop` is the integration branch.** It starts by fast-forwarding to the current working HEAD plus this roadmap + (`origin/develop` is a clean ancestor — 0 unique commits, 31 behind — so this is conflict-free). + The old `conda-forge` dev branch is retired after the fast-forward; that name is reserved for the future feedstock. +- **All work happens on feature branches PR'd into `develop`** (e.g. `feature/version-single-source`, + `feature/scikit-build-core`, `feature/cpu-backend-infra`). Useful material is pulled or cherry-picked from + existing branches — notably `feature/vcpkg` — rather than merged wholesale (see Step 0). +- **`main` is the release branch**: untouched until the first `v4.0.0` release PR from `develop`; tags live there. +- **Parallel lanes.** Steps 1-2 (packaging: CMake install/export, recipes, CI) and Step 3 (CPU backend: `src/c/Cpu/`, + dispatch macro, tests) touch mostly disjoint files and can proceed concurrently on separate feature branches + once Step 0 lands. Step 4 should follow Step 3, because the CPU parity tests are the safety net for its refactoring. + Backlog items (Step 5) unlock as Step 3's op waves land. ```text -Phase 0 (foundations, ~2-3 weeks) - ├──► Track A: CPU backend ─────────────► v4.0.0 release - ├──► Track B: packaging/install ───────► │ - │ ├──► Phase 2: C ABI + shared lib ──► C# + NuGet - │ ├──► Phase 2: device-apply refactor ──► Phase 3: pipelining ──► FFT deconvolution - │ ├──► Phase 4: FFT convolution/bandpass - │ └──► Phase 5: GPU-direct I/O (assess-only, last) - Ongoing: filter catalog (tier 1 unlocks right after Track A starts landing ops) +Step 0: foundations (branch reset, version single-source, recipe fixes, install/export) + │ + ├──► Step 1: conda / mamba / pixi / uv(sdist) ──► Step 2: vcpkg / conan + │ (CPU wheels + conda CPU variant are Step 3 deliverables that close Step 1 follow-ups) + │ + └──► Step 3: CPU fallback (parallel lane with Steps 1-2) + │ + └──► Step 4: pipelining (design doc ──► device-apply refactor ──► executor) + +Step 5: operation backlog (BACKLOG.md — seeded now, groomed continuously) +Later: C ABI ──► C# + NuGet; non-NVIDIA GPU assessment; GPU-direct disk I/O ``` -### Phase 0 — Foundations (~2-3 weeks total) +Effort key (solo-maintainer scale): **S** = days, **M** = 1-3 weeks, **L** = 1-2 months, **XL** = a quarter or more. + +## Step 0 — Foundations (~2-3 weeks; prerequisite for everything) -Everything else builds on these. All items are S unless noted. +All items are S unless noted. -- **Consolidate branches**: merge the `conda-forge` dev branch into `main` (main has nothing unique) and retire the branch name — - reserve `conda-forge` for the future feedstock. +- **Branch reset**: fast-forward `develop` to the current working HEAD, commit this roadmap and `BACKLOG.md` there, + retire the `conda-forge` branch name. `main` waits for the v4.0.0 release. - **Salvage `feature/vcpkg` by file, not by merge** (M). - Both branches diverged from the same commit and both modified the CMakeLists files, so a merge is all conflicts. + It diverged from the same commit as the current work and both sides modified the CMakeLists files, so a merge is all conflicts. Take verbatim: `cmake/hydra-config.cmake.in`; `src/c/Version.h.in` (adapt `@GITVERSION_*@` placeholders to `@PROJECT_VERSION*@`); - `ports/hydra/vcpkg.json`, `ports/hydra/portfile.cmake`, and `scripts/update-vcpkg-files.py` (parked until Phase 1B). - Re-implement by hand on mainline: the install/export blocks — `GNUInstallDirs`, + `ports/hydra/vcpkg.json`, `ports/hydra/portfile.cmake`, and `scripts/update-vcpkg-files.py` (parked until Step 2). + Re-implement by hand on a feature branch: the install/export blocks — `GNUInstallDirs`, `configure_package_config_file` + `write_basic_package_version_file`, `install(EXPORT HydraTargets NAMESPACE Hydra::)`, and headers installed to `include/hydra`. Skip `.gitversion.yml` and the self-hosted `hydra-ci.yml` (documented later as an option). @@ -84,7 +99,7 @@ Everything else builds on these. All items are S unless noted. Git-derived schemes (GitVersion, setuptools_scm) fail exactly where packaging needs them: conda-forge and vcpkg build from GitHub tarballs that contain no `.git` directory. Consumers: CMake reads it into `project(VERSION)` and configures `Version.h.in`; - `pyproject.toml` via scikit-build-core's regex metadata provider (Phase 1B); `meta.yaml` via Jinja `load_file_regex`; + `pyproject.toml` via scikit-build-core's regex metadata provider (Step 1); `meta.yaml` via Jinja `load_file_regex`; the MATLAB `.prj` is patched by the release workflow. A CI guard asserts git tag == `VERSION` on every tag push. - **Fix the known recipe bugs**: add the missing backslash continuations in `recipe/build.sh` @@ -94,7 +109,7 @@ Everything else builds on these. All items are S unless noted. - **License/metadata cleanup**: keep `LICENSE.md` as canonical, delete the duplicate `license.txt`, pick one author email everywhere, and add build-from-source instructions to `readme.md` (required for registry reviews anyway). -- **Small code prep for Track A**: fix the `sum_array`/`sum` binding bug in `src/Python/hydra_image_processor/core.py` +- **Small code prep for Step 3**: fix the `sum_array`/`sum` binding bug in `src/Python/hydra_image_processor/core.py` (the fallback path raises `AttributeError` instead of the intended `NotImplementedError`); reserve `HYDRA_DEVICE_CPU = -2` in `src/c/Cuda/Defines.h` (`-1` already means "all GPUs"); add a `HYDRA_HOST_DEVICE` macro (`__host__ __device__` under `__CUDACC__`, empty otherwise). @@ -102,22 +117,83 @@ Everything else builds on these. All items are S unless noted. mainline pins `75;86;89;120` while `feature/vcpkg` changed it to `89;90;103;121` — keep the mainline list as default and let packagers override. -### Phase 1, Track A — CPU backend (near-term centerpiece; XL total; ships in v4.0.0) +## Step 1 — conda / mamba / pixi / uv, with a documented update process + +**Goal**: a user can `conda install`/`mamba install`/`pixi add` a GPU build, `uv pip install` an sdist, +and the maintainer can publish an update with one tag push. + +- **Python build integration — scikit-build-core** (L). The prerequisite for everything Python-facing. + Move `pyproject.toml` to the repo root and let `pip install .` / `uv pip install .` drive the top-level CMake. + Add `install(TARGETS HydraPy ...)` and stop writing build products into the source tree + (today `src/c/Python/CMakeLists.txt` drops `Hydra.pyd/.so` into `src/Python/hydra_image_processor/` + and `MANIFEST.in` bundles whatever is lying around — + a `pip install .` without a prior manual CMake build yields an importable-but-nonfunctional package). + Standardize on `find_package(Python COMPONENTS Interpreter Development.Module NumPy)` + (scikit-build-core hints the `Python` find module, not `Python3`). +- **PyPI / uv story, sequenced honestly**: publish the **sdist** now — `uv pip install hydra-image-processor` works + but compiles from source and requires the CUDA toolkit locally; document this loudly. + **No CUDA wheels on PyPI** (GB-class, ABI-fragile, a maintenance treadmill). + CPU-only wheels via cibuildwheel are an explicit **Step 3 deliverable** that upgrades uv users to binary installs. + Until then, the documented binary path is conda/mamba/pixi. +- **conda-forge feedstock** (M + review latency). + Changes vs the current recipe: GitHub release tarball URL + `sha256` instead of `source: path: ..` + (needs the first v4.0.0 release); `{{ compiler('cuda') }}` / `{{ compiler('cxx') }}` / `{{ stdlib("c") }}` conventions; + imports-only tests for the GPU variant (conda-forge CI has no GPUs — building CUDA packages there is standard). + Submit linux-64 first, win-64 as a follow-up. + The CPU variant (`cuda_compiler_version: [None, 12.x]` matrix) is a **Step 3 deliverable**. + Interim: keep the anaconda.org channel healthy (fix CI trigger branches, add ubuntu to the matrix once `build.sh` is fixed). + mamba and pixi consume conda-forge automatically; add a root `pixi.toml` with dev/test tasks as contributor convenience (S). +- **Release automation + documentation** (M): one `release.yml` on `v*` tags — + guard (tag == `VERSION`), GitHub Release with notes, conda build + anaconda.org upload + (until the feedstock's bot takes over), sdist to PyPI via trusted publishing, + `.mltbx` build with `.prj` version patched from `VERSION`. + Write **`RELEASING.md`**: the exact update process (bump `VERSION`, update changelog, tag, what automation does, + what to verify afterward, how conda-forge/vcpkg pick up the release). "Easy to update" is a deliverable, not a hope. +- **LFS bandwidth**: default `lfs: false` in checkouts; one dedicated data-test job restoring LFS objects + from `actions/cache`; longer term, move test data to a GitHub Release asset or Zenodo. + +## Step 2 — vcpkg / conan + +**Goal**: a C++ consumer gets `find_package(Hydra)` from a registry, and each release updates the port automatically. + +- **vcpkg** (M). Finish the port parked in Step 0: + replace `SHA512 0` and `REF v${GITVERSION_SEMVER}` with literals written by `scripts/update-vcpkg-files.py`, + wired into `release.yml` so every tag opens a port-update PR (extend `RELEASING.md` accordingly). + Add a `HYDRA_BUILD_BINDINGS=OFF` guard so the port build needs no Python/MATLAB. + Ship as an **overlay port / small self-hosted registry first**; + submit to the central microsoft/vcpkg registry after 2-3 stable releases + (central CI review of CUDA ports is slow, and every release needs a version-database PR). +- **conan** (S-M). Assess after vcpkg works: the Step 0 install/export is package-manager-agnostic, + so a `conanfile.py` is mostly metadata. Publish to ConanCenter only if users ask; + otherwise document consuming via `conan` from a git URL. +- **C ABI design note** (S, design only — implementation is a later phase): + while the export surface is being shaped here, write down the flat C API design (see "Later phases") + so Step 2's installed headers and targets don't need rework when it lands. + +## Step 3 — CPU fallback (parallel lane with Steps 1-2; XL total) -**Why first**: it fixes "build requires nvcc," gives all three frontends automatic fallback in one place, -makes the library usable on non-GPU machines, lets CI actually execute filters for the first time, -makes the conda-forge feedstock testable, and is the honest non-NVIDIA story until a second GPU backend is justified. +**Goal**: every filter runs on machines without an NVIDIA GPU; implicit fallback **warns**; +a **strict flag** turns fallback into an error; CI finally executes filters. - **Where the code lives**: new `src/c/Cpu/` mirroring the CUDA headers one-to-one (`CpuMultiplySum.h`, `CpuMaxFilter.h`, ...). Each defines a host driver with the **same name and signature** as its CUDA counterpart, inside `namespace CpuBackend`. Identical signatures keep the dispatch change to a two-line macro edit. - **Dispatch, two layers**: 1. CMake option `HYDRA_CPU_ONLY`: builds `src/c/Cpu/` only — no `LANGUAGES CUDA`, no `find_package(CUDAToolkit)`. - This is the packaging enabler. - 2. In dual builds, the generated stub body (`_SCR_GEN_TYPED_IMPL` in `src/c/ScriptCmds/GenCommands.h`, ~lines 182-186) becomes: - if `device == HYDRA_DEVICE_CPU` or `deviceCount() == 0`, call `CpuBackend::cFoo(...)`, else call the CUDA driver. + This unlocks the Step 1 follow-ups (CPU wheels, conda CPU variant) and GPU-less CI. + 2. In dual builds, the generated stub body (`_SCR_GEN_TYPED_IMPL` in `src/c/ScriptCmds/GenCommands.h`, ~lines 182-186) + becomes: if `device == HYDRA_DEVICE_CPU` or `deviceCount() == 0`, call `CpuBackend::cFoo(...)`, + else call the CUDA driver. Fallback semantics move out of the Python/MATLAB wrappers and into C++, where every frontend inherits them. A future HIP backend is another namespace and another branch — no rewrite. +- **Fallback UX (explicit requirements)**: + - **Warning on implicit fallback**: when `device == -1` (default) and no GPU is found, the dispatch emits a warning + through each frontend's native channel — Python `RuntimeWarning`, MATLAB `warning('Hydra:cpuFallback', ...)`, + C/C++ a stderr message (later a registerable callback). Explicitly requesting `device = HYDRA_DEVICE_CPU` is silent — + intentional CPU use is not a fallback. + - **Strict mode**: `HYDRA_REQUIRE_GPU=1` environment variable (following the existing `HYDRA_ENABLE_MUTEX` pattern + in `src/c/ScriptCmds/HydraConfig.h`, surfaced via the `CheckConfig` command) makes implicit fallback an error + instead of a warning. Useful for benchmarking and for pipelines where silent CPU execution would be misleading. - **Boundary-condition parity is non-negotiable — share the code, don't reimplement it**: - Move `KernelIterator` method bodies into the header and decorate with `HYDRA_HOST_DEVICE`. The class is pure `Vec` arithmetic with no CUDA intrinsics, @@ -147,6 +223,8 @@ makes the conda-forge feedstock testable, and is the honest non-NVIDIA story unt - Consolidate the loose Python scripts into a pytest suite under `src/Python/tests/`, parameterized over the 8 pixel types. - New CPU-only CI workflow (linux/macos/windows matrix, `-DHYDRA_CPU_ONLY=ON`) — the first CI in the project's history that computes anything. + - Close the Step 1 follow-ups: CPU wheels via cibuildwheel to PyPI (uv users get binaries), + CPU variant added to the conda-forge feedstock. - **Risk mitigations**: land the `GenCommands.h` macro edit with the CUDA path only and diff preprocessor output before adding the CPU branch (an X-macro mistake breaks all 24 commands at once); add a dedicated even-kernel interpolation parity test; @@ -154,179 +232,102 @@ makes the conda-forge feedstock testable, and is the honest non-NVIDIA story unt currently duplicated across `ScriptCommandImpl.h`, `GenCommands.h`, and `LinkageTraitTfms.h` — into one X-macro header. Document clearly that CPU mode is a correctness/accessibility path, not a performance claim. -### Phase 1, Track B — Packaging and install (parallel with Track A) +## Step 4 — Filter pipelining (XL; after Step 3; starts with a design doc) -- **Python build integration — scikit-build-core** (L). - Move `pyproject.toml` to the repo root and let `pip install .` drive the top-level CMake. - Add `install(TARGETS HydraPy ...)` and stop writing build products into the source tree - (today `src/c/Python/CMakeLists.txt` drops `Hydra.pyd/.so` into `src/Python/hydra_image_processor/` - and `MANIFEST.in` bundles whatever is lying around — - a `pip install .` without a prior manual CMake build yields an importable-but-nonfunctional package). - Standardize on `find_package(Python COMPONENTS Interpreter Development.Module NumPy)` - (scikit-build-core hints the `Python` find module, not `Python3`). - Once `HYDRA_CPU_ONLY` exists, auto-select it when no CUDA toolkit is found so `pip install .` works everywhere. -- **Wheel strategy**: **no CUDA wheels on PyPI.** - They are GB-class, ABI-fragile, and a maintenance treadmill for a solo maintainer. - Ship: sdist always; CPU-only wheels via cibuildwheel once Track A lands - (small, testable on GPU-less CI, gives every `pip install hydra-image-processor` a working baseline); - GPU binaries via conda. Document "GPU users: conda/mamba/pixi" prominently. -- **conda-forge feedstock** (M + review latency). - Requirements vs the current recipe: GitHub release tarball URL + `sha256` instead of `source: path: ..` - (needs the first v4.0.0 release); `{{ compiler('cuda') }}` / `{{ compiler('cxx') }}` / `{{ stdlib("c") }}` conventions; - imports-only tests for the GPU variant (conda-forge CI has no GPUs — building CUDA packages there is standard). - Submit linux-64 first, win-64 as a follow-up. - When Track A lands, add the CPU variant (`cuda_compiler_version: [None, 12.x]` matrix) — - that variant runs real filter tests on conda-forge CI. - Interim: keep the anaconda.org channel healthy (fix CI trigger branches, add ubuntu to the matrix once `build.sh` is fixed). - mamba and pixi consume conda-forge automatically; - optionally add a root `pixi.toml` with dev tasks as contributor convenience (S). -- **vcpkg** (M). Finish the parked port: - replace `SHA512 0` and `REF v${GITVERSION_SEMVER}` with literals written by `scripts/update-vcpkg-files.py`, - wired into the release workflow so every tag refreshes the port. - Add a `HYDRA_BUILD_BINDINGS=OFF` guard so the port build needs no Python/MATLAB. - Ship as an **overlay port / small self-hosted registry first**; - submit to the central microsoft/vcpkg registry after 2-3 stable releases - (central CI review of CUDA ports is slow, and every release needs a version-database PR). -- **Conan**: skip for now, vcpkg-first. - The Phase 0 install/export work is package-manager-agnostic, so a conanfile is cheap later if users ask. -- **Release automation** (M): one `release.yml` on `v*` tags — - guard (tag == `VERSION`), GitHub Release with notes, - conda build + anaconda.org upload (until the feedstock's bot takes over), - vcpkg port-update PR, `.mltbx` build with `.prj` version patched from `VERSION`; - later phases append sdist/CPU wheels (PyPI trusted publishing), C-ABI shared libs, and NuGet push. -- **LFS bandwidth**: default `lfs: false` in checkouts; - one dedicated data-test job restoring LFS objects from `actions/cache`; - longer term, move test data to a GitHub Release asset or Zenodo. - -### Phase 2 — Bridge: C ABI, shared library, backend seam hardening (post-4.0) - -- **C ABI + shared library** (L; design it early — other bindings benefit from targeting it): - - New `src/c/CApi/`: `HydraC.h` (pure C public header), `HydraCTypes.h`, `HydraC.cpp`. - - Shape: **one exported C function per operation** (~19 + ~5 utilities), - with pixel type carried as an enum inside a caller-owned image descriptor struct - (`dims[3]`, `channels`, `frames`, `pixel_type`, `data`) mirroring `ImageView`/`ImageDimensions`. - The 8-way type dispatch happens inside the shim, - generated from the same `GenCommands.h` X-macros so new ops get C entry points automatically. - (Rejected: per-op-per-type exports — ~150 brittle symbols; - a single string-dispatch entry — discards compile-time signature checking and needs a third arg-converter stack.) - - Errors: every function returns an `int32_t` status; `hydra_last_error_message()` with thread-local storage; - every call wrapped in `try/catch` — nothing throws across the ABI. - - Build: `hydra_c` as a SHARED library statically linking `HydraCudaStatic`, - `CXX_VISIBILITY_PRESET hidden` so only the `extern "C"` surface exports. - This sidesteps the C++ ABI tar-pit entirely — the templated surface never crosses a DLL boundary. - `CUDA_RESOLVE_DEVICE_SYMBOLS` and PIC are already set on the static target; - verify device-symbol resolution on both platforms. Delete the empty `.def` files. - - Build it against the CPU backend too, so the C ABI is testable in GPU-less CI. - This layer also serves Rust/Julia/Java/ctypes users, not just C#. -- **C# + NuGet** (M + M, after the C ABI): - - `bindings/csharp/`: netstandard2.0 class library, - `[DllImport("hydra_c")]` declarations mirroring `HydraC.h` (~24 functions — hand-written is fine), - an ergonomic `Span`-based layer, and tests running on the CPU backend so `dotnet test` needs no GPU. - - NuGet layout: managed lib in `lib/netstandard2.0/`, - natives in `runtimes/win-x64/native/` and `runtimes/linux-x64/native/`, pulled from release artifacts. - - CUDA redistribution: `cudart_static` is currently the only CUDA dependency, so packages stay small. - **Standing cross-check**: the FFT phase adding cuFFT (a large dynamic redistributable) would break this — - if that happens, split a CPU-only base package from a GPU add-on package. -- **Device-apply refactor** (L, mechanical, fully covered by the parity tests): - split each CUDA driver into `deviceApply(residentBuffers, params, chunk)` plus the existing transfer shell. - No behavior change; it is the prerequisite for pipelining and improves code health regardless. -- **Non-NVIDIA GPU assessment** (assessment only, no dates): **hipify/ROCm over SYCL/Kokkos** when the time comes. - The kernels are idiomatic CUDA (`__constant__` kernel memory, `cudaMemcpyToSymbol`, occupancy API) - with direct HIP equivalents, so `hipify` keeps the code in-dialect, - and the Track-A namespace seam accommodates a `HipBackend::` branch without redesign. - SYCL/Kokkos would mean rewriting the entire kernel layer and adopting a heavy dependency — - unjustified for a solo-maintained library whose non-GPU users already have the CPU backend. - Trigger to act: demonstrated user demand plus access to AMD test hardware. Effort if triggered: L-XL. - -### Phase 3 — Filter pipelining (XL; depends on the device-apply refactor) +**Goal**: run a chain of filters keeping data in vRAM as long as possible, +with the ability to emit intermediate results to host memory at chosen points. -**Recommendation: a pipeline API ("declare the chain, execute once"), not exposed device handles.** +This step explicitly begins with a **design document** (`docs/design/pipeline.md`, reviewed as its own PR) +before implementation — the descriptor format, memory model, and frontend API deserve deliberate design. +Direction of record, to be refined by that document: -- Rationale: an opaque GPU-array handle crossing the Python/MATLAB boundary drags in cross-language lifetime management, +- **A pipeline API ("declare the chain, execute once"), not exposed device handles.** + An opaque GPU-array handle crossing the Python/MATLAB boundary drags in cross-language lifetime management, multi-GPU placement, and error-state surface — and is fundamentally incompatible with chunking (a chunked image cannot stay resident). The pipeline generalizes what Closure/Gaussian/LoG already do internally: per chunk, upload once, run every op in the chain via `CudaDeviceImages` ping-pong, download once. It works whether or not the image fits in vRAM — residency handles do not. -- Backend: a `PipelineExecutor` consuming a list of op descriptors and calling the Phase-2 `deviceApply` entry points. +- **Intermediate outputs (host taps)**: each stage descriptor carries an `emit` flag; + the executor retrieves that stage's ROI to a caller-provided host buffer in addition to feeding the next stage. + Tapped stages cost one extra device-to-host copy — data still never makes a round trip back up. + This satisfies "send intermediate results out to host memory" without breaking residency for the rest of the chain. +- **Prerequisite refactor — device-apply split** (L, mechanical, protected by Step 3's parity tests): + split each CUDA driver into `deviceApply(residentBuffers, params, chunk)` plus the existing transfer shell. + No behavior change; it is the enabling refactor for the executor and improves code health regardless. +- **Backend**: a `PipelineExecutor` consuming a list of op descriptors and calling the `deviceApply` entry points. The chunk halo becomes the **sum** of per-op kernel halos across the chain (extend `ImageChunk.cpp`), so interior pixels never see window clipping mid-chain and boundary semantics are identical to running the ops as separate calls. - The CPU backend runs the same descriptor list trivially, keeping parity testable. -- Frontend: one new `SCR_CMD_NOPROC` command (e.g. `RunPipeline`) + The CPU backend runs the same descriptor list trivially (taps are just pointer copies), keeping parity testable. +- **Frontend**: one new `SCR_CMD_NOPROC` command (e.g. `RunPipeline`) taking the descriptor list through the existing struct/deferred-type machinery, plus thin fluent builders in Python and MATLAB. - Later, if profiling shows transfer-bound single-op workloads on fits-in-vRAM images, a residency handle can be added *on top of* the executor — do not lead with it. -- Risks: halo-sum shrinks usable chunk size for long chains of large kernels - (document; mid-chain re-tiling is research-grade — defer); - descriptors must carry intermediate pixel-type promotion (the `OutMap` machinery). - -### Phase 4 — FFT filters (L for convolution/bandpass; + L for deconvolution, which wants Phase 3) - -- Ops that earn their keep: frequency-domain convolution for large kernels (direct cost explodes above roughly 15^3), - difference-of-Gaussians/bandpass, and **Richardson-Lucy deconvolution** — - the marquee microscopy feature, and iterative (convolve, divide, convolve, multiply per iteration), - which is exactly the shape the pipeline executor accelerates. Sequence R-L after Phase 3. -- Libraries: cuFFT on GPU (ships with the toolkit — no new dependency, but see the NuGet size cross-check above). - CPU: **pocketfft** (BSD-3, header-only, powers NumPy/SciPy). - FFTW is rejected on license (GPL); kissfft is BSD but slower. -- Chunking: overlap-add/overlap-save with kernel-sized padding per chunk — fits the existing `ImageChunk` margin model. - FFT plan and scratch memory must join the chunk-size budget calculation. -- Boundary story (be explicit in docs): circular convolution has no native equivalent of clipped-window renormalization. - Default to **normalized convolution** — divide the zero-padded convolution by the convolution of a ones-mask — - which reproduces the energy-insulated boundary exactly for one extra cacheable transform. - Offer plain pad modes as documented alternatives. - Parity tests against spatial MultiplySum validate the path - (tolerance-based, not bitwise — cuFFT and pocketfft round differently). - -### Phase 5 — GPU-direct disk I/O (assess-only; recommendation: defer) - -Honest assessment of the NVIDIA decoder/storage stack against this project's users -(1-5D microscopy volumes on lab workstations): - -- nvTIFF covers baseline TIFF and a subset of compressions — - OME-TIFF/BigTIFF multi-page hyperstacks with varied bit depths routinely exceed it. -- GPUDirect Storage (cuFile) requires supported filesystems and the `nvidia-fs` driver stack — - DGX-class deployments, not typical lab machines, and Linux-only. -- nvImageCodec targets JPEG-family codecs, not microscopy formats. - -The pragmatic 80% win needs none of that: **pinned-host-memory double-buffered prefetch** (M) — -e.g. Python-side `tifffile`/`zarr` readers filling `cudaHostRegister`-ed buffers that feed the Phase-3 pipeline executor, -overlapping disk I/O with compute using only existing dependencies. -If an I/O layer is ever built, it belongs in the Python package or a `hydra-io` companion — -never in `src/c/Cuda` (the core's zero-I/O design is a feature). -Revisit only after Phase 3 ships and profiling shows I/O-bound pipelines. - -### Ongoing — filter catalog (prioritized for microscopy) - -Current inventory (19 compute commands): Closure, ElementWiseDifference, EntropyFilter, Gaussian, GetMinMax, -HighPassFilter, IdentityFilter, LoG, MaxFilter, MeanFilter, MedianFilter, MinFilter, MultiplySum, NLMeans, -Opener, StdFilter, Sum, VarFilter, WienerFilter (plus 5 meta-commands). - -- **Tier 1 — nearly free compositions, chunking-compatible (S each, any time after Track A's waves land)**: - top-hat/bottom-hat (Opener/Closure + ElementWiseDifference), morphological gradient (Max − Min), - unsharp mask (HighPassFilter exists), difference-of-Gaussians. -- **Tier 2 — new local kernels, chunking-compatible (S-M each)**: - Niblack/Sauvola adaptive thresholding (Mean and Std already exist), Sobel/gradient magnitude, bilateral filter, - anisotropic diffusion (iterative-local — a natural pipeline citizen), - Otsu global threshold (a histogram is a mergeable reduction like GetMinMax, so it is chunk-safe despite being "global-valued"). -- **Tier 3 — fundamentally global: architecture warning.** - Watershed, connected components, distance transform, and morphological reconstruction - require global propagation passes that **conflict with the streaming-chunk architecture**; - correct chunked versions are research-grade border-merging problems. - Declare them out of scope for the chunked engine (or gate behind an explicit fits-on-one-GPU mode), - and document the intended workflow: Hydra for filtering, scikit-image/ITK for global segmentation steps. - Do not let these creep in without that decision. +- **Open questions for the design doc**: descriptor schema and intermediate pixel-type promotion (the `OutMap` machinery); + halo-sum growth for long chains of large kernels (mid-chain re-tiling is research-grade — likely document the limit); + multi-GPU chunk striping interaction with taps (output ordering); error semantics mid-chain; + how strict mode (`HYDRA_REQUIRE_GPU`) applies to whole-pipeline fallback. + +## Step 5 — Operation backlog (seeded now; groomed continuously) + +The explicit, priority-ordered list of operations to implement lives in **[BACKLOG.md](BACKLOG.md)** — +one row per operation with priority, effort, chunking compatibility, dependencies, and status. +Reordering rows is reprioritizing; rows are mirrored to GitHub issues as they are picked up. +It includes the FFT work (frequency-domain convolution, bandpass/DoG, Richardson-Lucy deconvolution), +the cheap morphological compositions, new local kernels, +and the **global operations (connected components, watershed, distance transform)** — +which are listed with an explicit architecture caveat: +they require global propagation passes that conflict with the streaming-chunk design, +so each needs either a fits-on-one-GPU gate or a border-merge design before implementation. + +Key technical notes that constrain backlog items: + +- **FFT stack**: cuFFT on GPU (ships with the toolkit); **pocketfft** on CPU (BSD-3, header-only, powers NumPy/SciPy — + FFTW rejected on GPL license). Chunking via overlap-add/overlap-save fits the existing `ImageChunk` margin model; + FFT plan and scratch memory must join the chunk-size budget. + Boundary story: default to **normalized convolution** (divide the zero-padded convolution by the convolution + of a ones-mask), which reproduces the energy-insulated boundary exactly for one extra cacheable transform. + cuFFT is a large *dynamic* redistributable — adding it breaks the "cudart_static-only, small packages" assumption + that the conda and (future) NuGet packaging rely on; check before landing. +- **Global ops**: correct chunked versions are research-grade border-merging problems. + The supported near-term posture is documented interop — Hydra for filtering, scikit-image/ITK for global + segmentation — unless/until a fits-on-one-GPU mode is added. + +## Later phases (after the five steps; kept on the map, no dates) + +- **C ABI + shared library** (L; design written during Step 2, implemented here): + `src/c/CApi/` with `HydraC.h` (pure C header), one exported function per operation (~24 total), + pixel type as an enum in a caller-owned descriptor struct, 8-way type dispatch inside the shim, + generated from the same `GenCommands.h` X-macros so new ops get C entry points automatically. + Error codes + thread-local `hydra_last_error_message()`; nothing throws across the ABI. + Built as `hydra_c` SHARED, statically linking the backend, `CXX_VISIBILITY_PRESET hidden` — + the templated C++ surface never crosses a DLL boundary. Serves C#, Rust, Julia, Java, and ctypes alike. + Testable without a GPU thanks to Step 3. +- **C# + NuGet** (M + M, after the C ABI): `bindings/csharp/` netstandard2.0 P/Invoke layer over `HydraC.h` + with an ergonomic `Span` API; NuGet with `runtimes/{win-x64,linux-x64}/native` layout from release artifacts; + `dotnet test` runs on the CPU backend. +- **Non-NVIDIA GPU support** (assessment of record): **hipify/ROCm over SYCL/Kokkos** when the time comes. + The kernels are idiomatic CUDA with direct HIP equivalents, and the Step 3 namespace seam accommodates + a `HipBackend::` branch without redesign. SYCL/Kokkos would mean rewriting the kernel layer for a heavy dependency — + unjustified while the CPU backend covers non-NVIDIA users. + Trigger to act: demonstrated user demand plus access to AMD test hardware. Effort if triggered: L-XL. +- **GPU-direct disk I/O** (recommendation: defer): nvTIFF doesn't cover OME-TIFF/BigTIFF hyperstacks; + GPUDirect Storage needs `nvidia-fs`/DGX-class deployments (Linux-only); nvImageCodec targets JPEG-family codecs. + The pragmatic 80% win is **pinned-host-memory double-buffered prefetch** (M) feeding the Step 4 executor + (e.g. `tifffile`/`zarr` readers filling `cudaHostRegister`-ed buffers), overlapping disk I/O with compute. + Any I/O layer belongs in the Python package or a `hydra-io` companion — never in `src/c/Cuda`. + Revisit only after Step 4 ships and profiling shows I/O-bound pipelines. ## Top risks -1. **LFS bandwidth exhaustion** — 435 MB of test data multiplied by a growing CI matrix; address in Track B before adding jobs. +1. **LFS bandwidth exhaustion** — 435 MB of test data multiplied by a growing CI matrix; address in Step 1 before adding jobs. 2. **conda-forge review latency for CUDA recipes** (weeks) — the anaconda.org channel is the hedge; - the CPU variant strengthens the submission. -3. **X-macro edits break all 24 commands at once** — mitigate with staged landing and preprocessor-output diffing (Track A). -4. **cuFFT vs the "cudart_static-only, small redistributables" assumption** that the conda and NuGet packaging rely on — - a standing cross-track check when Phase 4 starts. + the Step 3 CPU variant strengthens the submission. +3. **X-macro edits break all 24 commands at once** — mitigate with staged landing and preprocessor-output diffing (Step 3). +4. **cuFFT vs the "cudart_static-only, small redistributables" assumption** that conda and NuGet packaging rely on — + a standing cross-check whenever FFT backlog items start. 5. **Version-drift regression** — the tag == `VERSION` CI guard is the enforcement; without it the five-version chaos returns. 6. **CPU performance expectations** — document CPU mode as a correctness and accessibility path, not a performance claim. From 830c9d0c3f419e814664a1167e5cd5549bd9cef1 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 29 May 2026 19:29:36 -0500 Subject: [PATCH 26/40] build: adopt scikit-build-core and make the CUDA source self-sufficient Unify the Python build on scikit-build-core so a single `pip install .` drives CMake, compiles the Hydra CUDA extension into the hydra_image_processor package, and installs it. This is the same path used locally, by CI, and by the conda-forge feedstock, which lets the feedstock drop its source patches. - Move pyproject.toml to the repo root; version 4.0.0 is the single source of truth, and __version__ is read from installed metadata. - CMake: raise CUDA_STANDARD to 17, drop the hardcoded GPU architectures in favor of CMAKE_CUDA_ARCHITECTURES (default all-major), add a HYDRA_BUILD_TESTS option, and add package-relative install rules for the extension and shims. - Ship top-level HIP/Hydra compatibility shims so legacy `import HIP` / `import Hydra` keep working. - Remove the superseded in-repo conda-build recipe and MANIFEST.in; ignore build artifacts. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 11 ++++ CMakeLists.txt | 22 ++++++- pyproject.toml | 46 +++++++++++++++ recipe/bld.bat | 41 ------------- recipe/build.sh | 18 ------ recipe/meta.yaml | 62 -------------------- src/Python/HIP.py | 21 +++++++ src/Python/Hydra.py | 16 +++++ src/Python/MANIFEST.in | 1 - src/Python/hydra_image_processor/__init__.py | 10 +++- src/Python/pyproject.toml | 22 ------- src/c/Cuda/CMakeLists.txt | 16 ++--- src/c/Python/CMakeLists.txt | 13 ++++ 13 files changed, 144 insertions(+), 155 deletions(-) create mode 100644 pyproject.toml delete mode 100644 recipe/bld.bat delete mode 100644 recipe/build.sh delete mode 100644 recipe/meta.yaml create mode 100644 src/Python/HIP.py create mode 100644 src/Python/Hydra.py delete mode 100644 src/Python/MANIFEST.in delete mode 100644 src/Python/pyproject.toml diff --git a/.gitignore b/.gitignore index cbb5af0..d3d3117 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,14 @@ src/Python/hydra_image_processor.egg-info # Agent and Build artifacts .claude/ conda-build-dir/ + +# Compiled extension dropped into the package dir by local CMake/pip builds +# (the wheel gets its copy from the CMake install rule, not from git) +/src/Python/hydra_image_processor/*.pyd +/src/Python/hydra_image_processor/*.so +/src/Python/hydra_image_processor/*.dylib + +# Python build artifacts (scikit-build-core / pip / wheels) +/dist/ +*.egg-info/ +*.whl diff --git a/CMakeLists.txt b/CMakeLists.txt index caceafe..6be6087 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,26 @@ -cmake_minimum_required(VERSION 3.22) +cmake_minimum_required(VERSION 3.23) + +# Default GPU architectures when the caller doesn't specify any. +# 'all-major' (requires CMake >= 3.23) targets all major real architectures +# supported by the toolkit, plus PTX of the newest for forward compatibility. +# conda-forge overrides this via CMAKE_ARGS; developers can override with +# -DCMAKE_CUDA_ARCHITECTURES=... (e.g. "native" for a single local GPU). +if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES) + set(CMAKE_CUDA_ARCHITECTURES "all-major" CACHE STRING "CUDA architectures to build for") +endif() project(HydraImageProcessor LANGUAGES C CXX CUDA) set(HYDRA_MODULE_NAME "Hydra") +# Tests build by default for local development; wheel/feedstock builds pass +# -DHYDRA_BUILD_TESTS=OFF to skip the standalone C++ accuracy executable. +option(HYDRA_BUILD_TESTS "Build the C++ accuracy tests" ON) + # Use CMake's modern FindCUDAToolkit module find_package(CUDAToolkit REQUIRED) -# Find OpenMP - use default MSVC OpenMP +# Find OpenMP - uses llvm-openmp from conda-forge on Windows find_package(OpenMP) # Find optional dependencies @@ -17,7 +30,10 @@ find_package(Python3 COMPONENTS Development NumPy OPTIONAL) # Setup backend Hydra CUDA library (static) for CUDA building add_subdirectory(src/c/Cuda) -add_subdirectory(src/c/test_back) + +if (HYDRA_BUILD_TESTS) + add_subdirectory(src/c/test_back) +endif() # Setup MEX interface if Matlab was found if (Matlab_FOUND) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d3310a4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["scikit-build-core>=0.10", "numpy"] +build-backend = "scikit_build_core.build" + +[project] +name = "hydra-image-processor" +version = "4.0.0" +description = "A high-performance, GPU-accelerated (CUDA) image processing library with Python bindings." +readme = "src/Python/README.md" +requires-python = ">=3.10" +license = "BSD-3-Clause" +license-files = ["license.txt"] +authors = [ + { name = "Eric Wait", email = "info@ericwait.com" }, +] +keywords = ["image processing", "python bindings", "high performance", "C++", "CUDA", "GPU"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3", + "Programming Language :: C++", + "Topic :: Scientific/Engineering :: Image Recognition", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", +] +dependencies = ["numpy"] + +[project.urls] +Homepage = "https://hydraimageprocessor.com/" +Repository = "https://github.com/ericwait/hydra-image-processor" +Documentation = "https://hydraimageprocessor.com/" + +[tool.scikit-build] +# Pin behaviour to the minimum scikit-build-core in build-system.requires. +minimum-version = "0.10" +# The CMake project lives at the repository root alongside this file. +cmake.source-dir = "." +cmake.build-type = "Release" +# Only the Python extension is needed for the wheel; skip the C++ test exe. +build.targets = ["HydraPy"] +# Pure-Python package contents (the compiled extension and the top-level +# HIP/Hydra shims are added by CMake `install()` rules). +wheel.packages = ["src/Python/hydra_image_processor"] + +[tool.scikit-build.cmake.define] +HYDRA_BUILD_TESTS = "OFF" diff --git a/recipe/bld.bat b/recipe/bld.bat deleted file mode 100644 index f99be18..0000000 --- a/recipe/bld.bat +++ /dev/null @@ -1,41 +0,0 @@ -setlocal EnableDelayedExpansion - -:: Create a unique build directory for conda -mkdir conda-build-dir -cd conda-build-dir - -:: Configure CMake -:: We need to point to the root CMakeLists.txt -cmake -G "Ninja" ^ - -DCMAKE_BUILD_TYPE=Release ^ - -DPython3_EXECUTABLE="%PYTHON%" ^ - -DCMAKE_INSTALL_PREFIX="%LIBRARY_PREFIX%" ^ - -DCMAKE_CUDA_ARCHITECTURES="75;86;89;120" ^ - "%SRC_DIR%" -if errorlevel 1 exit 1 - -:: Build the HydraPy target -cmake --build . --config Release --target HydraPy -if errorlevel 1 exit 1 - -:: Build the C++ tests -cmake --build . --config Release --target test_accuracy -if errorlevel 1 exit 1 - -:: Run the C++ tests -:: Ninja places the executable in src/c/test_back/ -echo Running C++ Accuracy Tests... -src\c\test_back\test_accuracy.exe -if errorlevel 1 exit 1 - -:: The build should have placed Hydra.pyd into src/Python/hydra_image_processor -:: Verify it exists (optional but good for debugging) -if not exist "%SRC_DIR%\src\Python\hydra_image_processor\Hydra.pyd" ( - echo "Error: Hydra.pyd not found in expected location!" - exit 1 -) - -:: Install the Python package -cd "%SRC_DIR%\src\Python" -"%PYTHON%" -m pip install . --no-deps --ignore-installed -v -if errorlevel 1 exit 1 diff --git a/recipe/build.sh b/recipe/build.sh deleted file mode 100644 index d373b41..0000000 --- a/recipe/build.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -mkdir -p conda-build-dir -cd conda-build-dir - -# Configure CMake -cmake ${CMAKE_ARGS} -G "Ninja" - -DCMAKE_BUILD_TYPE=Release - -DPython3_EXECUTABLE="$PYTHON" - -DCMAKE_INSTALL_PREFIX="$PREFIX" - "$SRC_DIR" - -# Build the HydraPy target -cmake --build . --config Release --target HydraPy - -# Install the Python package -cd "$SRC_DIR/src/Python" -"$PYTHON" -m pip install . --no-deps --ignore-installed -v diff --git a/recipe/meta.yaml b/recipe/meta.yaml deleted file mode 100644 index 0a397c7..0000000 --- a/recipe/meta.yaml +++ /dev/null @@ -1,62 +0,0 @@ -{% set name = "hydra-image-processor" %} -{% set version = "0.1.0" %} - -package: - name: {{ name|lower }} - version: {{ version }} - -source: - path: .. - -build: - number: 0 - script_env: - - CMAKE_GENERATOR=Ninja - -requirements: - build: - - cmake - - ninja - host: - - python - - pip - - setuptools - - wheel - - numpy - - cuda-version 12.4 - run: - - python - - {{ pin_compatible('numpy') }} - - cuda-version >=12.4 - -test: - source_files: - - test_data - - src/Python/Test - imports: - - hydra_image_processor - - hydra_image_processor.cuda - - hydra_image_processor.local - requires: - - tifffile - commands: - - python -c "import hydra_image_processor; print(hydra_image_processor.__version__)" - # Run accuracy tests (will skip if no GPU) - # Note: source_files copies test_data to {test_work_dir}/test_data and src/Python/Test to {test_work_dir}/src/Python/Test - # We need to run the test script. - - python -m unittest src/Python/Test/test_accuracy.py - -about: - home: https://github.com/zfphil/hydra-image-processor - license: BSD-3-Clause - license_file: LICENSE.md - summary: 'A high-performance image processing library with Python bindings.' - description: | - Hydra Image Processor (Hydra) is a collection of signal filters for image analysis - that can handle 1-5 dimensional data (x, y, z, channels, time), efficiently processing - data larger than GPU memory by optimally chunking it. - dev_url: https://github.com/zfphil/hydra-image-processor - -extra: - recipe-maintainers: - - ericwait diff --git a/src/Python/HIP.py b/src/Python/HIP.py new file mode 100644 index 0000000..2bfacfb --- /dev/null +++ b/src/Python/HIP.py @@ -0,0 +1,21 @@ +"""Backwards-compatibility shim for the legacy top-level ``HIP`` module. + +Historically the conda-forge package shipped a top-level ``HIP`` C extension +exposing the raw Hydra API (e.g. ``HIP.Gaussian``). That extension is now +installed as :mod:`hydra_image_processor.Hydra`. This shim re-exports it so +that existing ``import HIP`` code keeps working unchanged. + +New code should prefer the Pythonic wrapper API:: + + import hydra_image_processor as HIP + + HIP.gaussian(image, sigmas=[2, 2, 1]) +""" + +import sys as _sys + +from hydra_image_processor import Hydra as _Hydra + +# Make ``import HIP`` resolve to the compiled extension module itself, matching +# the historical top-level module one-to-one. +_sys.modules[__name__] = _Hydra diff --git a/src/Python/Hydra.py b/src/Python/Hydra.py new file mode 100644 index 0000000..8152347 --- /dev/null +++ b/src/Python/Hydra.py @@ -0,0 +1,16 @@ +"""Backwards-compatibility shim exposing the compiled extension as ``Hydra``. + +The compiled CUDA extension is installed as +:mod:`hydra_image_processor.Hydra`. This shim re-exports it at the top level so +that ``import Hydra`` continues to resolve to the extension module. + +New code should prefer the Pythonic wrapper API:: + + import hydra_image_processor as HIP +""" + +import sys as _sys + +from hydra_image_processor import Hydra as _Hydra + +_sys.modules[__name__] = _Hydra diff --git a/src/Python/MANIFEST.in b/src/Python/MANIFEST.in deleted file mode 100644 index 677cea1..0000000 --- a/src/Python/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -recursive-include hydra_image_processor *.pyd *.dll *.so *.dylib diff --git a/src/Python/hydra_image_processor/__init__.py b/src/Python/hydra_image_processor/__init__.py index 478d5c0..779eeb3 100644 --- a/src/Python/hydra_image_processor/__init__.py +++ b/src/Python/hydra_image_processor/__init__.py @@ -76,7 +76,15 @@ - identity_filter """ -__version__ = "0.1.0" +from importlib.metadata import PackageNotFoundError, version as _pkg_version + +try: + # Single source of truth: the version declared in pyproject.toml and + # recorded in the installed package metadata. + __version__ = _pkg_version("hydra-image-processor") +except PackageNotFoundError: # running from a source tree without an install + __version__ = "0.0.0+unknown" + __author__ = "Eric Wait" __email__ = "info@ericwait.com" diff --git a/src/Python/pyproject.toml b/src/Python/pyproject.toml deleted file mode 100644 index 1753bf4..0000000 --- a/src/Python/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "hydra-image-processor" -version = "0.1.0" -description = "A high-performance image processing library with Python bindings." -readme = "README.md" -requires-python = ">=3.9" -license = "BSD-3-Clause" -authors = [ - {name = "Eric Wait", email = "info@ericwait.com"} -] -keywords = ["image processing", "python bindings", "high performance", "C++", "CUDA"] - -[tool.setuptools] -include-package-data = true - -[tool.setuptools.packages.find] -include = ["hydra_image_processor*"] -exclude = ["Test*"] diff --git a/src/c/Cuda/CMakeLists.txt b/src/c/Cuda/CMakeLists.txt index 4304dae..514563d 100644 --- a/src/c/Cuda/CMakeLists.txt +++ b/src/c/Cuda/CMakeLists.txt @@ -1,8 +1,10 @@ # Set a variable to turn on/off PROCESS_MUTEX support option(USE_PROCESS_MUTEX "Use process-level mutex to guard GPU calls" OFF) -# Common settings for both libraries -set(CUDA_ARCHITECTURES "75;86;89;120") +# Common settings for both libraries. +# GPU architectures are controlled by CMAKE_CUDA_ARCHITECTURES (defaulted in +# the top-level CMakeLists.txt and overridable by conda-forge / developers), +# so we intentionally do NOT hardcode an architecture list here. set(COMMON_INCLUDES $ $ @@ -78,9 +80,10 @@ set(COMMON_CPP_SOURCES # Determine the OpenMP flag based on the compiler if(MSVC) message(STATUS "MSVC compiler detected") - # Use default MSVC OpenMP (vcomp) - set(OPENMP_FLAG "/openmp") - message(STATUS "Using MSVC OpenMP (vcomp)") + # Use LLVM OpenMP (/openmp:llvm) so the runtime matches conda-forge's llvm-openmp package. + # /openmp (vcomp) would conflict with libomp.dll loaded by other conda packages. + set(OPENMP_FLAG "/openmp:llvm") + message(STATUS "Using LLVM OpenMP (/openmp:llvm)") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") message(STATUS "GCC/Clang compiler detected") set(OPENMP_FLAG "-fopenmp") @@ -111,10 +114,9 @@ function(setup_cuda_library LIB_NAME STATIC_OR_SHARED) endif() set_target_properties(${LIB_NAME} PROPERTIES - CUDA_STANDARD 11 + CUDA_STANDARD 17 CUDA_SEPARABLE_COMPILATION ON CUDA_RESOLVE_DEVICE_SYMBOLS ON - CUDA_ARCHITECTURES "${CUDA_ARCHITECTURES}" POSITION_INDEPENDENT_CODE ON ) diff --git a/src/c/Python/CMakeLists.txt b/src/c/Python/CMakeLists.txt index 56ad62c..1f0cb00 100644 --- a/src/c/Python/CMakeLists.txt +++ b/src/c/Python/CMakeLists.txt @@ -96,3 +96,16 @@ target_sources(HydraPy HydraPyModule.cpp PyCommandModule.cpp ) + +# --- Installation (used by scikit-build-core to assemble the wheel) --- +# Place the compiled extension module inside the hydra_image_processor package. +# (MODULE libraries are always treated as LIBRARY targets, on Windows too.) +install(TARGETS HydraPy LIBRARY DESTINATION hydra_image_processor) + +# Backwards-compatibility shims so legacy `import HIP` / `import Hydra` keep +# working. Preferred form going forward is `import hydra_image_processor as HIP`. +install(FILES + "${PROJECT_SOURCE_DIR}/src/Python/HIP.py" + "${PROJECT_SOURCE_DIR}/src/Python/Hydra.py" + DESTINATION "." +) From 4ab541385a78ff3e5a745d9331b7755dee89dcea Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 29 May 2026 19:29:43 -0500 Subject: [PATCH 27/40] ci: add Windows+Linux build-gate and tag-driven release Replace the Windows-only conda-build workflow (which uploaded to a separate anaconda.org channel) with two workflows: - ci.yml: build and import the package on Windows and Linux for PRs and pushes, catching breakage before it reaches the conda-forge feedstock. Runners have no GPU, so GPU tests auto-skip; this only verifies the package builds and imports. - release.yml: on a `v*` tag, verify the tag matches pyproject's version and create a GitHub Release, which the conda-forge autotick bot picks up to open a feedstock version-bump PR. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 78 +++++++++++++++++++++++++++++++ .github/workflows/conda-build.yml | 58 ----------------------- .github/workflows/release.yml | 47 +++++++++++++++++++ 3 files changed, 125 insertions(+), 58 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/conda-build.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..88b834f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +# Build-gate: compile the CUDA extension and import the package on the same +# platforms conda-forge ships (Windows + Linux). This catches source/recipe +# drift *before* a release reaches the feedstock. Runners have no GPU, so we +# only verify the package builds and imports; the full TIFF accuracy suite is +# run locally on a GPU box before tagging a release. + +on: + push: + branches: [main, develop, conda-forge] + pull_request: + branches: [main, develop] + workflow_dispatch: + inputs: + python-matrix: + description: "Comma-separated Python versions to build" + default: "3.12" + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-gate: + name: Build & import (${{ matrix.os }}, py${{ matrix.python }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + python: ["3.12"] + + defaults: + run: + shell: bash -el {0} + + steps: + - uses: actions/checkout@v4 + with: + lfs: false # source build does not need the LFS test images + + - name: Install CUDA toolkit (compiler only; runners have no GPU) + uses: Jimver/cuda-toolkit@v0.2.21 + with: + cuda: "12.6.2" + method: network + + - name: Set up MSVC (Windows) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + + - uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-version: latest + python-version: ${{ matrix.python }} + channels: conda-forge + + - name: Install build dependencies + run: conda install -y -c conda-forge cmake ninja "scikit-build-core>=0.10" numpy pip + + - name: Build & install (mirrors the feedstock pip path) + env: + # A single architecture keeps the gate fast; we only need it to + # compile + import, not to run on a specific GPU. + CMAKE_ARGS: "-DCMAKE_CUDA_ARCHITECTURES=75" + run: python -m pip install . --no-build-isolation -v + + - name: Import smoke test (no GPU required to import) + run: | + python - <<'PY' + import hydra_image_processor as hip + import HIP, Hydra # legacy back-compat shims + print("hydra_image_processor", hip.__version__) + assert hip.__version__ == "4.0.0", hip.__version__ + assert HIP is Hydra, "shims should both alias the compiled extension" + print("device_count:", hip.device_count()) # 0 on a GPU-less runner + PY diff --git a/.github/workflows/conda-build.yml b/.github/workflows/conda-build.yml deleted file mode 100644 index 2bb76fc..0000000 --- a/.github/workflows/conda-build.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Conda Build - -on: - push: - branches: [ "main", "master" ] - pull_request: - branches: [ "main", "master" ] - workflow_dispatch: - -jobs: - build-conda: - name: Build Conda Package (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [windows-latest] - python-version: ["3.10", "3.11", "3.12"] - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install CUDA Toolkit - uses: Jimver/cuda-toolkit@v0.2.14 - id: cuda-toolkit - with: - cuda: '12.4.1' - - - name: Set up Conda - uses: conda-incubator/setup-miniconda@v3 - with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - channels: conda-forge, defaults - - - name: Install Conda Build Tools - shell: bash -l {0} - run: | - conda install -y conda-build conda-verify anaconda-client - - - name: Build Conda Package - shell: bash -l {0} - run: | - conda build recipe --python ${{ matrix.python-version }} - - - name: Upload to Anaconda (Optional) - # Only upload on main branch pushes, not PRs - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - shell: bash -l {0} - env: - ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} - run: | - if [ -n "$ANACONDA_TOKEN" ]; then - anaconda -t $ANACONDA_TOKEN upload --user hydra-image-processor $(conda build recipe --output) - else - echo "ANACONDA_TOKEN not found, skipping upload." - fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..db9a3c6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release + +# Tag-driven release. Pushing a `vX.Y.Z` tag (after the CI gate is green on +# main) creates a GitHub Release. conda-forge's autotick bot detects the new +# release, opens a version-bump PR on the feedstock (updating version + sha256), +# and once merged the feedstock builds and publishes every variant. +# +# Release checklist: +# 1. Bump `[project].version` in pyproject.toml and update CHANGELOG.md. +# 2. Merge to main with a green CI gate. +# 3. git tag vX.Y.Z && git push origin vX.Y.Z (must match pyproject version) +# 4. Merge the feedstock autotick PR (or open one manually). + +on: + push: + tags: ["v*"] + +permissions: + contents: write # required to create a GitHub Release + +jobs: + release: + name: Create GitHub Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + lfs: false + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Verify tag matches pyproject version + run: | + tag="${GITHUB_REF_NAME#v}" + pyproj="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")" + echo "tag=$tag pyproject=$pyproj" + if [ "$tag" != "$pyproj" ]; then + echo "::error::Tag '$GITHUB_REF_NAME' does not match pyproject version '$pyproj'" + exit 1 + fi + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes --verify-tag From aa29ba40e612c28efa43ba34af00c7ae69f9661c Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 29 May 2026 19:29:50 -0500 Subject: [PATCH 28/40] docs: document conda-forge install and the build/release workflow - README: conda/mamba/pixi install, the NVIDIA GPU requirement, the unsupported pip/uv path, and the import convention. - Rewrite the Python package README for the new hydra_image_processor API and the HIP/Hydra back-compat shims. - Add CHANGELOG.md (4.0.0) and a build + release checklist in CONTRIBUTING.md. - Refresh GEMINI.md to reflect scikit-build-core and conda-forge distribution. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 33 +++++++++++++++ CONTRIBUTING.md | 38 +++++++++++++++++ GEMINI.md | 14 ++++--- readme.md | 47 +++++++++++++++++++++ src/Python/README.md | 98 +++++++++++++++++++++++--------------------- 5 files changed, 177 insertions(+), 53 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6cc94a7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to this project are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/), and the project adheres to +[Semantic Versioning](https://semver.org/). + +## [4.0.0] - Unreleased + +### Changed +- **Repackaged the Python distribution.** The library is imported as the + `hydra_image_processor` package (conventionally `import hydra_image_processor as HIP`), + which wraps the compiled `Hydra` CUDA extension and exposes a Pythonic, snake_case API + (e.g. `gaussian`, `device_count`). +- **Unified the build on [scikit-build-core].** A single `pip install .` now drives CMake, + builds the extension, and installs the package — the same path used locally, in CI, and by + the conda-forge feedstock. This retires the recipe-side patches the feedstock had to carry. +- Made the source self-sufficient for conda-forge: CUDA C++ standard raised to 17, hardcoded + GPU architectures removed in favor of `CMAKE_CUDA_ARCHITECTURES` (default `all-major`), and + a package-relative install target added. +- Single source of truth for the version (`pyproject.toml`); `__version__` is now read from + the installed package metadata. + +### Added +- Top-level `HIP` and `Hydra` compatibility shims so legacy `import HIP` / `import Hydra` + code keeps working. +- Windows + Linux CI build-gate (`.github/workflows/ci.yml`) and a tag-driven release + workflow with a version guard (`.github/workflows/release.yml`). + +### Removed +- The in-repo conda-build recipe (`recipe/`) and the anaconda.org upload workflow. + Distribution is now solely via conda-forge. + +[scikit-build-core]: https://scikit-build-core.readthedocs.io/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8bdde87..346d2df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,3 +20,41 @@ From the very beginning Hydra was written to be clean, consistent, and as clear 1. _**Try crazy things**_! Hydra was built to quickly get operations onto GPU hardware. Use this opportunity to create things that would not otherwise be tractable. ### These guidelines are intended for GitHub pull requests. Do not let them be a barrier to experimentation. Have FUN! + +# Building & testing locally + +The Python package builds with [scikit-build-core](https://scikit-build-core.readthedocs.io/): +a single `pip install` configures CMake, compiles the CUDA extension, and installs the +package. You need a CUDA toolkit, a C++ compiler, and a conda environment with the build +tools: + +```bash +conda create -n hydra-dev -c conda-forge python numpy cmake ninja scikit-build-core pip +conda activate hydra-dev +pip install . --no-build-isolation -v # from the repository root + +# Smoke test (no GPU required to import) +python -c "import hydra_image_processor as HIP; print(HIP.__version__, HIP.device_count())" +``` + +The full TIFF accuracy suite (`src/Python/Test`) and the C++ `test_accuracy` binary require a +real NVIDIA GPU and the LFS-tracked ground-truth images (`git lfs pull`). CI runners have no +GPU, so they only verify the package builds and imports; **run the accuracy suite locally on a +GPU box before tagging a release.** + +# Releasing + +Releases are tag-driven. conda-forge's autotick bot watches for new GitHub releases and opens +a feedstock version-bump PR automatically. + +1. Run the full accuracy suite locally on a GPU and confirm it passes. +2. Bump `[project].version` in `pyproject.toml` and add a section to `CHANGELOG.md`. +3. Merge to `main` with a green CI gate. +4. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z` (the tag **must** match the + `pyproject.toml` version — the release workflow enforces this). +5. The `Release` workflow creates the GitHub Release. Merge the resulting feedstock autotick + PR (or open one manually), updating the recipe if the build inputs changed. + +The conda-forge recipe lives in the +[`hydra-image-processor-feedstock`](https://github.com/conda-forge/hydra-image-processor-feedstock) +repository and is a thin `pip install .` wrapper — keep it free of source patches. diff --git a/GEMINI.md b/GEMINI.md index d3c2d60..b7009c4 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -43,8 +43,8 @@ To build and install the Python package locally: # Navigate to the project root cd E:\programming\hydra-image-processor -# Install in editable mode (requires scikit-build-core or manual cmake invocation usually, -# but this project uses a standard setup.py/cmake flow or pip install .) +# This project uses scikit-build-core: `pip install` drives CMake, compiles the +# CUDA extension, and installs the package. pip install . ``` @@ -55,13 +55,15 @@ cmake --preset dev cmake --build build --config Release --target HydraPy ``` -### Conda Package Build +### Conda Package Distribution -To build the Conda package: +The package is distributed via the conda-forge feedstock +(`conda-forge/hydra-image-processor-feedstock`), a thin `pip install .` wrapper +around this repository's scikit-build-core build. There is no in-repo recipe; +releases are tag-driven (see CONTRIBUTING.md). Install with: ```bash -conda install conda-build -conda build recipe +conda install -c conda-forge hydra-image-processor ``` ### MATLAB Build diff --git a/readme.md b/readme.md index df12f97..23e16a5 100644 --- a/readme.md +++ b/readme.md @@ -8,6 +8,53 @@ This library is licensed under BSD 3-Clause to encourage use in open-source and My only plea is that if you find bugs or make changes that you contribute them back to this repository. Happy processing and enjoy! +## Installation + +Hydra is distributed through [conda-forge](https://conda-forge.org/) and requires an +NVIDIA CUDA-capable GPU with an up-to-date driver. Install it with conda, mamba, or pixi: + +```bash +# conda / mamba +conda install -c conda-forge hydra-image-processor +mamba install -c conda-forge hydra-image-processor + +# pixi (per-project) +pixi add hydra-image-processor +``` + +The CUDA runtime is statically linked, so no separate CUDA toolkit is required to *import* +the package — only an NVIDIA GPU driver to *run* GPU operations. + +> **pip / uv are not supported.** Hydra is a CUDA-only compiled extension with no PyPI +> wheels. Use a conda-based installer (conda, mamba, or pixi). + +### Usage + +```python +import hydra_image_processor as HIP +import numpy as np + +image = np.random.rand(100, 100, 50).astype(np.float32) +smoothed = HIP.gaussian(image, sigmas=[2.0, 2.0, 1.0]) +print("GPUs:", HIP.device_count()) +``` + +Code written against older releases that used `import HIP` (or `import Hydra`) keeps working +via compatibility shims, though `import hydra_image_processor as HIP` is preferred. + +### Building from source + +Source builds use [scikit-build-core](https://scikit-build-core.readthedocs.io/): a single +`pip install` drives CMake, compiles the CUDA extension, and installs the package. From a +conda environment that has the build tools (`cmake`, `ninja`, `scikit-build-core`, `numpy`) +plus a CUDA toolkit and C++ compiler: + +```bash +pip install . +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the full developer and release workflow. + ## Quick Start Guide [https://www.hydraimageprocessor.com/quick-start](https://www.hydraimageprocessor.com/quick-start) diff --git a/src/Python/README.md b/src/Python/README.md index ece535e..f67bbae 100644 --- a/src/Python/README.md +++ b/src/Python/README.md @@ -1,75 +1,79 @@ -# Hydra Python Module +# Hydra Image Processor (Python) -This directory contains the Hydra Python extension module for GPU-accelerated image processing. +`hydra-image-processor` provides GPU-accelerated (CUDA) image-processing operations for +1–5 dimensional data (x, y, z, channels, time), efficiently processing data larger than GPU +memory by optimally chunking it. -## Files +## Installation -- **Hydra.pyd** - The Python extension module (Windows) -- **libomp140.x86_64.dll** - OpenMP runtime library (bundled for portability) -- **test_import.py** - Test script to verify the module imports correctly +Distributed through [conda-forge](https://conda-forge.org/). Requires an NVIDIA +CUDA-capable GPU with an up-to-date driver. -## Distribution - -To distribute this module to others, simply copy both files: -1. `Hydra.pyd` -2. `libomp140.x86_64.dll` - -Keep them in the same directory, and Python will automatically find the DLL when importing the module. +```bash +conda install -c conda-forge hydra-image-processor +# or: mamba install -c conda-forge hydra-image-processor +# or: pixi add hydra-image-processor +``` -## Requirements +> **pip / uv are not supported.** This is a CUDA-only compiled extension with no PyPI +> wheels — install it with conda, mamba, or pixi. -- Python 3.12 -- NumPy -- NVIDIA CUDA-capable GPU (for running the image processing operations) +The CUDA runtime is statically linked, so importing the package needs no separate CUDA +toolkit install; an NVIDIA driver is required to run GPU operations. ## Usage ```python -import Hydra +import hydra_image_processor as HIP +import numpy as np # Check available CUDA devices -num_devices = Hydra.DeviceCount() -print(f"Available CUDA devices: {num_devices}") +print(f"Found {HIP.device_count()} GPU(s)") -# Get help on available functions -Hydra.Help() +# Apply a Gaussian smoothing +image = np.random.rand(100, 100, 50).astype(np.float32) +smoothed = HIP.gaussian(image, sigmas=[2.0, 2.0, 1.0]) -# Example: Apply Gaussian filter -# result = Hydra.Gaussian(input_array, sigma=1.5) +# Median filter with a custom kernel +kernel = HIP.make_ball_mask(radius=3) +filtered = HIP.median_filter(image, kernel) ``` -## Testing +### Package layout -Run the test script to verify the module is working: +- `hydra_image_processor` — the public, Pythonic API (snake_case, e.g. `gaussian`, + `device_count`). This is the recommended entry point. +- `hydra_image_processor.cuda` — thin wrappers over the compiled `Hydra` CUDA extension. +- `hydra_image_processor.utils` — helpers such as mask creation. -```bash -python test_import.py +### Backwards compatibility + +Earlier releases shipped a top-level `HIP` (and `Hydra`) module exposing the raw extension +API (e.g. `HIP.Gaussian`). Those imports still work via compatibility shims: + +```python +import HIP # legacy: resolves to the compiled extension (raw API) +import Hydra # legacy: same compiled extension ``` -## Building from Source +New code should prefer `import hydra_image_processor as HIP`. -The module is built using CMake with the following key features: -- Linked against Python 3.12 -- Uses LLVM OpenMP (`/openmp:llvm`) for better performance -- Statically links CUDA runtime -- Automatically bundles the OpenMP DLL for portability +## Building from source -To rebuild: +Builds use [scikit-build-core](https://scikit-build-core.readthedocs.io/); a single +`pip install` configures CMake, compiles the CUDA extension, and installs the package. +From a conda environment with `cmake`, `ninja`, `scikit-build-core`, and `numpy`, plus a +CUDA toolkit and C++ compiler available: ```bash -cd ../.. # Go to project root -cmake --preset dev -cmake --build build --config Release --target HydraPy +# from the repository root (where pyproject.toml lives) +pip install . ``` -The build system will automatically: -1. Find the correct Python 3.12 installation -2. Link against the hydra conda environment -3. Copy the OpenMP DLL to this directory +To build for a specific GPU architecture instead of the default `all-major`: -## Notes +```bash +CMAKE_ARGS="-DCMAKE_CUDA_ARCHITECTURES=native" pip install . +``` -- The module requires the OpenMP DLL at runtime. This is automatically bundled during the build process. -- The module was compiled against Python 3.12.11 and requires that specific Python version. -- CUDA runtime is statically linked, so no separate CUDA runtime installation is needed for basic import. -- However, NVIDIA GPU drivers must be installed to actually run GPU operations. +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for the full developer and release workflow. From f0f260bcff693045c7070b7951de7358a57f0828 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Fri, 29 May 2026 19:34:20 -0500 Subject: [PATCH 29/40] docs: add release testing checklist A standing pre-release verification checklist (TESTING.md) covering the local build, GPU accuracy suite, CI gate, feedstock dry-run, and release rehearsal. The GPU correctness steps must be run locally since CI is build-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- TESTING.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 TESTING.md diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..658e4a3 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,64 @@ +# Release testing checklist + +Pre-release verification for `hydra-image-processor`. CI is **build-only** (runners have no +GPU), so the GPU correctness steps must be run locally before tagging. Work top to bottom; +each phase builds on the previous one. See [CONTRIBUTING.md](CONTRIBUTING.md) for the release +flow this feeds into. + +## A. Local build from a clean tree +- [ ] Clean tree: `git status` shows no stray build artifacts (e.g. a leftover `Hydra.pyd`). +- [ ] Fresh build from the repo root, inside a VS dev shell, in a clean conda env: + `pip install . --no-build-isolation -v` +- [ ] Build log shows the wheel layout: `hydra_image_processor/Hydra.pyd`, top-level `HIP.py`, + top-level `Hydra.py`, and `hydra_image_processor-4.0.0-...whl`. +- [ ] Building for all archs (closer to what conda-forge ships) also works: + `set CMAKE_ARGS=-DCMAKE_CUDA_ARCHITECTURES=all-major` then reinstall. + +## B. Import & API surface +- [ ] `python -c "import hydra_image_processor as HIP; print(HIP.__version__)"` → `4.0.0` +- [ ] `import HIP` and `import Hydra` both work and are the same object as + `hydra_image_processor.Hydra`. +- [ ] Legacy raw API reachable via shim (`HIP.Gaussian(...)`, capitalized) **and** the + pythonic API (`hydra_image_processor.gaussian(...)`, lowercase). + +## C. GPU correctness — the part CI cannot do +- [ ] `git lfs pull` to fetch the real ground-truth images (the GitHub tarball has only + LFS pointers, not the binaries). +- [ ] Build and run the C++ accuracy test (`-DHYDRA_BUILD_TESTS=ON`, target `test_accuracy`) + → `Test Summary: PASSED`. +- [ ] Run the Python TIFF accuracy suite in `src/Python/Test` against `test_data/` → all pass. + **This is the gate for tagging.** +- [ ] Spot-check one op end-to-end (e.g. `gaussian` on a small array) and `device_count() >= 1`. + +## D. Working-tree hygiene +- [ ] After building, `git status` is still clean — the in-source `Hydra.pyd` is ignored: + `git check-ignore src/Python/hydra_image_processor/Hydra.pyd` prints the path. +- [ ] `git log --oneline` shows the build/ci/docs commits; the `Hydra.mexw64` change remains + separate (commit or discard it intentionally). + +## E. CI workflows (GitHub) +- [ ] Push the working branch → `ci.yml` runs and **both** `ubuntu-latest` and `windows-latest` + jobs build and pass the import smoke test (`__version__ == 4.0.0`, `HIP is Hydra`). +- [ ] Open a PR to `main` → the gate runs on the PR. +- [ ] `release.yml` version guard sanity: the tag (minus `v`) must equal the `pyproject.toml` + version. (An `rc` tag won't match unless `pyproject` is set to that rc.) + +## F. Feedstock dry-run (proves the patches are truly gone) +- [ ] In `hydra-image-processor-feedstock` on branch `prepare-v4.0.0`, temporarily point + `source` at the local checkout / branch tarball, then run `python build-locally.py` + (or `pixi run`) for one Windows variant → builds **with no patches** and the + `import hydra_image_processor` test passes. + +## G. Release rehearsal (when A–F are green) +- [ ] Tag `v4.0.0` and push → `release.yml` passes the version guard and creates the GitHub + Release. +- [ ] Update `source.sha256` in the feedstock `prepare-v4.0.0` recipe to the `v4.0.0` tarball + hash; push and open the feedstock PR (or let the autotick bot open one and graft the + recipe changes onto it). +- [ ] Feedstock CI (Azure/GitHub) builds all variants green. + +## H. End-user install (post-publish) +- [ ] In a fresh env, each installer resolves and imports: + `conda install -c conda-forge hydra-image-processor`, `mamba install ...`, + `pixi add hydra-image-processor` + → `python -c "import hydra_image_processor as HIP; print(HIP.__version__)"`. From 246d75b4fc52bd07eefeb29441c83959c86046d8 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 18:54:57 -0500 Subject: [PATCH 30/40] fix(python): center make_ellipsoid_mask on the kernel midpoint The coordinate grid was shifted by vol_size/2 + 0.5, putting the ellipsoid center one voxel off (e.g. -6..+4 instead of -5..+5 for radius 5). Every morphological op using the mask diverged from the C++ createEllipsoidKernel / MATLAB results. With the fix, all eight ellipsoid-kernel ops match the MATLAB-generated ground truth exactly. Co-Authored-By: Claude Fable 5 --- src/Python/hydra_image_processor/utils/masks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Python/hydra_image_processor/utils/masks.py b/src/Python/hydra_image_processor/utils/masks.py index 032df1e..399b307 100644 --- a/src/Python/hydra_image_processor/utils/masks.py +++ b/src/Python/hydra_image_processor/utils/masks.py @@ -131,10 +131,11 @@ def make_ellipsoid_mask( # Create coordinate grids x, y, z = np.mgrid[0:vol_size[0], 0:vol_size[1], 0:vol_size[2]] - # Shift origin to center - x = x - vol_size[0] / 2 - 0.5 - y = y - vol_size[1] / 2 - 0.5 - z = z - vol_size[2] / 2 - 0.5 + # Shift origin to the center voxel, matching the C++ createEllipsoidKernel + # (for odd sizes the center lands on an integer index) + x = x - (vol_size[0] - 1) / 2 + y = y - (vol_size[1] - 1) / 2 + z = z - (vol_size[2] - 1) / 2 # Avoid division by zero for zero radii axes_radius = np.maximum(axes_radius, 1e-10) From 74711fc47788a86d26cbd9907d55acfdc8b5400a Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 18:55:14 -0500 Subject: [PATCH 31/40] test: repair the C++ accuracy test build and CTest registration test_accuracy.cpp never compiled: it called a nonexistent ImageOwner::getLinearAddress and passed a float[3] where the generated CudaCall_Gaussian::run stub takes Vec plus an lvalue ImageView for the output. The Gaussian energy-preservation check also assumed a volume too shallow for its sigma; the filter renormalizes truncated kernels at borders, so the impulse support must stay interior for the sum to be preserved. enable_testing() now runs at the top level so `ctest --test-dir build` actually finds CppAccuracyTest. Co-Authored-By: Claude Fable 5 --- CMakeLists.txt | 6 ++++++ src/c/test_back/test_accuracy.cpp | 27 ++++++++++++++++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6be6087..5a6689f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,12 @@ set(HYDRA_MODULE_NAME "Hydra") # -DHYDRA_BUILD_TESTS=OFF to skip the standalone C++ accuracy executable. option(HYDRA_BUILD_TESTS "Build the C++ accuracy tests" ON) +# enable_testing() must run at the top level for ctest to see tests +# registered in subdirectories. +if (HYDRA_BUILD_TESTS) + enable_testing() +endif() + # Use CMake's modern FindCUDAToolkit module find_package(CUDAToolkit REQUIRED) diff --git a/src/c/test_back/test_accuracy.cpp b/src/c/test_back/test_accuracy.cpp index e5f8baf..7a12d94 100644 --- a/src/c/test_back/test_accuracy.cpp +++ b/src/c/test_back/test_accuracy.cpp @@ -38,25 +38,34 @@ bool test_gaussian() { - Vec dims = {64, 64, 10}; + // Keep the impulse's Gaussian support (~3*sigma) away from every face: + // the filter renormalizes truncated kernels at the borders, so energy is + // only preserved while the smoothed impulse stays interior. + Vec dims = {64, 64, 32}; ImageOwner imIn(0, dims, 1, 1); ImageOwner imOut(0, dims, 1, 1); - + // Fill with a impulse in the middle std::fill(imIn.getPtr(), imIn.getPtr() + imIn.getNumElements(), 0.0f); - imIn.getPtr()[imIn.getLinearAddress({32, 32, 5, 0, 0})] = 100.0f; - - float sigmas[3] = {2.0f, 2.0f, 2.0f}; - CudaCall_Gaussian::run(imIn, imOut, sigmas, 1, 0); - + ImageDimensions midCoord(Vec(32, 32, 16), 0, 0); + imIn.getPtr()[imIn.getDims().linearAddressAt(midCoord)] = 100.0f; + + Vec sigmas(2.0, 2.0, 2.0); + ImageView imOutView(imOut); + CudaCall_Gaussian::run(imIn, imOutView, sigmas, 1, 0); + // Check if it smoothed (sum should be preserved approximately) double sumIn = 0; double sumOut = 0; for(size_t i=0; i 0.0f && peakOut < 100.0f, "Gaussian filter should smooth the impulse peak"); + return true; } From 81bd52ce5040f64adab4f8a349fbdacc537a29b1 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 18:55:14 -0500 Subject: [PATCH 32/40] test(python): add the TIFF accuracy suite TESTING.md gates on Port of src/MATLAB/+Test/AccuracyTest.m: 15 operations compared against the ground-truth TIFFs in test_data/ (integer results within 1 LSB, float within 1e-4, EntropyFilter as a soft check since its ground truth was saved min-max scaled). Documents the numpy<->Hydra axis convention: arrays pass as tifffile loads them (chan, z, y, x) with vector parameters in (z, y, x) order. Co-Authored-By: Claude Fable 5 --- TESTING.md | 3 +- src/Python/Test/test_accuracy.py | 175 +++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/Python/Test/test_accuracy.py diff --git a/TESTING.md b/TESTING.md index 658e4a3..e7e9e4f 100644 --- a/TESTING.md +++ b/TESTING.md @@ -26,7 +26,8 @@ flow this feeds into. LFS pointers, not the binaries). - [ ] Build and run the C++ accuracy test (`-DHYDRA_BUILD_TESTS=ON`, target `test_accuracy`) → `Test Summary: PASSED`. -- [ ] Run the Python TIFF accuracy suite in `src/Python/Test` against `test_data/` → all pass. +- [ ] Run the Python TIFF accuracy suite (needs `tifffile`): + `python src/Python/Test/test_accuracy.py` → `Test Summary: PASSED`. **This is the gate for tagging.** - [ ] Spot-check one op end-to-end (e.g. `gaussian` on a small array) and `device_count() >= 1`. diff --git a/src/Python/Test/test_accuracy.py b/src/Python/Test/test_accuracy.py new file mode 100644 index 0000000..2eb077e --- /dev/null +++ b/src/Python/Test/test_accuracy.py @@ -0,0 +1,175 @@ +""" +GPU accuracy tests for hydra_image_processor. + +Python port of src/MATLAB/+Test/AccuracyTest.m: runs each operation on the +two-channel test volume in test_data/ and compares the result against the +ground-truth TIFFs generated by the MATLAB suite. This is the release gate +described in TESTING.md; CI cannot run it because its runners have no GPU. + +Axis convention +--------------- +tifffile loads a TIFF stack as (z, y, x). Hydra maps numpy axes onto its +native (x, y, z, chan, frame) order fastest-to-slowest, so volumes are +passed exactly as tifffile loads them, channels are stacked on axis 0 +-> (chan, z, y, x), and vector parameters (sigmas, radii) are given in +(z, y, x) order — the reverse of the MATLAB test, which uses (x, y, z). + +Tolerances +---------- +Integer ground truth allows a 1 LSB difference (rounding jitter across +CUDA toolkit/GPU generations); float ground truth uses the same 1e-4 +bound as the MATLAB suite. + +Requires: numpy, tifffile, a CUDA device, and `git lfs pull` so +test_data/ holds real TIFF binaries rather than LFS pointers. + +Usage: python test_accuracy.py +""" + +import os +import sys + +import numpy as np + +try: + import tifffile +except ImportError: + print("[SKIP] tifffile is required (conda install tifffile)") + sys.exit(0) + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) + +import hydra_image_processor as hip + +TEST_DATA_DIR = os.path.normpath( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "test_data") +) + +INT_TOL = 1 # max absolute difference for integer ground truth +FLOAT_TOL = 1e-4 # max absolute difference for float ground truth (same as MATLAB) + +_failures = [] + + +def load_tif(name): + path = os.path.join(TEST_DATA_DIR, name) + return tifffile.imread(path) + + +def check(op_name, actual, expected_file): + """Compare one output channel against a ground-truth TIFF.""" + expected = load_tif(expected_file) + actual = np.asarray(actual) + + if actual.shape != expected.shape: + _failures.append(f"{op_name}: shape {actual.shape} != {expected.shape} ({expected_file})") + print(f" [FAILED] {expected_file}: shape mismatch {actual.shape} vs {expected.shape}") + return + + diff = np.abs(actual.astype(np.float64) - expected.astype(np.float64)) + max_diff = diff.max() + tol = INT_TOL if np.issubdtype(expected.dtype, np.integer) else FLOAT_TOL + + if max_diff <= tol: + print(f" [OK] {expected_file} (maxdiff={max_diff:g})") + else: + _failures.append(f"{op_name}: maxdiff {max_diff:g} > {tol} ({expected_file})") + print(f" [FAILED] {expected_file}: maxdiff={max_diff:g} > tol={tol}") + + +def check_channels(op_name, out4d, file_pattern): + check(op_name, out4d[0], file_pattern.format(c=1)) + check(op_name, out4d[1], file_pattern.format(c=2)) + + +def main(): + print("=" * 60) + print("Hydra Python TIFF accuracy suite") + print("=" * 60) + + if hip.device_count() <= 0: + print("[SKIP] No CUDA devices found. Skipping accuracy tests.") + return True + + for f in ("test_c0.tif", "test_c1.tif"): + path = os.path.join(TEST_DATA_DIR, f) + if not os.path.isfile(path): + print(f"[SKIP] Missing test input {path}") + return True + if os.path.getsize(path) < 4096: + print(f"[SKIP] {f} looks like a Git LFS pointer; run `git lfs pull`") + return True + + HIP = hip.Hydra # raw API, mirrors the MATLAB command names + + im0 = load_tif("test_c0.tif") + im1 = load_tif("test_c1.tif") + im = np.ascontiguousarray(np.stack([im0, im1], axis=0)) # (chan, z, y, x) + print(f"Input: {im.shape} {im.dtype} (chan, z, y, x)\n") + + # Per-channel min-max normalization, like ImUtils.ConvertType(im, 'single', true) + im_norm = np.stack( + [((c - c.min()) / float(c.max() - c.min())).astype(np.float32) for c in im], axis=0 + ) + + # MATLAB: sigmas [19,19,9], radius [5,5,2] in (x,y,z) -> reversed here + sigmas = [9.0, 19.0, 19.0] + mask = hip.make_ellipsoid_mask([5, 5, 2]) # (x, y, z) radii, shape (11,11,5) + kernel = np.ascontiguousarray(mask.transpose(2, 1, 0)).astype(np.float32) # (z, y, x) + + print("Gaussian") + check_channels("Gaussian", np.asarray(HIP.Gaussian(im, sigmas)), "Gaussian_c{c}_19-19-9.tif") + + print("HighPassFilter") + check_channels("HighPassFilter", np.asarray(HIP.HighPassFilter(im, sigmas)), + "HighPassFilter_c{c}_19-19-9.tif") + + print("LoG") + out = np.asarray(HIP.LoG(im_norm, [2.0, 4.0, 4.0])).copy() + out[out > 0.001] = 0 + out = np.abs(out) + check_channels("LoG", out, "LoG_c{c}_4-4-2.tif") + + for op in ("Closure", "MaxFilter", "MeanFilter", "MedianFilter", + "MinFilter", "Opener", "StdFilter", "VarFilter"): + print(op) + check_channels(op, np.asarray(getattr(HIP, op)(im, kernel)), op + "_c{c}_5-5-2.tif") + + print("MultiplySum") + check_channels("MultiplySum", np.asarray(HIP.MultiplySum(im, kernel * 3.0)), + "MultiplySum_c{c}_15-15-6.tif") + + print("ElementWiseDifference") + im_rev = np.ascontiguousarray(im[::-1]) + check_channels("ElementWiseDifference", np.asarray(HIP.ElementWiseDifference(im, im_rev)), + "ElementWiseDifference_c{c}_reverse.tif") + + # EntropyFilter has no MATLAB reference test; its ground truth was saved + # min-max scaled to the uint16 range, and histogram-bin edges make a few + # pixels jump a bin across toolkit versions. Soft check: >=99.99% of + # pixels within 1 LSB after the same scaling. + print("EntropyFilter (soft)") + for ch, name in ((0, "EntropyFilter_c1_5-5-2.tif"), (1, "EntropyFilter_c2_5-5-2.tif")): + expected = load_tif(name).astype(np.float64) + out = np.asarray(HIP.EntropyFilter(im[ch], kernel)).astype(np.float64) + scaled = np.round((out - out.min()) / (out.max() - out.min()) * 65535.0) + frac_bad = float((np.abs(scaled - expected) > 1).mean()) + if frac_bad < 1e-4: + print(f" [OK] {name} (pixels off by >1 LSB: {frac_bad:.2e})") + else: + _failures.append(f"EntropyFilter: {frac_bad:.2e} of pixels off by >1 LSB ({name})") + print(f" [FAILED] {name}: {frac_bad:.2e} of pixels off by >1 LSB") + + print() + print("=" * 60) + if _failures: + print(f"FAILED: {len(_failures)} comparison(s)") + for f in _failures: + print(f" - {f}") + return False + print("Test Summary: PASSED") + return True + + +if __name__ == "__main__": + sys.exit(0 if main() else 1) From 6fee6c878ae5d31530ee148dee9da3945326d4cd Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 18:55:15 -0500 Subject: [PATCH 33/40] ci: install CUDA from conda-forge instead of the Jimver action The pinned Jimver/cuda-toolkit network install of 12.6.2 broke as the runner images moved (apt failure on ubuntu-latest). Pull cuda-nvcc + cudart from conda-forge in the existing miniforge env instead - the same channel the feedstock builds against, and one less third-party action. Linux additionally gets conda-forge c/cxx compilers as the nvcc host compiler. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88b834f..6492d04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,12 +40,6 @@ jobs: with: lfs: false # source build does not need the LFS test images - - name: Install CUDA toolkit (compiler only; runners have no GPU) - uses: Jimver/cuda-toolkit@v0.2.21 - with: - cuda: "12.6.2" - method: network - - name: Set up MSVC (Windows) if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 @@ -55,9 +49,17 @@ jobs: miniforge-version: latest python-version: ${{ matrix.python }} channels: conda-forge + conda-remove-defaults: "true" + # CUDA comes from conda-forge (compiler only; runners have no GPU) — + # the same channel the feedstock builds against, and one less + # third-party action that breaks when runner images move. - name: Install build dependencies - run: conda install -y -c conda-forge cmake ninja "scikit-build-core>=0.10" numpy pip + run: conda install -y -c conda-forge cmake ninja "scikit-build-core>=0.10" numpy pip cuda-nvcc cuda-cudart-dev cuda-cudart-static "cuda-version=12.9" + + - name: Install host compiler for nvcc (Linux) + if: runner.os == 'Linux' + run: conda install -y -c conda-forge c-compiler cxx-compiler - name: Build & install (mirrors the feedstock pip path) env: From f3e98133a30a60eb0b0b95ba879fffeabac07448 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 19:00:40 -0500 Subject: [PATCH 34/40] ci: build in the conda base env instead of a named env The setup-miniconda-created 'test' env resolves fine right after creation but later steps on windows-latest fail activation with EnvironmentNameNotFound (seen across two runs). Installing the toolchain, python included, into base sidesteps named-env activation entirely. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6492d04..a959e74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,15 +47,18 @@ jobs: - uses: conda-incubator/setup-miniconda@v3 with: miniforge-version: latest - python-version: ${{ matrix.python }} channels: conda-forge conda-remove-defaults: "true" + # Use base rather than a named env: activating the auto-created + # 'test' env intermittently fails on the Windows runners + # (EnvironmentNameNotFound), and base always exists. + activate-environment: "" # CUDA comes from conda-forge (compiler only; runners have no GPU) — # the same channel the feedstock builds against, and one less # third-party action that breaks when runner images move. - name: Install build dependencies - run: conda install -y -c conda-forge cmake ninja "scikit-build-core>=0.10" numpy pip cuda-nvcc cuda-cudart-dev cuda-cudart-static "cuda-version=12.9" + run: conda install -y -c conda-forge python=${{ matrix.python }} cmake ninja "scikit-build-core>=0.10" numpy pip cuda-nvcc cuda-cudart-dev cuda-cudart-static "cuda-version=12.9" - name: Install host compiler for nvcc (Linux) if: runner.os == 'Linux' From e587c0f5ba3f14bc3ff07b952dedc4744d9ee7e3 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 19:07:44 -0500 Subject: [PATCH 35/40] ci: force the Ninja generator so nvcc comes from conda-forge Without a generator override, CMake on windows-latest picks Visual Studio, whose CUDA support needs the VS-integration files that only a system CUDA toolkit install provides - the conda-forge cuda-nvcc package does not ship them, so configure fails on the runner. Verified locally: Ninja + conda-forge nvcc 12.9 + MSVC 19.44 builds the wheel end to end. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a959e74..1cf973f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,10 @@ jobs: # A single architecture keeps the gate fast; we only need it to # compile + import, not to run on a specific GPU. CMAKE_ARGS: "-DCMAKE_CUDA_ARCHITECTURES=75" + # Force Ninja: on Windows CMake would otherwise pick the Visual + # Studio generator, which needs the CUDA VS-integration files that + # the conda-forge nvcc package does not ship. + CMAKE_GENERATOR: Ninja run: python -m pip install . --no-build-isolation -v - name: Import smoke test (no GPU required to import) From 3b9d13acbd6142ecb1fd32df10fce08146d1a2df Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 19:11:57 -0500 Subject: [PATCH 36/40] ci: annotate build failures with the first error lines Job logs need authentication to read, but annotations are public; on failure the build step now emits the toolchain locations and the first error lines from the pip log as ::error:: annotations. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cf973f..877faf9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,15 @@ jobs: # Studio generator, which needs the CUDA VS-integration files that # the conda-forge nvcc package does not ship. CMAKE_GENERATOR: Ninja - run: python -m pip install . --no-build-isolation -v + # On failure, surface the toolchain and first error lines as + # annotations so the failure is diagnosable from the run summary. + run: | + set -o pipefail + if ! python -m pip install . --no-build-isolation -v 2>&1 | tee build.log; then + echo "::error::cl=$(command -v cl 2>/dev/null || echo MISSING) nvcc=$(command -v nvcc 2>/dev/null || echo MISSING) cmake=$(cmake --version | head -1)" + grep -iE "error|fatal" build.log | head -n 8 | while IFS= read -r line; do echo "::error::${line}"; done + exit 1 + fi - name: Import smoke test (no GPU required to import) run: | From 354a3381ebb027d689d405dd650b2da5c2b8aab1 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 19:16:07 -0500 Subject: [PATCH 37/40] ci: build against CUDA 13.x - nvcc 12.x rejects VS 2026 windows-latest now ships Visual Studio 2026 (MSVC 14.51) and CUDA 12.9 nvcc refuses it outright (C1189: unsupported Microsoft Visual Studio version). nvcc 13.x supports current MSVC. The 12.x pairing stays covered by the conda-forge feedstock CI, which pins VS 2022, and was verified locally against MSVC 19.44. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 877faf9..a8f32f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,8 +57,11 @@ jobs: # CUDA comes from conda-forge (compiler only; runners have no GPU) — # the same channel the feedstock builds against, and one less # third-party action that breaks when runner images move. + # CUDA 13.x: nvcc 12.x rejects the MSVC that windows-latest ships + # (VS 2026); the 12.x pairing is covered by the feedstock's own CI, + # which pins VS 2022. - name: Install build dependencies - run: conda install -y -c conda-forge python=${{ matrix.python }} cmake ninja "scikit-build-core>=0.10" numpy pip cuda-nvcc cuda-cudart-dev cuda-cudart-static "cuda-version=12.9" + run: conda install -y -c conda-forge python=${{ matrix.python }} cmake ninja "scikit-build-core>=0.10" numpy pip cuda-nvcc cuda-cudart-dev cuda-cudart-static "cuda-version=13.*" - name: Install host compiler for nvcc (Linux) if: runner.os == 'Linux' From b490a7052b395bed7f8d1a2907679a50ea75b47d Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 19:23:20 -0500 Subject: [PATCH 38/40] fix: include where fixed-width integer types are used The command framework headers relied on transitive includes, which newer libstdc++ (pulled in by the CUDA 13 / gcc toolchain on CI) no longer provides - uint8_t and friends came up undefined in ScriptioMaps.h. Include it explicitly in every header and TU that names the fixed-width types. Co-Authored-By: Claude Fable 5 --- 0.10 | 1407 +++++++++++++++++ src/c/ScriptCmds/Commands/ScrCmdDeviceCount.h | 2 + src/c/ScriptCmds/GenCommands.h | 1 + src/c/ScriptCmds/ScriptCommandImpl.h | 1 + src/c/ScriptCmds/ScriptioMaps.h | 2 + src/c/test_back/test.cpp | 1 + src/c/test_back/test_accuracy.cpp | 1 + 7 files changed, 1415 insertions(+) create mode 100644 0.10 diff --git a/0.10 b/0.10 new file mode 100644 index 0000000..b5943eb --- /dev/null +++ b/0.10 @@ -0,0 +1,1407 @@ +Channels: + - conda-forge +Platform: win-64 +Collecting package metadata (repodata.json): - \ | / - \ | / - \ | / done +Solving environment: \ | done + +## Package Plan ## + + environment location: D:\envs\hydra-ci-repro + + added / updated specs: + - cmake + - cuda-cudart-dev + - cuda-cudart-static + - cuda-nvcc + - cuda-version=12.9 + - ninja + - numpy + - pip + - python=3.12 + - scikit-build-core + + +The following packages will be downloaded: + + package | build + ---------------------------|----------------- + cmake-4.3.4 | hdcbee5b_0 15.3 MB conda-forge + cuda-cccl_win-64-12.9.27 | h57928b3_0 1.1 MB conda-forge + cuda-crt-dev_win-64-12.9.86| h57928b3_2 93 KB conda-forge + cuda-crt-tools-12.9.86 | h57928b3_2 29 KB conda-forge + cuda-cudart-dev-12.9.79 | he0c23c2_0 23 KB conda-forge + cuda-cudart-dev_win-64-12.9.79| he0c23c2_0 1.1 MB conda-forge + cuda-cudart-static-12.9.79 | he0c23c2_0 23 KB conda-forge + cuda-cudart-static_win-64-12.9.79| he0c23c2_0 346 KB conda-forge + cuda-nvcc-12.9.86 | h8f04d04_6 25 KB conda-forge + cuda-nvcc-dev_win-64-12.9.86| h36c15f3_2 22.4 MB conda-forge + cuda-nvcc-impl-12.9.86 | h53cbb54_2 27 KB conda-forge + cuda-nvcc-tools-12.9.86 | he0c23c2_2 27 KB conda-forge + cuda-nvcc_win-64-12.9.86 | hd70436c_6 27 KB conda-forge + cuda-nvvm-dev_win-64-12.9.86| h57928b3_2 27 KB conda-forge + cuda-nvvm-impl-12.9.86 | h2466b09_2 30 KB conda-forge + cuda-nvvm-tools-12.9.86 | h2466b09_2 38.4 MB conda-forge + krb5-1.22.2 | h719d79b_1 733 KB conda-forge + libcurl-8.21.0 | h51a1c48_2 395 KB conda-forge + libexpat-2.8.1 | hac47afa_1 70 KB conda-forge + libnvptxcompiler-dev-12.9.86| h57928b3_2 27 KB conda-forge + libnvptxcompiler-dev_win-64-12.9.86| h57928b3_2 30.3 MB conda-forge + libpsl-0.22.0 | hc1744f7_0 84 KB conda-forge + libsqlite-3.53.3 | hf5d6505_0 1.3 MB conda-forge + llvm-openmp-22.1.8 | h4fa8253_0 340 KB conda-forge + numpy-2.5.1 | py312ha3f287d_0 7.0 MB conda-forge + pip-26.1.2 | pyh8b19718_0 1.1 MB conda-forge + scikit-build-core-1.0.1 | pyh04d0eab_0 327 KB conda-forge + typing_extensions-4.16.0 | pyhcf101f3_0 51 KB conda-forge + vc-14.5 | h1b7c187_39 20 KB conda-forge + vc14_runtime-14.51.36231 | h1b9f54f_39 720 KB conda-forge + vcomp14-14.51.36231 | h1b9f54f_39 118 KB conda-forge + vs2019_win-64-19.29.30139 | h7dcff83_39 24 KB conda-forge + vswhere-3.1.7 | h40126e0_1 233 KB conda-forge + ------------------------------------------------------------ + Total: 121.8 MB + +The following NEW packages will be INSTALLED: + + bzip2 conda-forge/win-64::bzip2-1.0.8-h0ad9c76_9 + ca-certificates conda-forge/noarch::ca-certificates-2026.6.17-h4c7d964_0 + cmake conda-forge/win-64::cmake-4.3.4-hdcbee5b_0 + cuda-cccl_win-64 conda-forge/noarch::cuda-cccl_win-64-12.9.27-h57928b3_0 + cuda-crt-dev_win-~ conda-forge/noarch::cuda-crt-dev_win-64-12.9.86-h57928b3_2 + cuda-crt-tools conda-forge/win-64::cuda-crt-tools-12.9.86-h57928b3_2 + cuda-cudart conda-forge/win-64::cuda-cudart-12.9.79-he0c23c2_0 + cuda-cudart-dev conda-forge/win-64::cuda-cudart-dev-12.9.79-he0c23c2_0 + cuda-cudart-dev_w~ conda-forge/noarch::cuda-cudart-dev_win-64-12.9.79-he0c23c2_0 + cuda-cudart-static conda-forge/win-64::cuda-cudart-static-12.9.79-he0c23c2_0 + cuda-cudart-stati~ conda-forge/noarch::cuda-cudart-static_win-64-12.9.79-he0c23c2_0 + cuda-cudart_win-64 conda-forge/noarch::cuda-cudart_win-64-12.9.79-he0c23c2_0 + cuda-nvcc conda-forge/win-64::cuda-nvcc-12.9.86-h8f04d04_6 + cuda-nvcc-dev_win~ conda-forge/noarch::cuda-nvcc-dev_win-64-12.9.86-h36c15f3_2 + cuda-nvcc-impl conda-forge/win-64::cuda-nvcc-impl-12.9.86-h53cbb54_2 + cuda-nvcc-tools conda-forge/win-64::cuda-nvcc-tools-12.9.86-he0c23c2_2 + cuda-nvcc_win-64 conda-forge/win-64::cuda-nvcc_win-64-12.9.86-hd70436c_6 + cuda-nvvm-dev_win~ conda-forge/noarch::cuda-nvvm-dev_win-64-12.9.86-h57928b3_2 + cuda-nvvm-impl conda-forge/win-64::cuda-nvvm-impl-12.9.86-h2466b09_2 + cuda-nvvm-tools conda-forge/win-64::cuda-nvvm-tools-12.9.86-h2466b09_2 + cuda-version conda-forge/noarch::cuda-version-12.9-h4f385c5_3 + exceptiongroup conda-forge/noarch::exceptiongroup-1.3.1-pyhd8ed1ab_0 + icu conda-forge/win-64::icu-78.3-h637d24d_0 + importlib-resourc~ conda-forge/noarch::importlib-resources-7.1.0-pyhd8ed1ab_0 + importlib_resourc~ conda-forge/noarch::importlib_resources-7.1.0-pyhd8ed1ab_0 + krb5 conda-forge/win-64::krb5-1.22.2-h719d79b_1 + libblas conda-forge/win-64::libblas-3.11.0-8_h8455456_mkl + libcblas conda-forge/win-64::libcblas-3.11.0-8_h2a3cdd5_mkl + libcurl conda-forge/win-64::libcurl-8.21.0-h51a1c48_2 + libexpat conda-forge/win-64::libexpat-2.8.1-hac47afa_1 + libffi conda-forge/win-64::libffi-3.5.2-h3d046cb_0 + libhwloc conda-forge/win-64::libhwloc-2.13.0-default_h049141e_1000 + libiconv conda-forge/win-64::libiconv-1.18-hc1393d2_2 + liblapack conda-forge/win-64::liblapack-3.11.0-8_hf9ab0e9_mkl + liblzma conda-forge/win-64::liblzma-5.8.3-hfd05255_0 + libnvptxcompiler-~ conda-forge/win-64::libnvptxcompiler-dev-12.9.86-h57928b3_2 + libnvptxcompiler-~ conda-forge/noarch::libnvptxcompiler-dev_win-64-12.9.86-h57928b3_2 + libpsl conda-forge/win-64::libpsl-0.22.0-hc1744f7_0 + libsqlite conda-forge/win-64::libsqlite-3.53.3-hf5d6505_0 + libssh2 conda-forge/win-64::libssh2-1.11.1-h9aa295b_0 + libuv conda-forge/win-64::libuv-1.52.1-h6a83c73_0 + libwinpthread conda-forge/win-64::libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10 + libxml2 conda-forge/win-64::libxml2-2.15.3-h8ef44ab_0 + libxml2-16 conda-forge/win-64::libxml2-16-2.15.3-h3cfd58e_0 + libzlib conda-forge/win-64::libzlib-1.3.2-hfd05255_2 + llvm-openmp conda-forge/win-64::llvm-openmp-22.1.8-h4fa8253_0 + mkl conda-forge/win-64::mkl-2026.0.0-hac47afa_908 + ninja conda-forge/win-64::ninja-1.13.2-h477610d_0 + numpy conda-forge/win-64::numpy-2.5.1-py312ha3f287d_0 + onemkl-license conda-forge/win-64::onemkl-license-2026.0.0-h57928b3_908 + openssl conda-forge/win-64::openssl-3.6.3-hf411b9b_0 + packaging conda-forge/noarch::packaging-26.2-pyhc364b38_0 + pathspec conda-forge/noarch::pathspec-1.1.1-pyhd8ed1ab_0 + pip conda-forge/noarch::pip-26.1.2-pyh8b19718_0 + python conda-forge/win-64::python-3.12.13-h0159041_0_cpython + python_abi conda-forge/noarch::python_abi-3.12-8_cp312 + scikit-build-core conda-forge/noarch::scikit-build-core-1.0.1-pyh04d0eab_0 + setuptools conda-forge/noarch::setuptools-82.0.1-pyh332efcf_0 + tbb conda-forge/win-64::tbb-2023.0.0-hd3d4ead_2 + tk conda-forge/win-64::tk-8.6.13-h6ed50ae_3 + tomli conda-forge/noarch::tomli-2.4.1-pyhcf101f3_0 + typing_extensions conda-forge/noarch::typing_extensions-4.16.0-pyhcf101f3_0 + tzdata conda-forge/noarch::tzdata-2025c-hc9c84f9_1 + ucrt conda-forge/win-64::ucrt-10.0.26100.0-h57928b3_0 + vc conda-forge/win-64::vc-14.5-h1b7c187_39 + vc14_runtime conda-forge/win-64::vc14_runtime-14.51.36231-h1b9f54f_39 + vcomp14 conda-forge/win-64::vcomp14-14.51.36231-h1b9f54f_39 + vs2019_win-64 conda-forge/win-64::vs2019_win-64-19.29.30139-h7dcff83_39 + vswhere conda-forge/noarch::vswhere-3.1.7-h40126e0_1 + wheel conda-forge/noarch::wheel-0.47.0-pyhd8ed1ab_0 + zipp conda-forge/noarch::zipp-4.1.0-pyhcf101f3_0 + zstd conda-forge/win-64::zstd-1.5.7-h534d264_6 + + + +Downloading and Extracting Packages: ...working... cuda-nvvm-tools-12.9 | 38.4 MB | | 0% + libnvptxcompiler-dev | 30.3 MB | | 0%  + + cuda-nvcc-dev_win-64 | 22.4 MB | | 0%  + + + cmake-4.3.4 | 15.3 MB | | 0%  + + + + numpy-2.5.1 | 7.0 MB | | 0%  + + + + + libsqlite-3.53.3 | 1.3 MB | | 0%  + + + + + + pip-26.1.2 | 1.1 MB | | 0%  + + + + + + + cuda-cudart-dev_win- | 1.1 MB | | 0%  + + + + + + + + cuda-cccl_win-64-12. | 1.1 MB | | 0%  + + + + + + + + + krb5-1.22.2 | 733 KB | | 0%  + + + + + + + + + + vc14_runtime-14.51.3 | 720 KB | | 0%  + + + + + + + + + + + libcurl-8.21.0 | 395 KB | | 0%  + + + + + + + + + + + + cuda-cudart-static_w | 346 KB | | 0%  + + + + + + + + + + + + + llvm-openmp-22.1.8 | 340 KB | | 0%  + + + + + + + + + + + + + + scikit-build-core-1. | 327 KB | | 0%  + + + + + + + + + + + + + + + vswhere-3.1.7 | 233 KB | | 0%  + + + + + + + + + + + + + + + + vcomp14-14.51.36231 | 118 KB | | 0%  + + + + + + + + + + + + + + + + + cuda-crt-dev_win-64- | 93 KB | | 0%  + + + + + + + + + + + + + + + + + + libpsl-0.22.0 | 84 KB | | 0%  + + + + + + + + + + + + + + + + + + + ... (more hidden) ... cuda-nvvm-tools-12.9 | 38.4 MB | 3 | 3% + libnvptxcompiler-dev | 30.3 MB | 6 | 7%  + + + + numpy-2.5.1 | 7.0 MB | #7 | 18%  + + cuda-nvcc-dev_win-64 | 22.4 MB | 1 | 2%  + + + cmake-4.3.4 | 15.3 MB | 5 | 5%  cuda-nvvm-tools-12.9 | 38.4 MB | 9 | 9% + + + + numpy-2.5.1 | 7.0 MB | #### | 40%  + libnvptxcompiler-dev | 30.3 MB | #8 | 18%  + + + cmake-4.3.4 | 15.3 MB | #5 | 16%  cuda-nvvm-tools-12.9 | 38.4 MB | ##2 | 22% + libnvptxcompiler-dev | 30.3 MB | ####2 | 42%  + + + cmake-4.3.4 | 15.3 MB | ###6 | 37%  + + + + numpy-2.5.1 | 7.0 MB | ########## | 100%  + + + + numpy-2.5.1 | 7.0 MB | ########## | 100%  + + + + + libsqlite-3.53.3 | 1.3 MB | 1 | 1%  cuda-nvvm-tools-12.9 | 38.4 MB | ###4 | 34% + + cuda-nvcc-dev_win-64 | 22.4 MB | 4 | 5%  + + + + + libsqlite-3.53.3 | 1.3 MB | ########## | 100%  + libnvptxcompiler-dev | 30.3 MB | ######1 | 62%  + + + cmake-4.3.4 | 15.3 MB | ####### | 70%  + + + + + + pip-26.1.2 | 1.1 MB | 1 | 1%  cuda-nvvm-tools-12.9 | 38.4 MB | ####4 | 44% + + + + + libsqlite-3.53.3 | 1.3 MB | ########## | 100%  + + + + + libsqlite-3.53.3 | 1.3 MB | ########## | 100%  + + + + + + pip-26.1.2 | 1.1 MB | ########## | 100%  + + cuda-nvcc-dev_win-64 | 22.4 MB | ##4 | 24%  + libnvptxcompiler-dev | 30.3 MB | #######9 | 79%  + + + + + + + cuda-cudart-dev_win- | 1.1 MB | 1 | 1%  + + + + + + + cuda-cudart-dev_win- | 1.1 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | #####4 | 54% + + + + + + + + cuda-cccl_win-64-12. | 1.1 MB | 1 | 1%  + + cuda-nvcc-dev_win-64 | 22.4 MB | #####3 | 53%  + libnvptxcompiler-dev | 30.3 MB | #########6 | 97%  + + + + + + + + cuda-cccl_win-64-12. | 1.1 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | ######4 | 65% + + + + + + + + + krb5-1.22.2 | 733 KB | 2 | 2%  + + + cmake-4.3.4 | 15.3 MB | ########## | 100%  + + + cmake-4.3.4 | 15.3 MB | ########## | 100%  + + cuda-nvcc-dev_win-64 | 22.4 MB | ######## | 81%  + + + + + + + + + krb5-1.22.2 | 733 KB | ########## | 100%  + + + + + + + + + + vc14_runtime-14.51.3 | 720 KB | 2 | 2%  + + + + + + + + + + vc14_runtime-14.51.3 | 720 KB | ########## | 100%  + + + + + + + + + + + libcurl-8.21.0 | 395 KB | 4 | 4%  + + + + + + + + + + + libcurl-8.21.0 | 395 KB | ########## | 100%  + + + + + + + + + + + + cuda-cudart-static_w | 346 KB | 4 | 5%  cuda-nvvm-tools-12.9 | 38.4 MB | #######7 | 77% + + + + + + + + + + + + cuda-cudart-static_w | 346 KB | ########## | 100%  + + + + + + + + + + + + + llvm-openmp-22.1.8 | 340 KB | 4 | 5%  + + + + + + + + + + + + + + scikit-build-core-1. | 327 KB | 4 | 5%  + + + + + + + + + + + + + llvm-openmp-22.1.8 | 340 KB | ########## | 100%  + + + + + + + + + + + + + + scikit-build-core-1. | 327 KB | ########## | 100%  + + + + + + + + + + + + + + + vswhere-3.1.7 | 233 KB | 6 | 7%  + + + + + + + + + + + + + + + + vcomp14-14.51.36231 | 118 KB | #3 | 14%  + + + + + + + + + + + + + + + vswhere-3.1.7 | 233 KB | ########## | 100%  + + + + + + + cuda-cudart-dev_win- | 1.1 MB | ########## | 100%  + + + + + + + + + + + + + + + + vcomp14-14.51.36231 | 118 KB | ########## | 100%  + + + + + + + cuda-cudart-dev_win- | 1.1 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | #########3 | 93% + + + + + + + + + + + + + + + + + cuda-crt-dev_win-64- | 93 KB | #7 | 17%  + + + + + + + + + + + + + + + + + + libpsl-0.22.0 | 84 KB | #9 | 19%  + + + + + + + + + + + + + + + + + cuda-crt-dev_win-64- | 93 KB | ########## | 100%  + + + + + + + + + + + + + + + + + + libpsl-0.22.0 | 84 KB | ########## | 100%  + + + + + + + + + + + + + + + + + + + ... (more hidden) ... + + + + + + + + + + + + + + + + + + + ... (more hidden) ... + libnvptxcompiler-dev | 30.3 MB | ########## | 100%  + + cuda-nvcc-dev_win-64 | 22.4 MB | ########## | 100%  + + + + + + pip-26.1.2 | 1.1 MB | ########## | 100%  + + + + + + pip-26.1.2 | 1.1 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | ########## | 100% + + + + numpy-2.5.1 | 7.0 MB | ########## | 100%  + + + + + + + + + krb5-1.22.2 | 733 KB | ########## | 100%  + + + + + + + + + krb5-1.22.2 | 733 KB | ########## | 100%  + + + + + + + + + + vc14_runtime-14.51.3 | 720 KB | ########## | 100%  + + + + + + + + + + vc14_runtime-14.51.3 | 720 KB | ########## | 100%  + + + + + + + + + + + libcurl-8.21.0 | 395 KB | ########## | 100%  + + + + + + + + + + + libcurl-8.21.0 | 395 KB | ########## | 100%  + + + + + + + + + + + + cuda-cudart-static_w | 346 KB | ########## | 100%  + + + + + + + + + + + + cuda-cudart-static_w | 346 KB | ########## | 100%  + + + + + + + + + + + + + llvm-openmp-22.1.8 | 340 KB | ########## | 100%  + + + + + + + + + + + + + llvm-openmp-22.1.8 | 340 KB | ########## | 100%  + + + + + + + + cuda-cccl_win-64-12. | 1.1 MB | ########## | 100%  + + + + + + + + cuda-cccl_win-64-12. | 1.1 MB | ########## | 100%  + + + + + + + + + + + + + + + vswhere-3.1.7 | 233 KB | ########## | 100%  + + + + + + + + + + + + + + + vswhere-3.1.7 | 233 KB | ########## | 100%  + + + + + + + + + + + + + + + + vcomp14-14.51.36231 | 118 KB | ########## | 100%  + + + + + + + + + + + + + + + + vcomp14-14.51.36231 | 118 KB | ########## | 100%  + + + + + + + + + + + + + + + + + cuda-crt-dev_win-64- | 93 KB | ########## | 100%  + + + + + + + + + + + + + + + + + cuda-crt-dev_win-64- | 93 KB | ########## | 100%  + + + + + + + + + + + + + + + + + + libpsl-0.22.0 | 84 KB | ########## | 100%  + + + + + + + + + + + + + + + + + + libpsl-0.22.0 | 84 KB | ########## | 100%  + + + + + + + + + + + + + + + + + + + ... (more hidden) ... + + + + + + + + + + + + + + + + + + + ... (more hidden) ... + + + + + + + + + + + + + + scikit-build-core-1. | 327 KB | ########## | 100%  + + + + + + + + + + + + + + scikit-build-core-1. | 327 KB | ########## | 100%  + + cuda-nvcc-dev_win-64 | 22.4 MB | ########## | 100%  + libnvptxcompiler-dev | 30.3 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | ########## | 100% + + + cmake-4.3.4 | 15.3 MB | ########## | 100%  + + + + + + + + + + + + + + + + + + +  +  + +  + + +  + + + +  + + + + +  + + + + + +  + + + + + + +  + + + + + + + +  + + + + + + + + +  + + + + + + + + + +  + + + + + + + + + + +  + + + + + + + + + + + +  + + + + + + + + + + + + +  + + + + + + + + + + + + + +  + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + + + + +  +  + +  + + +  + + + +  + + + + +  + + + + + +  + + + + + + +  + + + + + + + +  + + + + + + + + +  + + + + + + + + + +  + + + + + + + + + + +  + + + + + + + + + + + +  + + + + + + + + + + + + +  done +Preparing transaction: - \ | / done +Verifying transaction: \ | / - \ | / - \ | / - \ | / done +Executing transaction: \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - done +# +# To activate this environment, use +# +# $ conda activate D:\envs\hydra-ci-repro +# +# To deactivate an active environment, use +# +# $ conda deactivate + diff --git a/src/c/ScriptCmds/Commands/ScrCmdDeviceCount.h b/src/c/ScriptCmds/Commands/ScrCmdDeviceCount.h index 8800441..3266b79 100644 --- a/src/c/ScriptCmds/Commands/ScrCmdDeviceCount.h +++ b/src/c/ScriptCmds/Commands/ScrCmdDeviceCount.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "ScriptCommandImpl.h" #include "ScriptCommandDefines.h" diff --git a/src/c/ScriptCmds/GenCommands.h b/src/c/ScriptCmds/GenCommands.h index 72f686c..c62e2af 100644 --- a/src/c/ScriptCmds/GenCommands.h +++ b/src/c/ScriptCmds/GenCommands.h @@ -3,6 +3,7 @@ #include "mph/preproc_helper.h" #include +#include // Script parameters #define SCR_PARAMS(...) __VA_ARGS__ diff --git a/src/c/ScriptCmds/ScriptCommandImpl.h b/src/c/ScriptCmds/ScriptCommandImpl.h index 25103a5..331f4f7 100644 --- a/src/c/ScriptCmds/ScriptCommandImpl.h +++ b/src/c/ScriptCmds/ScriptCommandImpl.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "ScriptCommand.h" diff --git a/src/c/ScriptCmds/ScriptioMaps.h b/src/c/ScriptCmds/ScriptioMaps.h index 073b376..dcb66dc 100644 --- a/src/c/ScriptCmds/ScriptioMaps.h +++ b/src/c/ScriptCmds/ScriptioMaps.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "ScriptCommandDefines.h" #define GENERATE_DEFAULT_IO_MAPPERS diff --git a/src/c/test_back/test.cpp b/src/c/test_back/test.cpp index 2071a71..beea573 100644 --- a/src/c/test_back/test.cpp +++ b/src/c/test_back/test.cpp @@ -1,6 +1,7 @@ #include "CWrappers.h" #include "ImageView.h" +#include #include #include diff --git a/src/c/test_back/test_accuracy.cpp b/src/c/test_back/test_accuracy.cpp index 7a12d94..d44b5af 100644 --- a/src/c/test_back/test_accuracy.cpp +++ b/src/c/test_back/test_accuracy.cpp @@ -2,6 +2,7 @@ #include "ImageView.h" #include "Vec.h" +#include #include #include #include From 0ceae514e2f88639b885ca1685ca820a2cfd0d94 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 19:23:36 -0500 Subject: [PATCH 39/40] chore: remove stray file created by a shell redirect Co-Authored-By: Claude Fable 5 --- 0.10 | 1407 ---------------------------------------------------------- 1 file changed, 1407 deletions(-) delete mode 100644 0.10 diff --git a/0.10 b/0.10 deleted file mode 100644 index b5943eb..0000000 --- a/0.10 +++ /dev/null @@ -1,1407 +0,0 @@ -Channels: - - conda-forge -Platform: win-64 -Collecting package metadata (repodata.json): - \ | / - \ | / - \ | / done -Solving environment: \ | done - -## Package Plan ## - - environment location: D:\envs\hydra-ci-repro - - added / updated specs: - - cmake - - cuda-cudart-dev - - cuda-cudart-static - - cuda-nvcc - - cuda-version=12.9 - - ninja - - numpy - - pip - - python=3.12 - - scikit-build-core - - -The following packages will be downloaded: - - package | build - ---------------------------|----------------- - cmake-4.3.4 | hdcbee5b_0 15.3 MB conda-forge - cuda-cccl_win-64-12.9.27 | h57928b3_0 1.1 MB conda-forge - cuda-crt-dev_win-64-12.9.86| h57928b3_2 93 KB conda-forge - cuda-crt-tools-12.9.86 | h57928b3_2 29 KB conda-forge - cuda-cudart-dev-12.9.79 | he0c23c2_0 23 KB conda-forge - cuda-cudart-dev_win-64-12.9.79| he0c23c2_0 1.1 MB conda-forge - cuda-cudart-static-12.9.79 | he0c23c2_0 23 KB conda-forge - cuda-cudart-static_win-64-12.9.79| he0c23c2_0 346 KB conda-forge - cuda-nvcc-12.9.86 | h8f04d04_6 25 KB conda-forge - cuda-nvcc-dev_win-64-12.9.86| h36c15f3_2 22.4 MB conda-forge - cuda-nvcc-impl-12.9.86 | h53cbb54_2 27 KB conda-forge - cuda-nvcc-tools-12.9.86 | he0c23c2_2 27 KB conda-forge - cuda-nvcc_win-64-12.9.86 | hd70436c_6 27 KB conda-forge - cuda-nvvm-dev_win-64-12.9.86| h57928b3_2 27 KB conda-forge - cuda-nvvm-impl-12.9.86 | h2466b09_2 30 KB conda-forge - cuda-nvvm-tools-12.9.86 | h2466b09_2 38.4 MB conda-forge - krb5-1.22.2 | h719d79b_1 733 KB conda-forge - libcurl-8.21.0 | h51a1c48_2 395 KB conda-forge - libexpat-2.8.1 | hac47afa_1 70 KB conda-forge - libnvptxcompiler-dev-12.9.86| h57928b3_2 27 KB conda-forge - libnvptxcompiler-dev_win-64-12.9.86| h57928b3_2 30.3 MB conda-forge - libpsl-0.22.0 | hc1744f7_0 84 KB conda-forge - libsqlite-3.53.3 | hf5d6505_0 1.3 MB conda-forge - llvm-openmp-22.1.8 | h4fa8253_0 340 KB conda-forge - numpy-2.5.1 | py312ha3f287d_0 7.0 MB conda-forge - pip-26.1.2 | pyh8b19718_0 1.1 MB conda-forge - scikit-build-core-1.0.1 | pyh04d0eab_0 327 KB conda-forge - typing_extensions-4.16.0 | pyhcf101f3_0 51 KB conda-forge - vc-14.5 | h1b7c187_39 20 KB conda-forge - vc14_runtime-14.51.36231 | h1b9f54f_39 720 KB conda-forge - vcomp14-14.51.36231 | h1b9f54f_39 118 KB conda-forge - vs2019_win-64-19.29.30139 | h7dcff83_39 24 KB conda-forge - vswhere-3.1.7 | h40126e0_1 233 KB conda-forge - ------------------------------------------------------------ - Total: 121.8 MB - -The following NEW packages will be INSTALLED: - - bzip2 conda-forge/win-64::bzip2-1.0.8-h0ad9c76_9 - ca-certificates conda-forge/noarch::ca-certificates-2026.6.17-h4c7d964_0 - cmake conda-forge/win-64::cmake-4.3.4-hdcbee5b_0 - cuda-cccl_win-64 conda-forge/noarch::cuda-cccl_win-64-12.9.27-h57928b3_0 - cuda-crt-dev_win-~ conda-forge/noarch::cuda-crt-dev_win-64-12.9.86-h57928b3_2 - cuda-crt-tools conda-forge/win-64::cuda-crt-tools-12.9.86-h57928b3_2 - cuda-cudart conda-forge/win-64::cuda-cudart-12.9.79-he0c23c2_0 - cuda-cudart-dev conda-forge/win-64::cuda-cudart-dev-12.9.79-he0c23c2_0 - cuda-cudart-dev_w~ conda-forge/noarch::cuda-cudart-dev_win-64-12.9.79-he0c23c2_0 - cuda-cudart-static conda-forge/win-64::cuda-cudart-static-12.9.79-he0c23c2_0 - cuda-cudart-stati~ conda-forge/noarch::cuda-cudart-static_win-64-12.9.79-he0c23c2_0 - cuda-cudart_win-64 conda-forge/noarch::cuda-cudart_win-64-12.9.79-he0c23c2_0 - cuda-nvcc conda-forge/win-64::cuda-nvcc-12.9.86-h8f04d04_6 - cuda-nvcc-dev_win~ conda-forge/noarch::cuda-nvcc-dev_win-64-12.9.86-h36c15f3_2 - cuda-nvcc-impl conda-forge/win-64::cuda-nvcc-impl-12.9.86-h53cbb54_2 - cuda-nvcc-tools conda-forge/win-64::cuda-nvcc-tools-12.9.86-he0c23c2_2 - cuda-nvcc_win-64 conda-forge/win-64::cuda-nvcc_win-64-12.9.86-hd70436c_6 - cuda-nvvm-dev_win~ conda-forge/noarch::cuda-nvvm-dev_win-64-12.9.86-h57928b3_2 - cuda-nvvm-impl conda-forge/win-64::cuda-nvvm-impl-12.9.86-h2466b09_2 - cuda-nvvm-tools conda-forge/win-64::cuda-nvvm-tools-12.9.86-h2466b09_2 - cuda-version conda-forge/noarch::cuda-version-12.9-h4f385c5_3 - exceptiongroup conda-forge/noarch::exceptiongroup-1.3.1-pyhd8ed1ab_0 - icu conda-forge/win-64::icu-78.3-h637d24d_0 - importlib-resourc~ conda-forge/noarch::importlib-resources-7.1.0-pyhd8ed1ab_0 - importlib_resourc~ conda-forge/noarch::importlib_resources-7.1.0-pyhd8ed1ab_0 - krb5 conda-forge/win-64::krb5-1.22.2-h719d79b_1 - libblas conda-forge/win-64::libblas-3.11.0-8_h8455456_mkl - libcblas conda-forge/win-64::libcblas-3.11.0-8_h2a3cdd5_mkl - libcurl conda-forge/win-64::libcurl-8.21.0-h51a1c48_2 - libexpat conda-forge/win-64::libexpat-2.8.1-hac47afa_1 - libffi conda-forge/win-64::libffi-3.5.2-h3d046cb_0 - libhwloc conda-forge/win-64::libhwloc-2.13.0-default_h049141e_1000 - libiconv conda-forge/win-64::libiconv-1.18-hc1393d2_2 - liblapack conda-forge/win-64::liblapack-3.11.0-8_hf9ab0e9_mkl - liblzma conda-forge/win-64::liblzma-5.8.3-hfd05255_0 - libnvptxcompiler-~ conda-forge/win-64::libnvptxcompiler-dev-12.9.86-h57928b3_2 - libnvptxcompiler-~ conda-forge/noarch::libnvptxcompiler-dev_win-64-12.9.86-h57928b3_2 - libpsl conda-forge/win-64::libpsl-0.22.0-hc1744f7_0 - libsqlite conda-forge/win-64::libsqlite-3.53.3-hf5d6505_0 - libssh2 conda-forge/win-64::libssh2-1.11.1-h9aa295b_0 - libuv conda-forge/win-64::libuv-1.52.1-h6a83c73_0 - libwinpthread conda-forge/win-64::libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10 - libxml2 conda-forge/win-64::libxml2-2.15.3-h8ef44ab_0 - libxml2-16 conda-forge/win-64::libxml2-16-2.15.3-h3cfd58e_0 - libzlib conda-forge/win-64::libzlib-1.3.2-hfd05255_2 - llvm-openmp conda-forge/win-64::llvm-openmp-22.1.8-h4fa8253_0 - mkl conda-forge/win-64::mkl-2026.0.0-hac47afa_908 - ninja conda-forge/win-64::ninja-1.13.2-h477610d_0 - numpy conda-forge/win-64::numpy-2.5.1-py312ha3f287d_0 - onemkl-license conda-forge/win-64::onemkl-license-2026.0.0-h57928b3_908 - openssl conda-forge/win-64::openssl-3.6.3-hf411b9b_0 - packaging conda-forge/noarch::packaging-26.2-pyhc364b38_0 - pathspec conda-forge/noarch::pathspec-1.1.1-pyhd8ed1ab_0 - pip conda-forge/noarch::pip-26.1.2-pyh8b19718_0 - python conda-forge/win-64::python-3.12.13-h0159041_0_cpython - python_abi conda-forge/noarch::python_abi-3.12-8_cp312 - scikit-build-core conda-forge/noarch::scikit-build-core-1.0.1-pyh04d0eab_0 - setuptools conda-forge/noarch::setuptools-82.0.1-pyh332efcf_0 - tbb conda-forge/win-64::tbb-2023.0.0-hd3d4ead_2 - tk conda-forge/win-64::tk-8.6.13-h6ed50ae_3 - tomli conda-forge/noarch::tomli-2.4.1-pyhcf101f3_0 - typing_extensions conda-forge/noarch::typing_extensions-4.16.0-pyhcf101f3_0 - tzdata conda-forge/noarch::tzdata-2025c-hc9c84f9_1 - ucrt conda-forge/win-64::ucrt-10.0.26100.0-h57928b3_0 - vc conda-forge/win-64::vc-14.5-h1b7c187_39 - vc14_runtime conda-forge/win-64::vc14_runtime-14.51.36231-h1b9f54f_39 - vcomp14 conda-forge/win-64::vcomp14-14.51.36231-h1b9f54f_39 - vs2019_win-64 conda-forge/win-64::vs2019_win-64-19.29.30139-h7dcff83_39 - vswhere conda-forge/noarch::vswhere-3.1.7-h40126e0_1 - wheel conda-forge/noarch::wheel-0.47.0-pyhd8ed1ab_0 - zipp conda-forge/noarch::zipp-4.1.0-pyhcf101f3_0 - zstd conda-forge/win-64::zstd-1.5.7-h534d264_6 - - - -Downloading and Extracting Packages: ...working... cuda-nvvm-tools-12.9 | 38.4 MB | | 0% - libnvptxcompiler-dev | 30.3 MB | | 0%  - - cuda-nvcc-dev_win-64 | 22.4 MB | | 0%  - - - cmake-4.3.4 | 15.3 MB | | 0%  - - - - numpy-2.5.1 | 7.0 MB | | 0%  - - - - - libsqlite-3.53.3 | 1.3 MB | | 0%  - - - - - - pip-26.1.2 | 1.1 MB | | 0%  - - - - - - - cuda-cudart-dev_win- | 1.1 MB | | 0%  - - - - - - - - cuda-cccl_win-64-12. | 1.1 MB | | 0%  - - - - - - - - - krb5-1.22.2 | 733 KB | | 0%  - - - - - - - - - - vc14_runtime-14.51.3 | 720 KB | | 0%  - - - - - - - - - - - libcurl-8.21.0 | 395 KB | | 0%  - - - - - - - - - - - - cuda-cudart-static_w | 346 KB | | 0%  - - - - - - - - - - - - - llvm-openmp-22.1.8 | 340 KB | | 0%  - - - - - - - - - - - - - - scikit-build-core-1. | 327 KB | | 0%  - - - - - - - - - - - - - - - vswhere-3.1.7 | 233 KB | | 0%  - - - - - - - - - - - - - - - - vcomp14-14.51.36231 | 118 KB | | 0%  - - - - - - - - - - - - - - - - - cuda-crt-dev_win-64- | 93 KB | | 0%  - - - - - - - - - - - - - - - - - - libpsl-0.22.0 | 84 KB | | 0%  - - - - - - - - - - - - - - - - - - - ... (more hidden) ... cuda-nvvm-tools-12.9 | 38.4 MB | 3 | 3% - libnvptxcompiler-dev | 30.3 MB | 6 | 7%  - - - - numpy-2.5.1 | 7.0 MB | #7 | 18%  - - cuda-nvcc-dev_win-64 | 22.4 MB | 1 | 2%  - - - cmake-4.3.4 | 15.3 MB | 5 | 5%  cuda-nvvm-tools-12.9 | 38.4 MB | 9 | 9% - - - - numpy-2.5.1 | 7.0 MB | #### | 40%  - libnvptxcompiler-dev | 30.3 MB | #8 | 18%  - - - cmake-4.3.4 | 15.3 MB | #5 | 16%  cuda-nvvm-tools-12.9 | 38.4 MB | ##2 | 22% - libnvptxcompiler-dev | 30.3 MB | ####2 | 42%  - - - cmake-4.3.4 | 15.3 MB | ###6 | 37%  - - - - numpy-2.5.1 | 7.0 MB | ########## | 100%  - - - - numpy-2.5.1 | 7.0 MB | ########## | 100%  - - - - - libsqlite-3.53.3 | 1.3 MB | 1 | 1%  cuda-nvvm-tools-12.9 | 38.4 MB | ###4 | 34% - - cuda-nvcc-dev_win-64 | 22.4 MB | 4 | 5%  - - - - - libsqlite-3.53.3 | 1.3 MB | ########## | 100%  - libnvptxcompiler-dev | 30.3 MB | ######1 | 62%  - - - cmake-4.3.4 | 15.3 MB | ####### | 70%  - - - - - - pip-26.1.2 | 1.1 MB | 1 | 1%  cuda-nvvm-tools-12.9 | 38.4 MB | ####4 | 44% - - - - - libsqlite-3.53.3 | 1.3 MB | ########## | 100%  - - - - - libsqlite-3.53.3 | 1.3 MB | ########## | 100%  - - - - - - pip-26.1.2 | 1.1 MB | ########## | 100%  - - cuda-nvcc-dev_win-64 | 22.4 MB | ##4 | 24%  - libnvptxcompiler-dev | 30.3 MB | #######9 | 79%  - - - - - - - cuda-cudart-dev_win- | 1.1 MB | 1 | 1%  - - - - - - - cuda-cudart-dev_win- | 1.1 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | #####4 | 54% - - - - - - - - cuda-cccl_win-64-12. | 1.1 MB | 1 | 1%  - - cuda-nvcc-dev_win-64 | 22.4 MB | #####3 | 53%  - libnvptxcompiler-dev | 30.3 MB | #########6 | 97%  - - - - - - - - cuda-cccl_win-64-12. | 1.1 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | ######4 | 65% - - - - - - - - - krb5-1.22.2 | 733 KB | 2 | 2%  - - - cmake-4.3.4 | 15.3 MB | ########## | 100%  - - - cmake-4.3.4 | 15.3 MB | ########## | 100%  - - cuda-nvcc-dev_win-64 | 22.4 MB | ######## | 81%  - - - - - - - - - krb5-1.22.2 | 733 KB | ########## | 100%  - - - - - - - - - - vc14_runtime-14.51.3 | 720 KB | 2 | 2%  - - - - - - - - - - vc14_runtime-14.51.3 | 720 KB | ########## | 100%  - - - - - - - - - - - libcurl-8.21.0 | 395 KB | 4 | 4%  - - - - - - - - - - - libcurl-8.21.0 | 395 KB | ########## | 100%  - - - - - - - - - - - - cuda-cudart-static_w | 346 KB | 4 | 5%  cuda-nvvm-tools-12.9 | 38.4 MB | #######7 | 77% - - - - - - - - - - - - cuda-cudart-static_w | 346 KB | ########## | 100%  - - - - - - - - - - - - - llvm-openmp-22.1.8 | 340 KB | 4 | 5%  - - - - - - - - - - - - - - scikit-build-core-1. | 327 KB | 4 | 5%  - - - - - - - - - - - - - llvm-openmp-22.1.8 | 340 KB | ########## | 100%  - - - - - - - - - - - - - - scikit-build-core-1. | 327 KB | ########## | 100%  - - - - - - - - - - - - - - - vswhere-3.1.7 | 233 KB | 6 | 7%  - - - - - - - - - - - - - - - - vcomp14-14.51.36231 | 118 KB | #3 | 14%  - - - - - - - - - - - - - - - vswhere-3.1.7 | 233 KB | ########## | 100%  - - - - - - - cuda-cudart-dev_win- | 1.1 MB | ########## | 100%  - - - - - - - - - - - - - - - - vcomp14-14.51.36231 | 118 KB | ########## | 100%  - - - - - - - cuda-cudart-dev_win- | 1.1 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | #########3 | 93% - - - - - - - - - - - - - - - - - cuda-crt-dev_win-64- | 93 KB | #7 | 17%  - - - - - - - - - - - - - - - - - - libpsl-0.22.0 | 84 KB | #9 | 19%  - - - - - - - - - - - - - - - - - cuda-crt-dev_win-64- | 93 KB | ########## | 100%  - - - - - - - - - - - - - - - - - - libpsl-0.22.0 | 84 KB | ########## | 100%  - - - - - - - - - - - - - - - - - - - ... (more hidden) ... - - - - - - - - - - - - - - - - - - - ... (more hidden) ... - libnvptxcompiler-dev | 30.3 MB | ########## | 100%  - - cuda-nvcc-dev_win-64 | 22.4 MB | ########## | 100%  - - - - - - pip-26.1.2 | 1.1 MB | ########## | 100%  - - - - - - pip-26.1.2 | 1.1 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | ########## | 100% - - - - numpy-2.5.1 | 7.0 MB | ########## | 100%  - - - - - - - - - krb5-1.22.2 | 733 KB | ########## | 100%  - - - - - - - - - krb5-1.22.2 | 733 KB | ########## | 100%  - - - - - - - - - - vc14_runtime-14.51.3 | 720 KB | ########## | 100%  - - - - - - - - - - vc14_runtime-14.51.3 | 720 KB | ########## | 100%  - - - - - - - - - - - libcurl-8.21.0 | 395 KB | ########## | 100%  - - - - - - - - - - - libcurl-8.21.0 | 395 KB | ########## | 100%  - - - - - - - - - - - - cuda-cudart-static_w | 346 KB | ########## | 100%  - - - - - - - - - - - - cuda-cudart-static_w | 346 KB | ########## | 100%  - - - - - - - - - - - - - llvm-openmp-22.1.8 | 340 KB | ########## | 100%  - - - - - - - - - - - - - llvm-openmp-22.1.8 | 340 KB | ########## | 100%  - - - - - - - - cuda-cccl_win-64-12. | 1.1 MB | ########## | 100%  - - - - - - - - cuda-cccl_win-64-12. | 1.1 MB | ########## | 100%  - - - - - - - - - - - - - - - vswhere-3.1.7 | 233 KB | ########## | 100%  - - - - - - - - - - - - - - - vswhere-3.1.7 | 233 KB | ########## | 100%  - - - - - - - - - - - - - - - - vcomp14-14.51.36231 | 118 KB | ########## | 100%  - - - - - - - - - - - - - - - - vcomp14-14.51.36231 | 118 KB | ########## | 100%  - - - - - - - - - - - - - - - - - cuda-crt-dev_win-64- | 93 KB | ########## | 100%  - - - - - - - - - - - - - - - - - cuda-crt-dev_win-64- | 93 KB | ########## | 100%  - - - - - - - - - - - - - - - - - - libpsl-0.22.0 | 84 KB | ########## | 100%  - - - - - - - - - - - - - - - - - - libpsl-0.22.0 | 84 KB | ########## | 100%  - - - - - - - - - - - - - - - - - - - ... (more hidden) ... - - - - - - - - - - - - - - - - - - - ... (more hidden) ... - - - - - - - - - - - - - - scikit-build-core-1. | 327 KB | ########## | 100%  - - - - - - - - - - - - - - scikit-build-core-1. | 327 KB | ########## | 100%  - - cuda-nvcc-dev_win-64 | 22.4 MB | ########## | 100%  - libnvptxcompiler-dev | 30.3 MB | ########## | 100%  cuda-nvvm-tools-12.9 | 38.4 MB | ########## | 100% - - - cmake-4.3.4 | 15.3 MB | ########## | 100%  - - - - - - - - - - - - - - - - - - -  -  - -  - - -  - - - -  - - - - -  - - - - - -  - - - - - - -  - - - - - - - -  - - - - - - - - -  - - - - - - - - - -  - - - - - - - - - - -  - - - - - - - - - - - -  - - - - - - - - - - - - -  - - - - - - - - - - - - - -  - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - -  -  - -  - - -  - - - -  - - - - -  - - - - - -  - - - - - - -  - - - - - - - -  - - - - - - - - -  - - - - - - - - - -  - - - - - - - - - - -  - - - - - - - - - - - -  - - - - - - - - - - - - -  done -Preparing transaction: - \ | / done -Verifying transaction: \ | / - \ | / - \ | / - \ | / done -Executing transaction: \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - done -# -# To activate this environment, use -# -# $ conda activate D:\envs\hydra-ci-repro -# -# To deactivate an active environment, use -# -# $ conda deactivate - From 9e29b496841da4d82969cabb13614022e7b6b348 Mon Sep 17 00:00:00 2001 From: Eric Wait Date: Wed, 8 Jul 2026 19:44:23 -0500 Subject: [PATCH 40/40] docs: set the 4.0.0 release date and record the pre-release fixes Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc94a7..13c5905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/), and the project adheres to [Semantic Versioning](https://semver.org/). -## [4.0.0] - Unreleased +## [4.0.0] - 2026-07-08 ### Changed - **Repackaged the Python distribution.** The library is imported as the @@ -25,6 +25,14 @@ All notable changes to this project are documented here. The format is based on code keeps working. - Windows + Linux CI build-gate (`.github/workflows/ci.yml`) and a tag-driven release workflow with a version guard (`.github/workflows/release.yml`). +- Python TIFF accuracy suite (`src/Python/Test/test_accuracy.py`) mirroring the MATLAB + `AccuracyTest.m`; it is the GPU-correctness gate for releases (see TESTING.md). + +### Fixed +- `make_ellipsoid_mask` produced an off-center, asymmetric structuring element (shifted one + voxel), so morphological ops using it diverged from the MATLAB/C++ results. +- The command-framework headers now include `` explicitly instead of relying on + transitive includes, fixing the build with newer gcc/libstdc++ toolchains. ### Removed - The in-repo conda-build recipe (`recipe/`) and the anaconda.org upload workflow.