diff --git a/.condaignore b/.condaignore new file mode 100644 index 00000000..f7c541a9 --- /dev/null +++ b/.condaignore @@ -0,0 +1,7 @@ +build/ +out/ +.vscode/ +.claude/ +*.mltbx +nul +*.reg diff --git a/.gitattributes b/.gitattributes index cef6a61f..038e2da0 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 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..cb281327 --- /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/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..a8f32f21 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,98 @@ +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: Set up MSVC (Windows) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + + - uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-version: latest + 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. + # 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=13.*" + + - 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: + # 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 + # 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: | + 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/matlab-multibuild.yml b/.github/workflows/matlab-multibuild.yml new file mode 100644 index 00000000..c2e59299 --- /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/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..db9a3c66 --- /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 diff --git a/.gitignore b/.gitignore index 7e5b4b14..d3d31171 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,19 @@ CmakeCache.txt out .vs CMakeSettings.json +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/BACKLOG.md b/BACKLOG.md new file mode 100644 index 00000000..95f5fc51 --- /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/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..13c59055 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# 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] - 2026-07-08 + +### 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`). +- 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. + Distribution is now solely via conda-forge. + +[scikit-build-core]: https://scikit-build-core.readthedocs.io/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..f327ee0a --- /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`. diff --git a/CMakeLists.txt b/CMakeLists.txt index acb80ca2..5a6689fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,21 +1,45 @@ -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 "HIP") +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) + +# 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) + +# Find OpenMP - uses llvm-openmp from conda-forge on Windows 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 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) @@ -23,6 +47,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/CMakePresets.json b/CMakePresets.json new file mode 100644 index 00000000..85e7432f --- /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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..4bd8d6c8 --- /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/CONTRIBUTING.md b/CONTRIBUTING.md index 31d8940e..346d2df6 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,44 @@ 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! + +# 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 new file mode 100644 index 00000000..b7009c41 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,78 @@ +# 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 + +# This project uses scikit-build-core: `pip install` drives CMake, compiles the +# CUDA extension, and installs the package. +pip install . +``` + +Alternatively, using the provided CMake presets: + +```bash +cmake --preset dev +cmake --build build --config Release --target HydraPy +``` + +### Conda Package Distribution + +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 -c conda-forge hydra-image-processor +``` + +### 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/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..50c13bea --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,333 @@ +# Hydra Image Processor Roadmap + +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 + +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 | +| --- | --- | --- | +| 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 | +| 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 | +| 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 | +| 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: + +- **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 (Step 3) fixes this permanently. +- **Test data weight**: `test_data/` is ~435 MB of Git-LFS TIFFs — a CI bandwidth hazard as jobs multiply. + +## Development workflow + +- **`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 +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 +``` + +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) + +All items are S unless noted. + +- **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). + 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 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). +- **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 (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` + (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 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). +- **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. + +## 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) + +**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 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, + 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. + - 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; + 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. + +## Step 4 — Filter pipelining (XL; after Step 3; starts with a design doc) + +**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. + +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: + +- **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. +- **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 (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. +- **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 Step 1 before adding jobs. +2. **conda-forge review latency for CUDA recipes** (weeks) — the anaconda.org channel is the hedge; + 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. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..393942f3 --- /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. diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000..e7e9e4f8 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,65 @@ +# 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 (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`. + +## 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__)"`. diff --git a/build-linux/build.md b/build-linux/build.md deleted file mode 100644 index cd53a99a..00000000 --- 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 47b060f9..00000000 --- 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 c7e38562..00000000 --- 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 diff --git a/check_dll_deps.ps1 b/check_dll_deps.ps1 new file mode 100644 index 00000000..3645f396 --- /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 diff --git a/ci-readme.md b/ci-readme.md new file mode 100644 index 00000000..783dc7f3 --- /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 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 00000000..8f64ae97 --- /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 00000000..862a4e1b --- /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 00000000..0357a641 --- /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 00000000..47a8e4d1 --- /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 00000000..9d3ef90a --- /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 00000000..7a5dea58 --- /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 00000000..0c390d9b --- /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 00000000..7863841f --- /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 00000000..b031a78c --- /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 00000000..c6789477 --- /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 00000000..72afd57a --- /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 00000000..7768c478 --- /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 00000000..c0792df9 --- /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 00000000..e1fae514 --- /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 00000000..25936a37 --- /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 00000000..c338fda4 --- /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 00000000..617b4d32 --- /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 00000000..775578d8 --- /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 00000000..50bc76cc --- /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 00000000..94f53419 --- /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 00000000..bd429b7c --- /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 00000000..672395e3 --- /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 00000000..7324b58c --- /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 00000000..8fad0fb7 --- /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 00000000..db01c155 --- /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 00000000..7a195212 --- /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 00000000..352f1179 --- /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 00000000..6aaaf586 --- /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 00000000..44bd4f9c --- /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 00000000..6cdcc61c --- /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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d3310a4b --- /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/readme.md b/readme.md index 7960da89..23e16a5e 100644 --- a/readme.md +++ b/readme.md @@ -1,17 +1,64 @@ -# 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! +## 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) ## 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). diff --git a/src/MATLAB/+Hydra/@Cuda/CheckConfig.m b/src/MATLAB/+Hydra/@Cuda/CheckConfig.m new file mode 100644 index 00000000..473d05cc --- /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 00000000..4e967ca0 --- /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 00000000..05993d9f --- /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 00000000..3522ebc0 --- /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 00000000..37f7d2db --- /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 00000000..a4a4b952 --- /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 00000000..cf2d7f1b --- /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 00000000..adbfbfc1 --- /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 00000000..ea33b06a --- /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 00000000..af64f9a7 --- /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 00000000..c31a295e --- /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 00000000..3dc7331d --- /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 00000000..609b1e33 --- /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 00000000..9015fe6b --- /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 00000000..55cdd3a0 --- /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 00000000..462a2280 --- /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 00000000..fb75f9aa --- /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 00000000..65eb726d --- /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 00000000..db56ef98 --- /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 00000000..56cabb1f --- /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 00000000..c97224c7 --- /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 00000000..17862e3d --- /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 00000000..e685d224 --- /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 00000000..eb401fe0 --- /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 00000000..2c2d955b --- /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 00000000..720499d1 --- /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 00000000..7d480f54 --- /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 00000000..6e4adb33 --- /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 00000000..c0e73552 --- /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 00000000..08f26a81 --- /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 00000000..67cdd5b7 --- /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 00000000..04efe045 --- /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 00000000..6428639d --- /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 00000000..a3cf8645 --- /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 00000000..ce6b9d8d --- /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 00000000..c4594033 --- /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 00000000..3267f9b0 --- /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 00000000..ff3cce1d --- /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 00000000..7687af0f --- /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 00000000..abb87159 --- /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 00000000..a98005d3 --- /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 00000000..5fefbc92 --- /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 00000000..e3191249 --- /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 00000000..7b1ab7c2 --- /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 00000000..9fd2c5fc --- /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 00000000..1218a581 --- /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 00000000..620082ef --- /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 00000000..55e5a646 --- /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 diff --git a/src/MATLAB/+Test/AccuracyTest.m b/src/MATLAB/+Test/AccuracyTest.m new file mode 100644 index 00000000..f0a346bd --- /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/Python/HIP.py b/src/Python/HIP.py new file mode 100644 index 00000000..2bfacfb5 --- /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/HIP.so b/src/Python/HIP.so deleted file mode 100755 index 5739ceef..00000000 --- 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/Hydra.py b/src/Python/Hydra.py new file mode 100644 index 00000000..81523471 --- /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/README.md b/src/Python/README.md new file mode 100644 index 00000000..f67bbaef --- /dev/null +++ b/src/Python/README.md @@ -0,0 +1,79 @@ +# Hydra Image Processor (Python) + +`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. + +## Installation + +Distributed through [conda-forge](https://conda-forge.org/). Requires an NVIDIA +CUDA-capable GPU with an up-to-date driver. + +```bash +conda install -c conda-forge hydra-image-processor +# or: mamba install -c conda-forge hydra-image-processor +# or: pixi add hydra-image-processor +``` + +> **pip / uv are not supported.** This is a CUDA-only compiled extension with no PyPI +> wheels — install it with conda, mamba, or pixi. + +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_image_processor as HIP +import numpy as np + +# Check available CUDA devices +print(f"Found {HIP.device_count()} GPU(s)") + +# 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]) + +# Median filter with a custom kernel +kernel = HIP.make_ball_mask(radius=3) +filtered = HIP.median_filter(image, kernel) +``` + +### Package layout + +- `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. + +### 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 +``` + +New code should prefer `import hydra_image_processor as HIP`. + +## Building from source + +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 +# from the repository root (where pyproject.toml lives) +pip install . +``` + +To build for a specific GPU architecture instead of the default `all-major`: + +```bash +CMAKE_ARGS="-DCMAKE_CUDA_ARCHITECTURES=native" pip install . +``` + +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for the full developer and release workflow. diff --git a/src/Python/Test/test_accuracy.py b/src/Python/Test/test_accuracy.py new file mode 100644 index 00000000..2eb077ec --- /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) diff --git a/src/Python/download-script.py b/src/Python/download-script.py new file mode 100644 index 00000000..cce88cfa --- /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/environment.yml b/src/Python/environment.yml index 8c6d2aa4..1f0b6f96 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" diff --git a/src/Python/example_usage.py b/src/Python/example_usage.py new file mode 100644 index 00000000..95650285 --- /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/hydra_image_processor/README.md b/src/Python/hydra_image_processor/README.md new file mode 100644 index 00000000..f69a91c8 --- /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 00000000..779eeb3c --- /dev/null +++ b/src/Python/hydra_image_processor/__init__.py @@ -0,0 +1,190 @@ +""" +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 +""" + +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" + +# 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 00000000..d144cab7 --- /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 00000000..a3775d4e --- /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 00000000..dc73a5b1 --- /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 00000000..ccb48899 --- /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 00000000..13f99d50 --- /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 00000000..22cdfe34 --- /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 00000000..399b307b --- /dev/null +++ b/src/Python/hydra_image_processor/utils/masks.py @@ -0,0 +1,153 @@ +""" +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 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) + + # 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/test-hydra.py b/src/Python/test-hydra.py new file mode 100644 index 00000000..fd3c3d30 --- /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 00000000..bf705bd6 --- /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 7363b60b..72028400 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 00000000..4b478137 --- /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()) diff --git a/src/c/Cuda/CMakeLists.txt b/src/c/Cuda/CMakeLists.txt index cf419dd4..514563de 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 "52;61;70;75;86;89") +# 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,12 +80,13 @@ 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 (/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") - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -Xcompiler=-fopenmp") else() message(FATAL_ERROR "Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}") endif() @@ -97,21 +100,23 @@ 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(OpenMP_CXX_FOUND) + target_link_libraries(${LIB_NAME} PRIVATE OpenMP::OpenMP_CXX) + # Set CUDA-specific compile options + target_compile_options(${LIB_NAME} PRIVATE + $<$:${OPENMP_FLAG}> + $<$:-Xcompiler=${OPENMP_FLAG}> + ) + 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/Cuda/CudaAddTwoImages.cuh b/src/c/Cuda/CudaAddTwoImages.cuh index ef8df265..9e8c1494 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 b17edc44..e90bbeee 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 723114a6..98a89ccc 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 50fd49b3..c9645310 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 1c201280..16164f96 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 3e5be064..55dbd43b 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 446e417e..af3127ac 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 c72e9811..eac13819 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 ba088cdc..95c3871a 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 1ada5590..434ee963 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 d5343301..e89353b3 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 498602e9..207c893c 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 059adbf7..beaccfbf 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 93fd8309..f8da1e8e 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 cd3e124c..5b538c4b 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 46a94be9..120a057b 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 2e4758da..6e10d6bb 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 02218b3a..2cbfd992 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 446efb36..b56d7fd2 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 282c4489..9b1f1954 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 cc9d8aa4..ec48a433 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 9da0bd51..6db109a4 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 0d359288..8c4cad45 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 8f9dead2..9b523a12 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()); diff --git a/src/c/Python/CMakeLists.txt b/src/c/Python/CMakeLists.txt index 8c86a710..1f0cb00b 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 @@ -19,7 +19,7 @@ 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 @@ -28,7 +28,7 @@ if ( WIN32 ) 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 @@ -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 "." +) diff --git a/src/c/Python/HydraPyModule.cpp b/src/c/Python/HydraPyModule.cpp index 4b7131d8..912c13a2 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 3fe7c2b0..75fd7158 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 0f65a294..ebda1a2b 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 diff --git a/src/c/ScriptCmds/Commands/ScrCmdDeviceCount.h b/src/c/ScriptCmds/Commands/ScrCmdDeviceCount.h index 8800441c..3266b798 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 72f686c3..c62e2aff 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 25103a54..331f4f7c 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 073b3764..dcb66dc3 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/CMakeLists.txt b/src/c/test_back/CMakeLists.txt index 2b87f21a..9828442e 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.cpp b/src/c/test_back/test.cpp index 2071a714..beea5730 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 new file mode 100644 index 00000000..d44b5aff --- /dev/null +++ b/src/c/test_back/test_accuracy.cpp @@ -0,0 +1,133 @@ +#include "CWrappers.h" +#include "ImageView.h" +#include "Vec.h" + +#include +#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() +{ + // 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); + 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; +} + +bool test_min_max() +{ + Vec dims = {32, 32, 10}; + ImageOwner imIn(0, dims, 1, 1); + + // Pattern that covers range + for(size_t i=0; i