From 8b77be2443512fd071d89fe00667ccc30d67840d Mon Sep 17 00:00:00 2001 From: Demian Vladi Date: Thu, 16 Jul 2026 07:53:51 -0700 Subject: [PATCH 1/4] chore(vetting): squash pre-IO vetting/process housekeeping (not part of the IO series) Foundation branch for the main-io-reorg-* stack. Bundles oracle-vetting promotions for the moments/GLCM/GLRLM/GLSZM/GLDM/morphology/caliper feature families, a release-please manifest fix, and the CLAUDE.md workflow guide -- none of which are IO/OME work. Kept separate from the 4 IO PRs; should be reviewed/merged via its own track. Absorbed commits (oldest to newest): 73f6ca8 fix(moments): correct Hu invariant h5/h6 formulas (9x bracket, stray +eta03) a500bd9 ci(release-please): use manifest config so 0.x breaking = minor, and pin 0.12.0 79d0974 fix(3d): compute 3ROBUST_MEAN (was hardcoded 0) + vet 18 3D first-order features 60a1ebb fix(glcm): normalize HOM2/ENTROPY by sum_p (2D+3D) + fully oracle-vet GLCM d44dde7 test(glrlm): vet 12 2D gray-level-run joint-emphasis features vs IBSI cb07b9d test(glrlm): vet 14 3D _AVE features vs PyRadiomics -> glrlm 100% 285bc39 docs(vetting): record the config scope of the glrlm promotions 7db4532 test(glszm): vet 6 2D gray-level-zone joint-emphasis features vs IBSI -> glszm 100% c4bb8b1 docs(vetting): record the config scope of the glszm promotions d213b50 test(gldm): vet 14 2D grey-level-dependence features vs IBSI -> gldm 100% 0780912 docs(vetting): record the config scope of the gldm promotions (7 are mode-invariant) 0af100b fix(3d-shape): correct mislabeled axis lengths + vet 7 morphology features bc3e92a fix(caliper): correct Martin/Nassenstein diameters + vet 12 features vs imea 059998c test(morphology): vet Feret diameters vs imea + promote 6 features 701201b test(morphology): vet min-enclosing-circle diameter vs imea/cv2 965ab47 test(morphology): vet 8 EXTREMA coords vs matlab regionprops 992d5e6 test(morphology): promote FRACT_DIM_BOXCOUNT/PERIMETER (fraclac oracle) a9ddc15 test(morphology): vet FRACT_DIM (fraclac) + CIRCULARITY/ROUNDNESS (analytic) 3377183 fix(caliper): float-precision hull rotation + address PR #381 review cc5b4de docs(caliper): correct inaccurate/stale comments (no code change) b924f8e docs(vetting): retract unproven 3D NGLDM "ibsi" promotions; document MIRP disagreement e301fe6 docs: add CLAUDE.md codebase and workflow guide a548b57 docs(CLAUDE): add git workflow and tests/vetting conformance rules 466c038 docs(CLAUDE): address PR #388 review - C++20; agentic git workflow 4235e0c docs(CLAUDE): address PR #388 review - generalize paths, drop z5py pin, add no-local-paths rule --- .github/workflows/release-please.yml | 8 +- CLAUDE.md | 264 ++++++++++++ src/nyx/features/2d_geomoments_basic.cpp | 6 +- src/nyx/features/2d_geomoments_basic_nt.cpp | 6 +- src/nyx/features/3d_glcm.cpp | 11 +- src/nyx/features/3d_intensity.cpp | 25 ++ src/nyx/features/3d_surface.cpp | 118 +++--- src/nyx/features/caliper.h | 3 - src/nyx/features/caliper_feret.cpp | 9 +- src/nyx/features/caliper_martin.cpp | 221 ++++------ src/nyx/features/caliper_nassenstein.cpp | 189 +++------ src/nyx/features/glcm.cpp | 11 +- src/nyx/features/rotation.cpp | 35 +- src/nyx/features/rotation.h | 14 +- src/nyx/gpu/geomoments_hu.cu | 12 +- tests/test_3d_coverage_common.h | 18 +- tests/test_3d_glcm_pyradiomics.h | 65 +++ tests/test_3d_glrlm_pyradiomics.h | 60 +++ ...gldm_ibsi.h => test_3d_ngldm_regression.h} | 37 ++ tests/test_all.cc | 62 ++- tests/test_glcm_ibsi.h | 12 + tests/test_glcm_regression.h | 4 +- tests/test_glrlm_ibsi.h | 50 ++- tests/test_moments_common.h | 55 +++ tests/test_moments_regression.h | 8 +- tests/test_moments_skimage.h | 46 +- tests/test_morphology_common.h | 8 + tests/test_morphology_regression.h | 82 ++++ tests/test_remaining2d_common.h | 210 +++++++++- tests/vetting/MIGRATION.md | 10 +- tests/vetting/coverage_report.md | 18 +- tests/vetting/oracle_coverage.csv | 396 +++++++++--------- tests/vetting/oracles/gen_moments_skimage.py | 260 ++++++++++++ 33 files changed, 1727 insertions(+), 606 deletions(-) create mode 100644 CLAUDE.md rename tests/{test_3d_ngldm_ibsi.h => test_3d_ngldm_regression.h} (68%) create mode 100644 tests/vetting/oracles/gen_moments_skimage.py diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index b8b4d7946..600de0ccb 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -20,8 +20,12 @@ jobs: - uses: google-github-actions/release-please-action@v4 id: release with: - release-type: python - package-name: nyxus + # Use the manifest config so release-please honors bump-minor-pre-major + # (0.x breaking changes -> minor, not a jump to 1.0.0). release-type/ + # package-name live in release-please-config.json; passing them inline put + # v4 on the non-manifest path and silently ignored the config. + config-file: release-please-config.json + manifest-file: .release-please-manifest.json - name: Checkout code if: ${{ steps.release.outputs.release_created }} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..54a315536 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,264 @@ +# CLAUDE.md + +Guidance for Claude Code (and humans) working in this repository. + +## What Nyxus is + +Nyxus is a scalable C++/Python library that computes engineered features +(intensity, texture, morphology, moments, digital-filter) from segmented and +whole-slide/whole-volume images. It computes 450+ 2D features and a large 3D +feature set at the ROI or whole-image level, and is designed to operate at any +scale by assembling ROIs that span multiple image tiles and files. + +- **Python API**: classes `Nyxus` (2D), `Nyxus3D` (3D volumes), `Nested` + (hierarchical parent/child ROIs), `ImageQuality`. Results come back as a + pandas DataFrame; Arrow IPC and Parquet output are also supported. +- **CLI**: the `nyxus` executable (built with `-DBUILD_CLI=ON`). +- **IO**: 2D from OME-TIFF, OME-Zarr, DICOM; 3D from NIFTI (compressed and + uncompressed); in-memory NumPy arrays via the Python API. +- Upstream is [PolusAI/nyxus](https://github.com/PolusAI/nyxus). C++20, + CMake build system. Version is derived from git tags via `setuptools_scm`. + +## Repository layout + +``` +CMakeLists.txt top-level build; feature/IO/GPU options +pyproject.toml Python packaging (package-dir = src/nyx/python) +environment.yml conda deps +ci-utils/envs/ conda dep lists: conda_cpp.txt, conda_py.txt, conda_gpu.txt, ... +ci-utils/install_prereq_windows.bat / install_prereq_linux.sh non-conda dep builders +.github/workflows/ CI: build_and_test_{windows,ubuntu,mac}.yml + wheel/publish jobs +docs/ Sphinx docs (Read the Docs); docs/source/featurelist.rst +tests/python/ pytest suite (this is what CI runs) +tests/vetting/ feature-validation "oracle" framework (see below) +src/nyx/ all C++ sources +``` + +### `src/nyx/` — the C++ core + +- **Entry points**: `main_nyxus.cpp` (CLI `main()`), `python/new_bindings_py.cpp` + (pybind11 module), `python/nested_roi_py.cpp`. +- **Feature dispatch / registration** (the backbone — read these first when + adding or debugging a feature): + - `featureset.h` / `featureset.cpp` — the `Feature2D`, `Feature3D`, and + `FeatureIMQ` (image-quality) enums (canonical feature codes) and the + code↔name maps. Every feature has an enum entry here. + - `feature_method.h/.cpp` — `FeatureMethod` abstract base every feature class + derives from. Each feature declares `provide_features(...)` / + `add_dependencies(...)` and implements `calculate()` (trivial/in-RAM ROI) + and `osized_*()` (out-of-core ROI). + - `feature_mgr.cpp`, `feature_mgr.h` — the `FeatureManager`: registration, + 1:1 code-correspondence check, cyclic-dependency resolution, user-selection + compilation, `init_feature_classes()`. + - **`feature_mgr_init.cpp`** — the concrete registration list + (`register_feature(new GLCMFeature()); ...` for every 2D/3D feature). + **Start here to see what is actually wired in.** + - `env_features.cpp`, `environment*.cpp/h`, `feature_settings.h` — parsing the + requested feature set / groups (e.g. `*ALL*`, `*ALL_GLCM*`, `*WHOLESLIDE*`, + `*3D_ALL*`) and all run parameters. +- **Feature implementations**: `src/nyx/features/` (~100 files). Naming: + - 2D feature classes (e.g. `glcm.*`, `glrlm.*`, `basic_morphology.*`, + `2d_geomoments.*`, `gabor.*`, `caliper_*`, `convex_hull*`, `contour.*`, + `erosion.*`, `radial_distribution.*`, `zernike.*`). + - 3D feature classes are prefixed `3d_` (e.g. `3d_glcm.*`, `3d_intensity.*`, + `3d_surface.*`, `3d_gldzm.*`). + - Image-quality features (exposed via the `ImageQuality` Python class): + `focus_score.*`, `power_spectrum.*`, `saturation.*`, `sharpness.*`. + - `*_nontriv*.cpp` files hold the **out-of-core ("non-trivial ROI")** variant + of a feature — the streaming implementation used when an ROI is too large to + hold in RAM as a dense buffer. The in-RAM ("trivial") path and the + out-of-core path must produce identical values. +- **ROI model**: `roi_cache*.cpp/h` (`LR` = labeled ROI accumulators), + `nested_roi.*` (parent/child aggregation), `roi_blacklist.*` (`--skiproi`), + `cache.*` / `gpucache.cpp`. In-RAM ROI image in `features/image_matrix.*`; + out-of-core `OutOfRamPixelCloud` in `features/image_matrix_nontriv.*`; 3D in + `features/image_cube.h`. +- **Analysis pipeline** (three phases, declared in `globals.h`, dispatched from + `main_nyxus.cpp`): dataset drivers `workflow_2d_segmented.cpp`, + `workflow_2d_whole.cpp`, `workflow_3d_segmented.cpp`, `workflow_3d_whole.cpp`, + `workflow_pythonapi.cpp`; then `phase1.cpp` (gather ROI metrics, decide + trivial vs oversized), `phase2_2d.cpp` / `phase2_25d.cpp` / `phase2_3d.cpp` + (feature calc), `phase3.cpp` (output). ROIs over the memory limit go through + `processNontrivialRois()` + each feature's streaming + `osized_scan_whole_image()` / `osized_add_online_pixel()`; in-RAM ROIs go + through `processTrivialRois*()`. See also `reduce_trivial_rois.cpp`, + `pixel_feed.cpp`, `features_calc_workflow.cpp`. +- **Image loading**: `image_loader*.cpp/h`, `raw_image_loader.*`, and format + backends `raw_tiff.h`, `raw_omezarr.h`, `raw_dicom.h`, `raw_nifti.*`, + `grayscale_tiff.h`, `abs_tile_loader.h`; NIFTI support under `io/nifti/`. +- **GPU (CUDA)**: `src/nyx/gpu/*.cu/.cuh` — GPU kernels for the compute-heavy + features (Gabor, erosion, geometric moments). Guarded by `-DUSEGPU=ON` / + `USE_GPU`. Enabled at runtime with `--useGpu=true` (CLI) or the constructor. +- **Arrow/Parquet output**: `arrow_helpers.cpp`, `arrow_output_stream.*` + (guarded by `USE_ARROW`). +- **Helpers / 3rd-party**: `helpers/` (thread pool, FFT, timing, least-squares), + `3rdparty/` (quickhull, Jacobi eigensolver). + +### `src/nyx/python/` — Python layer + +- `new_bindings_py.cpp` — pybind11 bindings exposing the C++ backend as + `nyxus.backend`. +- `nyxus/__init__.py` — public exports: `Nyxus`, `Nyxus3D`, `Nested`, + `ImageQuality`, `gpu_is_available`, `get_gpu_properties`. +- `nyxus/nyxus.py` — the Python-facing wrapper classes (`featurize`, + `featurize_directory`, `featurize_files`, `get_params`/`set_params`, + `blacklist_roi`, …). +- `nyxus/functions.py` — GPU helper functions. + +## Building + +A C++20 compiler + CMake ≥ 3.20 are required. Key CMake options: + +| Option | Meaning | +|---|---| +| `-DBUILD_CLI=ON` | build the `nyxus` command-line executable | +| `-DBUILD_LIB=ON` | build the Python extension (`backend` target) | +| `-DALLEXTRAS=ON` | enable **all** IO: Zarr (z5) + DICOM (dcmtk) + Arrow | +| `-DNOEXTRAS=OFF` + `-DUSE_ARROW/USE_Z5/USE_DCMTK=ON` | enable IO backends selectively (NOEXTRAS defaults ON and force-disables them) | +| `-DUSEGPU=ON` | build CUDA GPU kernels (requires a CUDA toolkit matching the host compiler) | + +Deps are easiest via conda (`ci-utils/envs/conda_cpp.txt` + `conda_py.txt`); set +`NYXUS_DEP_DIR=$CONDA_PREFIX` (or `$CONDA_PREFIX/Library` on Windows) so CMake +finds them. The README "Building from source" section has full Linux/Windows and +Docker recipes. + +### Windows (MSVC + optional CUDA) + +Use **MSVC**, not gcc — conda-forge Windows packages are MSVC-ABI, so a gcc +build cannot link them. Full-feature + CUDA build (paths below are placeholders: +`$ENV` = your conda env prefix): + +1. Create the build env: + ``` + conda create -n nyxus_build -c conda-forge python=3.12 \ + --file ci-utils/envs/conda_cpp.txt --file ci-utils/envs/conda_py.txt + ``` +2. Configure with the **Ninja** generator so `nvcc` can use `cl` as its host + compiler (no CUDA MSBuild integration needed): run the Visual Studio + `vcvars64.bat` first, then `cmake -G Ninja -DCMAKE_CXX_COMPILER=cl + -DBUILD_CLI=ON -DALLEXTRAS=ON -DUSEGPU=ON -DCMAKE_PREFIX_PATH=$ENV\Library + -DNYXUS_DEP_DIR=$ENV\Library ..` then `cmake --build . --config Release`. +3. To run, put `$ENV\Library\bin` (dependency DLLs) and the CUDA toolkit `bin` + on PATH. + +**Fast Python-extension build** for running the test suite (tiff-only CPU): +same as above but `-DBUILD_LIB=ON -DALLEXTRAS=OFF -DUSEGPU=OFF +-DPYTHON_EXECUTABLE=$ENV\python.exe -Dpybind11_DIR=$ENV\Lib\site-packages\pybind11\share\cmake\pybind11` +and `--target backend`, with `-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE` pointed at +`src\nyx\python\nyxus` so the `.pyd` lands in the package. With `ALLEXTRAS=OFF` +the `@pytest.mark.arrow` tests fail with "Apache Arrow functionality is not +available" — expected, not a regression. To exercise Arrow/Parquet without a slow +CUDA build, use `-DNOEXTRAS=OFF -DUSE_ARROW=ON -DUSE_Z5=OFF -DUSE_DCMTK=OFF`. + +> Gotcha: in a Git Bash / MSYS shell, bare `cmd` resolves to an MSYS shim that +> silently no-ops `.bat` files and returns exit 0. Invoke `.bat` files via +> `& "$env:ComSpec" /c ` from the PowerShell tool, or the full-path +> `cmd.exe` under `%SystemRoot%\System32`. + +## Running tests + +**Python (`tests/python/`)** — the suite CI runs +(`.github/workflows/build_and_test_*.yml`): + +``` +python -m pytest tests/python/ -vv +``` + +Run the **whole directory**, not selected files — some bugs only appear as +cross-test state pollution (e.g. a mutable-default-argument leak surfaced only +because an alphabetically-earlier test seeded shared state). Selecting a single +test can hide such failures. Requires an importable `nyxus.backend` (`.pyd` on +`PYTHONPATH`, dependency DLLs on `PATH`). Markers (`pytest.ini`): `arrow`, +`serial`, `skip_ci`. Key files: `test_nyxus.py` (main API) plus the oracle / +regression suites listed under "Feature validation" below. + +**C++ (GoogleTest, `tests/`)** — target `runAllTests` (enabled from the +top-level `CMakeLists.txt`; gtest fetched via `tests/CMakeLists.txt` + +`CMakeLists.txt.gtest`). `tests/test_all.cc` is the single gtest `main` and +`#include`s the many `test_*.h` headers (they are intentionally **not** listed +in CMake — see the note in `tests/CMakeLists.txt`). Headers are grouped by +feature and by oracle: `test__ibsi.h` (IBSI conformance), +`test__pyradiomics.h`, `test__regression.h`, +`test__mechanics.h`, and `test_3d_*.h`; fixtures in `test_data.h`. +Images live in `tests/data/`. + +## Feature validation ("oracle") framework + +Correctness of feature values is validated against external reference +implementations ("oracles"), not just regression snapshots. This is a +first-class concern in this project. + +**Tests must conform to the framework specified in `tests/vetting/`.** The +governing documents are: + +- **`tests/vetting/SPEC.md`** — the authoritative spec. Defines the vocabulary, + the four test kinds (**oracle** / **regression** / **invariant** / + **mechanics**, kept in separate files), the rule that *vetting is a property of + a (feature × config × reference) assertion* (only oracle tests establish + vetting; regression tests never claim it), the tolerance policy, and the + authoring checklist for adding a vetted feature. Any new or changed test must + follow it — taxonomy, naming, tolerances, and registry update included. +- `tests/vetting/oracle_coverage.csv` — the single source of truth: one row per + assertion (`feature × config_recipe × oracle`) with its `outcome` + (`vetted` / `regression` / `invariant` / `not_tested` / `not_implemented` / + `dropped_invalid`), tolerance, backing test, and benchmark. A feature counts + as vetted iff it has ≥1 `vetted` oracle row. +- `tests/vetting/config_recipes.md` — the exact Nyxus + tool settings that make + a feature directly comparable to a reference (referenced by id from the + registry). `tests/vetting/matrix/.md` — the config-point matrix and + per-cell verdict. `tests/vetting/README.md`, `TOOLS.md`, `MIGRATION.md` — + supporting guides. +- Allowed oracle tokens (SPEC §4): pyradiomics, radiomicsj, skimage, mirp, + matlab, cellprofiler, mitk, feature2djava, wndcharm, imea, imagej, fraclac, + pydicom, ibsi, analytic. Oracle goldens are generated **offline** by a + checked-in generator (`tests/vetting/oracles/gen__.*`) and + pinned with full provenance — **reference tools are never CI runtime + dependencies**. +- `tests/vetting/check_coverage.py` validates the registry and regenerates + `coverage_report.md` (stdlib only). Naming: test files are + `test__.{h,py}` (e.g. `test_glcm_pyradiomics.py`, + `test_glcm_regression.h`, `test_glcm_mechanics.h`); functions carry the oracle + suffix so vetting status is self-evident. + +When you change a feature's math, re-vet it against its oracle at the recipe's +config and update the registry row — don't just re-baseline a snapshot. A +tolerance loose enough to pass a known-bad value is itself a test bug. + +## Conventions for changes + +- **Branches** are cut from and named after their base branch as + `main-` (not `fix/` or `feat/`). +- **Annotate changed source lines**: when fixing a defect, add a brief comment + on the changed line(s) explaining the defect and the fix, so the reason + survives in the code. +- Match the density, naming, and idiom of the surrounding code. +- **Run `pytest tests/python/` (the full directory) locally before proposing a + push** — it mirrors CI and catches cross-test pollution. +- The trivial (in-RAM) and non-trivial (out-of-core) implementations of a + feature must return identical values; if you touch one, check the other. +- **Any new or modified test must conform to the framework in + `tests/vetting/SPEC.md`** — correct test kind (oracle/regression/invariant/ + mechanics), naming, tolerance policy, and an updated `oracle_coverage.csv` + row. See "Feature validation" above. +- **Never commit local or machine-specific content into repository files.** No + absolute paths (`C:\Users\...`, `/home/...`), usernames, personal install + locations, or references to files that live only on one machine. Use + placeholders instead — `$ENV` / `%CONDA_PREFIX%` for the conda prefix, + ``, `%SystemRoot%`, `~`. Keep machine-specific setup in a local, + gitignored file, never in a tracked doc. Scan every doc for these before + committing. + +### Git + +- `main` is branch-protected — **never commit or push to `main` directly.** +- Claude may commit and push to non-`main` (feature/topic) branches as part of + an agentic workflow; final review and merge into `main` are the maintainers'. +- **Never add a `Co-Authored-By:` trailer** (or any similar attribution line) + to commit messages. + +## Docs + +Feature reference: `docs/source/featurelist.rst`. Rendered docs at +https://nyxus.readthedocs.io. The README has extensive Python/CLI usage +examples (directory/file/NumPy featurization, whole-slide, 3D, nested ROIs, +Hounsfield/CT handling, anisotropy, Arrow/Parquet output). diff --git a/src/nyx/features/2d_geomoments_basic.cpp b/src/nyx/features/2d_geomoments_basic.cpp index 29ab46d0b..ed288a173 100644 --- a/src/nyx/features/2d_geomoments_basic.cpp +++ b/src/nyx/features/2d_geomoments_basic.cpp @@ -241,10 +241,10 @@ std::tuple BasicGeomoms2 (_30 + _12) * (int_pow(_30 + _12, 2) - 3 * int_pow(_21 + _03, 2)) + (3 * _21 - _03) * (_21 + _03) * - (int_pow(3 * (_30 + _12), 2) - int_pow(_21 + _03, 2)); + (3 * int_pow(_30 + _12, 2) - int_pow(_21 + _03, 2)); // FIX: was int_pow(3*(_30+_12), 2) == 9*(eta30+eta12)^2; Hu's I5 second bracket is 3*(eta30+eta12)^2 - (eta21+eta03)^2 (cf. the correct bracket in h7 below) double h6 = (_20 - _02) * (int_pow(_30 + _12, 2) - - int_pow(_21 + _03, 2)) + (4 * _11 * (_30 + _12) * - _21 + _03); + int_pow(_21 + _03, 2)) + + 4 * _11 * (_30 + _12) * (_21 + _03); // FIX: was 4*_11*(_30+_12)*_21 + _03 - precedence error left "+_03" outside the product, so the stray raw eta03 term dominated h6; Hu's I6 last term is 4*eta11*(eta30+eta12)*(eta21+eta03) double h7 = (3 * _21 - _03) * (_30 + _12) * (int_pow(_30 + _12, 2) - 3 * int_pow(_21 + _03, 2)) - (_30 - 3 * _12) * (_21 + _03) * (3 * int_pow(_30 + _12, 2) - int_pow(_21 + _03, 2)); diff --git a/src/nyx/features/2d_geomoments_basic_nt.cpp b/src/nyx/features/2d_geomoments_basic_nt.cpp index a38a0121c..bb39f4f20 100644 --- a/src/nyx/features/2d_geomoments_basic_nt.cpp +++ b/src/nyx/features/2d_geomoments_basic_nt.cpp @@ -112,10 +112,10 @@ std::tuple BasicGeomoms2 (_30 + _12) * (pow(_30 + _12, 2) - 3 * pow(_21 + _03, 2)) + (3 * _21 - _03) * (_21 + _03) * - (pow(3 * (_30 + _12), 2) - pow(_21 + _03, 2)); + (3 * pow(_30 + _12, 2) - pow(_21 + _03, 2)); // FIX: was pow(3*(_30+_12), 2) == 9*(eta30+eta12)^2; Hu's I5 second bracket is 3*(eta30+eta12)^2 - (eta21+eta03)^2 (same defect as the trivial-ROI sibling; cf. correct h7 below) double h6 = (_20 - _02) * (pow(_30 + _12, 2) - - pow(_21 + _03, 2)) + (4 * _11 * (_30 + _12) * - _21 + _03); + pow(_21 + _03, 2)) + + 4 * _11 * (_30 + _12) * (_21 + _03); // FIX: was 4*_11*(_30+_12)*_21 + _03 - precedence error left "+_03" outside the product; Hu's I6 last term is 4*eta11*(eta30+eta12)*(eta21+eta03) double h7 = (3 * _21 - _03) * (_30 + _12) * (pow(_30 + _12, 2) - 3 * pow(_21 + _03, 2)) - (_30 - 3 * _12) * (_21 + _03) * (3 * pow(_30 + _12, 2) - pow(_21 + _03, 2)); diff --git a/src/nyx/features/3d_glcm.cpp b/src/nyx/features/3d_glcm.cpp index 505c4dce1..08ebb27fa 100644 --- a/src/nyx/features/3d_glcm.cpp +++ b/src/nyx/features/3d_glcm.cpp @@ -613,7 +613,12 @@ double D3_GLCM_feature::f_entropy(const SimpleMatrix& P) for (j = 0; j < Ng; j++) for (i = 0; i < Ng; i++) - entropy += P.xy(i, j) * fast_log10(P.xy(i, j) + EPSILON) / LOG10_2; + { + // FIX: normalize by sum_p (joint probability), matching f_GLCM_JE. Was summing the + // UNNORMALIZED co-occurrence counts -> negative "entropy" (same bug as 2D glcm.cpp). + double p = P.xy(i, j) / sum_p; + entropy += p * fast_log10(p + EPSILON) / LOG10_2; + } return -entropy; } @@ -933,7 +938,9 @@ double D3_GLCM_feature::f_GLCM_HOM2(const SimpleMatrix& P_matrix) for (int x = 0; x < n_levels; x++) for (int y = 0; y < n_levels; y++) - hom2 += P_matrix.xy(x, y) / (1.0 + (double)std::abs(x - y) * (double)std::abs(x - y)); + // FIX: normalize by sum_p (joint probability), matching f_idm/f_GLCM_JE. Was summing the + // UNNORMALIZED co-occurrence counts -> homogeneity > 1 (same bug as 2D glcm.cpp). + hom2 += (P_matrix.xy(x, y) / sum_p) / (1.0 + (double)std::abs(x - y) * (double)std::abs(x - y)); return hom2; } diff --git a/src/nyx/features/3d_intensity.cpp b/src/nyx/features/3d_intensity.cpp index 91292a1cc..2777609fb 100644 --- a/src/nyx/features/3d_intensity.cpp +++ b/src/nyx/features/3d_intensity.cpp @@ -136,6 +136,19 @@ void D3_VoxelIntensityFeatures::calculate (LR &r, const Fsettings& s, const Data medad += std::abs(px.inten - median_); val_MEDIAN_ABSOLUTE_DEVIATION = medad / n; + // FIX 3ROBUST_MEAN: was never assigned in this (trivial) path so it defaulted to 0, unlike the + // 2D PixelIntensityFeatures which computes it (intensity.cpp) -> port the same definition: + // mean of voxels within the [P10,P90] robust window (p10_/p90_ from H.get_stats() above). + double robustMean = 0.0; + size_t robustCount = 0; + for (auto& px : B) + if (px.inten >= p10_ && px.inten <= p90_) + { + robustMean += px.inten; + robustCount++; + } + val_ROBUST_MEAN = robustCount ? robustMean / double(robustCount) : 0.0; + // --Uniformity calculated as PIU, percent image uniformity - see "A comparison of five standard methods for evaluating image intensity uniformity in partially parallel imaging MRI" [https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3745492/] and https://aapm.onlinelibrary.wiley.com/doi/abs/10.1118/1.2241606 double piu = (1.0 - double(r.aux_max - r.aux_min) / double(r.aux_max + r.aux_min)) * 100.0; val_UNIFORMITY_PIU = piu; @@ -260,6 +273,18 @@ void D3_VoxelIntensityFeatures::osized_calculate (LR & r, const Fsettings & s, c medad += std::abs(px.inten - median_); val_MEDIAN_ABSOLUTE_DEVIATION = medad / n; + // FIX 3ROBUST_MEAN: same omission in the out-of-core path (defaulted to 0) -> compute the + // mean of voxels within the [P10,P90] robust window, matching the 2D implementation. + double robustMean = 0.0; + size_t robustCount = 0; + for (auto& px : r.raw_pixels_3D) + if (px.inten >= p10_ && px.inten <= p90_) + { + robustMean += px.inten; + robustCount++; + } + val_ROBUST_MEAN = robustCount ? robustMean / double(robustCount) : 0.0; + // --Uniformity calculated as PIU, percent image uniformity - see "A comparison of five standard methods for evaluating image // intensity uniformity in partially parallel imaging MRI" [https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3745492/] // and https://aapm.onlinelibrary.wiley.com/doi/abs/10.1118/1.2241606 diff --git a/src/nyx/features/3d_surface.cpp b/src/nyx/features/3d_surface.cpp index 7883a71a9..ddc30bf12 100644 --- a/src/nyx/features/3d_surface.cpp +++ b/src/nyx/features/3d_surface.cpp @@ -278,19 +278,19 @@ void dump_skinny_contour_3D ( void D3_SurfaceFeature::build_surface (LR & r) { // read the point cloud of contours - constexpr std::size_t dim = 3; - using Points = std::vector>; + constexpr std::size_t dim = 3; + using Points = std::vector>; Points P; for (auto& plane : r.contours_3D) { for (auto ip : plane) { auto v = r.raw_pixels_3D[ip]; - P.push_back (std::array({ (float)v.x, (float)v.y, (float)v.z })); + P.push_back (std::array({ (float)v.x, (float)v.y, (float)v.z })); } } - const auto eps = 1e-10f; + const auto eps = 1e-10f; quick_hull qh{ dim, eps }; qh.add_points(std::cbegin(P), std::cend(P)); auto initial_simplex = qh.get_affine_basis(); @@ -311,13 +311,13 @@ void D3_SurfaceFeature::build_surface (LR & r) { const auto & V = f.vertices_; auto ax = (*V[0])[0], ay = (*V[0])[1], az = (*V[0])[2]; - auto bx = (*V[1])[0], by = (*V[1])[1], bz = (*V[1])[2]; + auto bx = (*V[1])[0], by = (*V[1])[1], bz = (*V[1])[2]; auto cx = (*V[2])[0], cy = (*V[2])[1], cz = (*V[2])[2]; - float a[3] = { ax, ay, az }, b[3] = { bx, by, bz }, c[3] = {cx, cy, cz}; - Simplex3 s(a, b, c); - hull_complex.push_back(s); - } -} + float a[3] = { ax, ay, az }, b[3] = { bx, by, bz }, c[3] = {cx, cy, cz}; + Simplex3 s(a, b, c); + hull_complex.push_back(s); + } +} void D3_SurfaceFeature::calculate (LR& r, const Fsettings& s) { @@ -392,48 +392,48 @@ void D3_SurfaceFeature::calculate (LR& r, const Fsettings& s) r.contours_3D.push_back (K); } - // surface area: count exposed voxel faces in the 6-neighborhood - struct VoxelKey - { - StatsInt x, y, z; - bool operator==(const VoxelKey& other) const - { - return x == other.x && y == other.y && z == other.z; - } - }; - struct VoxelKeyHash - { - std::size_t operator()(const VoxelKey& key) const noexcept - { - std::size_t h = std::hash{}(key.x); - h ^= std::hash{}(key.y) + 0x9e3779b9 + (h << 6) + (h >> 2); - h ^= std::hash{}(key.z) + 0x9e3779b9 + (h << 6) + (h >> 2); - return h; - } - }; - std::unordered_set voxels; - voxels.reserve(r.raw_pixels_3D.size() * 2); - for (const auto& vox : r.raw_pixels_3D) - voxels.insert({ vox.x, vox.y, vox.z }); - - static constexpr StatsInt nbr[6][3] = { - { 1, 0, 0 }, { -1, 0, 0 }, - { 0, 1, 0 }, { 0, -1, 0 }, - { 0, 0, 1 }, { 0, 0, -1 } - }; - - fval_AREA = 0.0; - for (const auto& vox : r.raw_pixels_3D) - { - for (const auto& d : nbr) - { - if (voxels.find({ vox.x + d[0], vox.y + d[1], vox.z + d[2] }) == voxels.end()) - fval_AREA += 1.0; - } - } - - // -- build the hull complex - build_surface (r); + // surface area: count exposed voxel faces in the 6-neighborhood + struct VoxelKey + { + StatsInt x, y, z; + bool operator==(const VoxelKey& other) const + { + return x == other.x && y == other.y && z == other.z; + } + }; + struct VoxelKeyHash + { + std::size_t operator()(const VoxelKey& key) const noexcept + { + std::size_t h = std::hash{}(key.x); + h ^= std::hash{}(key.y) + 0x9e3779b9 + (h << 6) + (h >> 2); + h ^= std::hash{}(key.z) + 0x9e3779b9 + (h << 6) + (h >> 2); + return h; + } + }; + std::unordered_set voxels; + voxels.reserve(r.raw_pixels_3D.size() * 2); + for (const auto& vox : r.raw_pixels_3D) + voxels.insert({ vox.x, vox.y, vox.z }); + + static constexpr StatsInt nbr[6][3] = { + { 1, 0, 0 }, { -1, 0, 0 }, + { 0, 1, 0 }, { 0, -1, 0 }, + { 0, 0, 1 }, { 0, 0, -1 } + }; + + fval_AREA = 0.0; + for (const auto& vox : r.raw_pixels_3D) + { + for (const auto& d : nbr) + { + if (voxels.find({ vox.x + d[0], vox.y + d[1], vox.z + d[2] }) == voxels.end()) + fval_AREA += 1.0; + } + } + + // -- build the hull complex + build_surface (r); // convex hull volume @@ -495,11 +495,15 @@ void D3_SurfaceFeature::calculate (LR& r, const Fsettings& s) double L[3]; if (Nyxus::calc_eigvals(L, K)) { - fval_MAJOR_AXIS_LEN = 4.0 * sqrt(L[1]); - fval_MINOR_AXIS_LEN = 4.0 * sqrt(L[2]); - fval_LEAST_AXIS_LEN = 4.0 * sqrt(L[0]); - fval_ELONGATION = sqrt(L[2] / L[1]); - fval_FLATNESS = sqrt(L[0] / L[1]); + // FIX: calc_eigvals returns L sorted DESCENDING (L[0] largest). The axis lengths were indexed + // wrong (MAJOR<-L[1], MINOR<-L[2], LEAST<-L[0]), producing LEAST>MAJOR and FLATNESS>1 (both + // structurally impossible). Correct mapping: MAJOR<-L[0] (largest), MINOR<-L[1], LEAST<-L[2]; + // ELONGATION=MINOR/MAJOR=sqrt(L[1]/L[0]), FLATNESS=LEAST/MAJOR=sqrt(L[2]/L[0]). Matches MIRP/IBSI. + fval_MAJOR_AXIS_LEN = 4.0 * sqrt(L[0]); + fval_MINOR_AXIS_LEN = 4.0 * sqrt(L[1]); + fval_LEAST_AXIS_LEN = 4.0 * sqrt(L[2]); + fval_ELONGATION = sqrt(L[1] / L[0]); + fval_FLATNESS = sqrt(L[2] / L[0]); } else { diff --git a/src/nyx/features/caliper.h b/src/nyx/features/caliper.h index 590d10352..a99198c16 100644 --- a/src/nyx/features/caliper.h +++ b/src/nyx/features/caliper.h @@ -43,7 +43,6 @@ class CaliperNassensteinFeature : public FeatureMethod // Implementation constant const float rot_angle_increment = 10.f; // degrees - const int n_steps = 10; }; class CaliperFeretFeature : public FeatureMethod @@ -91,7 +90,6 @@ class CaliperFeretFeature : public FeatureMethod // Implementation constant const double rot_angle_increment = 10.f; // degrees - const int n_steps = 10; }; class CaliperMartinFeature : public FeatureMethod @@ -129,6 +127,5 @@ class CaliperMartinFeature : public FeatureMethod // Implementation constant const float rot_angle_increment = 10.f; // degrees - const int n_steps = 10; }; diff --git a/src/nyx/features/caliper_feret.cpp b/src/nyx/features/caliper_feret.cpp index 740a1f2ef..f4b7595ec 100644 --- a/src/nyx/features/caliper_feret.cpp +++ b/src/nyx/features/caliper_feret.cpp @@ -1,3 +1,4 @@ +#include // FIX (caliper float-precision): std::min/std::max for the float-hull X-extent #include "caliper.h" #include "../environment.h" #include "../helpers/helpers.h" @@ -79,7 +80,7 @@ void CaliperFeretFeature::save_value(std::vector>& fvals) void CaliperFeretFeature::calculate_angled_caliper_measurements (const std::vector& convex_hull, std::vector& angles, std::vector& ferets) { // Rotated convex hull - std::vector CH_rot; + std::vector CH_rot; // FIX (caliper float-precision): float-precision rotated hull (was integer Pixel2, truncated inward) CH_rot.reserve (convex_hull.size()); // Rotate and calculate the diameter @@ -87,8 +88,10 @@ void CaliperFeretFeature::calculate_angled_caliper_measurements (const std::vect ferets.clear(); for (float theta = 0.f; theta <= 180.f; theta += rot_angle_increment) { - Rotation::rotate_around_center (convex_hull, theta, CH_rot); - auto [minX, minY, maxX, maxY] = AABB::from_pixelcloud (CH_rot); + Rotation::rotate_around_center_fp (convex_hull, theta, CH_rot); // FIX (caliper float-precision): no integer truncation + // FIX (caliper float-precision): min/max X directly over the float hull (AABB::from_pixelcloud takes Pixel2) + double minX = CH_rot[0].x, maxX = CH_rot[0].x; + for (auto& p : CH_rot) { minX = std::min(minX, (double)p.x); maxX = std::max(maxX, (double)p.x); } // Save a caliper measurement orthogonal to X double feret = maxX - minX; diff --git a/src/nyx/features/caliper_martin.cpp b/src/nyx/features/caliper_martin.cpp index 03f901d69..16a067ed0 100644 --- a/src/nyx/features/caliper_martin.cpp +++ b/src/nyx/features/caliper_martin.cpp @@ -1,9 +1,47 @@ +#include // FIX (caliper reimpl): std::min/std::max for the analytic chord math +#include #include "caliper.h" #include "../environment.h" #include "rotation.h" using namespace Nyxus; +// FIX (caliper reimpl): width of the convex hull at a horizontal cut y = span of the +// x-coordinates where the line y intersects the hull edges. The hull (convHull_CH) is +// stored OPEN (no duplicated closing vertex), so we must include the wrap-around edge +// last->first — the previous implementation omitted it. Inclusive edge test + min/max of +// the intersection x is robust to a line passing exactly through a shared vertex. +// FIX (caliper float-precision): operate on Point2f (float-precision rotated hull) so the chord width is not +// quantized by the old integer-Pixel2 truncation. +static double hull_width_at_y (const std::vector& poly, double y) +{ + bool have = false; + double xlo = 0.0, xhi = 0.0; + size_t n = poly.size(); + for (size_t i = 0; i < n; i++) + { + const Point2f& a = poly[i]; + const Point2f& b = poly[(i + 1) % n]; + double ay = a.y, by = b.y, lo = std::min(ay, by), hi = std::max(ay, by); + if (y < lo || y > hi) + continue; + double e0, e1; + if (by != ay) + { + double x = a.x + (b.x - a.x) * (y - ay) / (by - ay); + e0 = e1 = x; + } + else // horizontal edge lying on the cut: it spans [min x, max x] + { + e0 = std::min ((double)a.x, (double)b.x); + e1 = std::max ((double)a.x, (double)b.x); + } + if (!have) { xlo = e0; xhi = e1; have = true; } + else { xlo = std::min (xlo, e0); xhi = std::max (xhi, e1); } + } + return have ? (xhi - xlo) : 0.0; +} + CaliperMartinFeature::CaliperMartinFeature() : FeatureMethod("CaliperMartinFeature") { // Letting the feature dependency manager know @@ -50,169 +88,60 @@ void CaliperMartinFeature::save_value(std::vector>& fvals) void CaliperMartinFeature::calculate_imp(const std::vector& convex_hull, std::vector& all_D) { - // Rotated convex hull - std::vector CH_rot; + // FIX (caliper reimpl): the previous code was NOT the Martin diameter at all — it pushed + // BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so the per-angle + // shortest (a spurious near-apex ~0-length chord) corrupted MIN/MEAN/MEDIAN/MODE/STDDEV. + // The Martin diameter (Pahl/Rumpf 1973; imea reference) is a SINGLE chord per angle: the + // horizontal chord at the level that bisects the projected area (50%/50%). We reproduce that + // analytically on the rotated convex hull — one diameter per rotation angle. + std::vector CH_rot; // FIX (caliper float-precision): float-precision rotated hull (was integer Pixel2) CH_rot.reserve(convex_hull.size()); - // Rotate and calculate the diameter all_D.clear(); + const int NGRID = 100; // FIX: fine Y-grid for the area integral (converges well before this; see morph_oracle/caliper_proto.py) for (float theta = 0.f; theta < 180.f; theta += rot_angle_increment) { - Rotation::rotate_around_center(convex_hull, theta, CH_rot); - auto [minX, minY, maxX, maxY] = AABB::from_pixelcloud(CH_rot); - - std::vector DA; // Diameters at this angle + Rotation::rotate_around_center_fp(convex_hull, theta, CH_rot); // FIX (caliper float-precision): no integer truncation + // FIX (caliper float-precision): min/max Y directly over the float hull (AABB::from_pixelcloud takes Pixel2) + double minY = CH_rot[0].y, maxY = CH_rot[0].y; + for (auto& p : CH_rot) { minY = std::min(minY, (double)p.y); maxY = std::max(maxY, (double)p.y); } + if (maxY <= minY) // FIX: degenerate (collinear) hull at this angle — skip + continue; - // Iterate the Y-grid - float stepY = (maxY - minY) / float(n_steps); - for (int iy = 1; iy <= n_steps; iy++) + // FIX: sample the hull width at NGRID horizontal levels (midpoint rule) and integrate area + double stepY = (double(maxY) - double(minY)) / NGRID; + std::vector widths(NGRID); + double total = 0.0; + for (int i = 0; i < NGRID; i++) { - float chord_y = minY + iy * stepY; - - // Find convex hull segments intersecting 'y' - std::vector> X; // intersection points - for (int iH = 1; iH < CH_rot.size(); iH++) - { - // The convex hull points are guaranteed to be consecutive - auto& a = CH_rot[iH - 1], - & b = CH_rot[iH]; - - // Chord's Y is between segment AB's Ys ? - if ((a.y >= chord_y && b.y < chord_y) || (b.y >= chord_y && a.y < chord_y)) - { - float chord_x = b.y != a.y ? - (b.x - a.x) * (chord_y - a.y) / (b.y - a.y) + a.x - : (b.x + a.x) / 2; - auto tup1 = std::make_pair(chord_x, chord_y); - X.push_back (tup1); - - // find the 2nd intersection - for (int kH = iH+1; kH < CH_rot.size(); kH++) - { - auto & a = CH_rot [kH-1], - & b = CH_rot [kH]; - - // chord chord_y crosses hull's segment (a,b) ? - if ((a.y >= chord_y && b.y < chord_y) || (b.y >= chord_y && a.y < chord_y)) - { - float chord_x = b.y != a.y ? - (b.x - a.x) * (chord_y - a.y) / (b.y - a.y) + a.x - : (b.x + a.x) / 2; - auto tup2 = std::make_pair(chord_x, chord_y); - - // save point #2 only if it's different from #1, otherwise the chord's length will be ==0 - // (#1==#2 happens when chord's Y is exactly the Y of the contact of 2 convex hull segments.) - if (tup1 != tup2) - X.push_back (tup2); - - break; // done with chord point #2 - } - } - - break; // done with chord point #1 - } - } - - // Save the length of this chord. There must be 2 items in 'chordEnds' because we don't allow uniformative chords of zero length - if (X.size() == 2) // we ignore special 1-vertex segments because their lengths are ==0 - { - // for N segments - auto compareFunc = [](const std::pair& p1, const std::pair& p2) { return p1.first < p2.first; }; - auto idx_minX = std::distance(X.begin(), std::min_element(X.begin(), X.end(), compareFunc)); - auto idx_maxX = std::distance(X.begin(), std::max_element(X.begin(), X.end(), compareFunc)); - // left X and right X segments - auto& e1 = X[idx_minX], & e2 = X[idx_maxX]; - auto x1 = e1.first, y1 = e1.second, x2 = e2.first, y2 = e2.second; - // save this chord - auto dist = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); // Squared distance - DA.push_back(dist); - } + double y = double(minY) + (i + 0.5) * stepY; + widths[i] = hull_width_at_y(CH_rot, y); + total += widths[i]; } + if (total <= 0.0) // FIX: empty cut set — skip + continue; - if (DA.size() > 0) + // FIX: walk the cumulative area from one side; the width at the 50% level is the Martin diameter + double half = 0.5 * total, cum = 0.0, martin = widths[NGRID - 1]; + for (int i = 0; i < NGRID; i++) { - // Find the shortest and longest chords (diameters) - double minD2 = *std::min_element(DA.begin(), DA.end()), - maxD2 = *std::max_element(DA.begin(), DA.end()), - min_ = sqrt(minD2), - max_ = sqrt(maxD2); - - // Save them - all_D.push_back(min_); - all_D.push_back(max_); + cum += widths[i]; + if (cum >= half) + { + martin = widths[i]; + break; + } } + all_D.push_back(martin); // FIX: one Martin diameter per angle (was two spurious values) } } void CaliperMartinFeature::osized_calculate (LR& r, const Fsettings& settings, ImageLoader&) { - // Rotated convex hull - std::vector CH_rot; - CH_rot.reserve(r.convHull_CH.size()); - - // Rotate and calculate the diameter + // FIX (caliper reimpl): the out-of-RAM path duplicated the same defective min+max chord logic. + // Route it through the corrected area-bisecting calculate_imp so both paths agree. std::vector all_D; - for (float theta = 0.f; theta < 180.f; theta += rot_angle_increment) - { - Rotation::rotate_around_center(r.convHull_CH, theta, CH_rot); - auto [minX, minY, maxX, maxY] = AABB::from_pixelcloud(CH_rot); - - std::vector DA; // Diameters at this angle - - // Iterate y-grid - float stepY = (maxY - minY) / float(n_steps); - for (int iy = 1; iy <= n_steps; iy++) - { - float chord_y = minY + iy * stepY; - - // Find convex hull segments intersecting 'y' - std::vector> X; // intersection points - for (int iH = 1; iH < CH_rot.size(); iH++) - { - // The convex hull points are guaranteed to be consecutive - auto& a = CH_rot[iH - 1], - & b = CH_rot[iH]; - - // Chord's Y is between segment AB's Ys ? - if ((a.y >= chord_y && b.y <= chord_y) || (b.y >= chord_y && a.y <= chord_y)) - { - auto chord_x = b.y != a.y ? - (b.x - a.x) * (chord_y - a.y) / (b.y - a.y) + a.x - : (b.y + a.y) / 2; - auto tup = std::make_pair(chord_x, chord_y); - X.push_back(tup); - } - } - - // Save the length of this chord. There must be 2 items in 'chordEnds' because we don't allow uniformative chords of zero length - if (X.size() >= 2) - { - // for N segments - auto compareFunc = [](const std::pair& p1, const std::pair& p2) { return p1.first < p2.first; }; - auto idx_minX = std::distance(X.begin(), std::min_element(X.begin(), X.end(), compareFunc)); - auto idx_maxX = std::distance(X.begin(), std::max_element(X.begin(), X.end(), compareFunc)); - // left X and right X segments - auto& e1 = X[idx_minX], & e2 = X[idx_maxX]; - auto x1 = e1.first, y1 = e1.second, x2 = e2.first, y2 = e2.second; - // save this chord - auto dist = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); // Squared distance - DA.push_back(dist); - } - } - - if (DA.size() > 0) - { - // Find the shortest and longest chords (diameters) - double minD2 = *std::min_element(DA.begin(), DA.end()), - maxD2 = *std::max_element(DA.begin(), DA.end()), - min_ = sqrt(minD2), - max_ = sqrt(maxD2); - - // Save them - all_D.push_back(min_); - all_D.push_back(max_); - } - } + calculate_imp(r.convHull_CH, all_D); // Process the stats auto s = ComputeCommonStatistics2(all_D); diff --git a/src/nyx/features/caliper_nassenstein.cpp b/src/nyx/features/caliper_nassenstein.cpp index b42b1a01b..9df7925f1 100644 --- a/src/nyx/features/caliper_nassenstein.cpp +++ b/src/nyx/features/caliper_nassenstein.cpp @@ -1,9 +1,47 @@ +#include // FIX (caliper reimpl): std::min/std::max used by the analytic chord helper +#include // FIX (caliper reimpl): std::abs +#include #include "caliper.h" #include "../environment.h" #include "rotation.h" using namespace Nyxus; +// FIX (caliper reimpl): height of the convex hull at a vertical cut x = span of the +// y-coordinates where the line x intersects the hull edges. Hull is stored OPEN, so the +// wrap-around edge (last->first) is included. Inclusive edge test + min/max is robust to a +// line passing exactly through a shared vertex (the Nassenstein contact column is a vertex). +// FIX (caliper float-precision): operate on Point2f (float-precision rotated hull) so the chord height is not +// quantized by the old integer-Pixel2 truncation. +static double hull_height_at_x (const std::vector& poly, double x) +{ + bool have = false; + double ylo = 0.0, yhi = 0.0; + size_t n = poly.size(); + for (size_t i = 0; i < n; i++) + { + const Point2f& a = poly[i]; + const Point2f& b = poly[(i + 1) % n]; + double ax = a.x, bx = b.x, lo = std::min(ax, bx), hi = std::max(ax, bx); + if (x < lo || x > hi) + continue; + double e0, e1; + if (bx != ax) + { + double yv = a.y + (b.y - a.y) * (x - ax) / (bx - ax); + e0 = e1 = yv; + } + else // vertical edge lying on the cut: it spans [min y, max y] + { + e0 = std::min ((double)a.y, (double)b.y); + e1 = std::max ((double)a.y, (double)b.y); + } + if (!have) { ylo = e0; yhi = e1; have = true; } + else { ylo = std::min (ylo, e0); yhi = std::max (yhi, e1); } + } + return have ? (yhi - ylo) : 0.0; +} + CaliperNassensteinFeature::CaliperNassensteinFeature() : FeatureMethod("CaliperNassensteinFeature") { // Letting the feature dependency manager know @@ -50,144 +88,49 @@ void CaliperNassensteinFeature::save_value (std::vector>& fv void CaliperNassensteinFeature::calculate_imp (const std::vector& convex_hull, std::vector& all_D) { - // Rotated convex hull - std::vector CH_rot; + // FIX (caliper reimpl): the previous code was byte-identical to the (also wrong) Martin loop — + // two diameters cannot share one algorithm. It pushed both the shortest and longest horizontal + // chord per angle, so the per-angle shortest (a near-apex ~0-length chord) drove MIN and MODE to + // 0.0 — impossible for a solid shape. The Nassenstein diameter (Pahl/Rumpf 1973; imea reference) + // is a SINGLE chord per angle: the vertical chord measured at the bottom-tangent contact column. + // We reproduce that on the rotated convex hull: the contact is the extreme (max-y) vertex/edge, + // and the diameter is the hull's vertical extent at that contact column. + std::vector CH_rot; // FIX (caliper float-precision): float-precision rotated hull (was integer Pixel2) CH_rot.reserve(convex_hull.size()); - // Rotate and calculate the diameter all_D.clear(); for (float theta = 0.f; theta < 180.f; theta += rot_angle_increment) { - Rotation::rotate_around_center(convex_hull, theta, CH_rot); - auto [minX, minY, maxX, maxY] = AABB::from_pixelcloud(CH_rot); - - std::vector DA; // Diameters at this angle - - // Iterate y-grid - float stepY = (maxY - minY) / float(n_steps); - for (int iy = 1; iy <= n_steps; iy++) - { - float chord_y = minY + iy * stepY; - - // Find convex hull segments intersecting 'y' - std::vector> X; // intersection points - for (int iH = 1; iH < CH_rot.size(); iH++) - { - // The convex hull points are guaranteed to be consecutive - auto& a = CH_rot[iH - 1], - & b = CH_rot[iH]; - - // Chord's Y is between segment AB's Ys ? - if ((a.y >= chord_y && b.y <= chord_y) || (b.y >= chord_y && a.y <= chord_y)) - { - auto chord_x = b.y != a.y ? - (b.x - a.x) * (chord_y - a.y) / (b.y - a.y) + a.x - : (b.y + a.y) / 2; - auto tup = std::make_pair(chord_x, chord_y); - X.push_back(tup); - } - } + Rotation::rotate_around_center_fp(convex_hull, theta, CH_rot); // FIX (caliper float-precision): no integer truncation + if (CH_rot.size() < 3) // FIX: degenerate hull — no measurable tangent chord + continue; - // Save the length of this chord. There must be 2 items in 'chordEnds' because we don't allow uniformative chords of zero length - if (X.size() >= 2) + // FIX: bottom tangent = the max-y extreme; the contact column is the mean x of the max-y + // vertices (a single vertex generically, or the midpoint of a flat bottom edge) + double ymax = CH_rot[0].y; + for (auto& p : CH_rot) + ymax = std::max (ymax, (double)p.y); + double xsum = 0.0; + int cnt = 0; + for (auto& p : CH_rot) + if (std::abs((double)p.y - ymax) < 1e-3) { - // for N segments - auto compareFunc = [](const std::pair& p1, const std::pair& p2) { return p1.first < p2.first; }; - auto idx_minX = std::distance(X.begin(), std::min_element(X.begin(), X.end(), compareFunc)); - auto idx_maxX = std::distance(X.begin(), std::max_element(X.begin(), X.end(), compareFunc)); - // left X and right X segments - auto& e1 = X[idx_minX], & e2 = X[idx_maxX]; - auto x1 = e1.first, y1 = e1.second, x2 = e2.first, y2 = e2.second; - // save this chord - auto dist = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); // Squared distance - DA.push_back(dist); + xsum += p.x; + cnt++; } - } + double xc = xsum / std::max(cnt, 1); - if (DA.size() > 0) - { - // Find the shortest and longest chords (diameters) - double minD2 = *std::min_element(DA.begin(), DA.end()), - maxD2 = *std::max_element(DA.begin(), DA.end()), - min_ = sqrt(minD2), - max_ = sqrt(maxD2); - - // Save them - all_D.push_back(min_); - all_D.push_back(max_); - } + // FIX: Nassenstein diameter = vertical extent of the hull at the contact column + all_D.push_back (hull_height_at_x(CH_rot, xc)); } } void CaliperNassensteinFeature::osized_calculate (LR& r, const Fsettings& settings, ImageLoader&) { - // Rotated convex hull - std::vector CH_rot; - CH_rot.reserve (r.convHull_CH.size()); - - // Rotate and calculate the diameter + // FIX (caliper reimpl): the out-of-RAM path duplicated the same defective min+max chord logic. + // Route it through the corrected bottom-tangent calculate_imp so both paths agree. std::vector all_D; - for (float theta = 0.f; theta < 180.f; theta += rot_angle_increment) - { - Rotation::rotate_around_center(r.convHull_CH, theta, CH_rot); - auto [minX, minY, maxX, maxY] = AABB::from_pixelcloud(CH_rot); - - std::vector DA; // Diameters at this angle - - // Iterate y-grid - float stepY = (maxY - minY) / float(n_steps); - for (int iy = 1; iy <= n_steps; iy++) - { - float chord_y = minY + iy * stepY; - - // Find convex hull segments intersecting 'y' - std::vector> X; // intersection points - for (int iH = 1; iH < CH_rot.size(); iH++) - { - // The convex hull points are guaranteed to be consecutive - auto& a = CH_rot[iH - 1], - & b = CH_rot[iH]; - - // Chord's Y is between segment AB's Ys ? - if ((a.y >= chord_y && b.y <= chord_y) || (b.y >= chord_y && a.y <= chord_y)) - { - auto chord_x = b.y != a.y ? - (b.x - a.x) * (chord_y - a.y) / (b.y - a.y) + a.x - : (b.y + a.y) / 2; - auto tup = std::make_pair(chord_x, chord_y); - X.push_back(tup); - } - } - - // Save the length of this chord. There must be 2 items in 'chordEnds' because we don't allow uniformative chords of zero length - if (X.size() >= 2) - { - // for N segments - auto compareFunc = [](const std::pair& p1, const std::pair& p2) { return p1.first < p2.first; }; - auto idx_minX = std::distance(X.begin(), std::min_element(X.begin(), X.end(), compareFunc)); - auto idx_maxX = std::distance(X.begin(), std::max_element(X.begin(), X.end(), compareFunc)); - // left X and right X segments - auto& e1 = X[idx_minX], & e2 = X[idx_maxX]; - auto x1 = e1.first, y1 = e1.second, x2 = e2.first, y2 = e2.second; - // save this chord - auto dist = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); // Squared distance - DA.push_back(dist); - } - } - - if (DA.size() > 0) - { - // Find the shortest and longest chords (diameters) - double minD2 = *std::min_element(DA.begin(), DA.end()), - maxD2 = *std::max_element(DA.begin(), DA.end()), - min_ = sqrt(minD2), - max_ = sqrt(maxD2); - - // Save them - all_D.push_back(min_); - all_D.push_back(max_); - } - } + calculate_imp(r.convHull_CH, all_D); // Process the stats auto s = ComputeCommonStatistics2 (all_D); diff --git a/src/nyx/features/glcm.cpp b/src/nyx/features/glcm.cpp index 521fc594d..2ee95d952 100644 --- a/src/nyx/features/glcm.cpp +++ b/src/nyx/features/glcm.cpp @@ -728,7 +728,12 @@ double GLCMFeature::f_entropy(const SimpleMatrix& P) for (j = 0; j < Ng; j++) for (i = 0; i < Ng; i++) - entropy += P.xy(i, j) * fast_log10(P.xy(i, j) + EPSILON) / LOG10_2; + { + // FIX: normalize by sum_p (joint probability), matching f_GLCM_JE. Was summing the + // UNNORMALIZED co-occurrence counts -> negative "entropy" (e.g. -60.7 on the IBSI phantom). + double p = P.xy(i, j) / sum_p; + entropy += p * fast_log10(p + EPSILON) / LOG10_2; + } return -entropy; } @@ -1059,7 +1064,9 @@ double GLCMFeature::f_GLCM_HOM2(const SimpleMatrix& P_matrix) for (int x = 0; x < n_levels; x++) for (int y = 0; y < n_levels; y++) - hom2 += P_matrix.xy(x, y) / (1.0 + (double)std::abs(x - y) * (double)std::abs(x - y)); + // FIX: normalize by sum_p (joint probability), matching f_idm/f_GLCM_JE. Was summing the + // UNNORMALIZED co-occurrence counts -> homogeneity > 1 (e.g. 14.79 on the IBSI phantom). + hom2 += (P_matrix.xy(x, y) / sum_p) / (1.0 + (double)std::abs(x - y) * (double)std::abs(x - y)); return hom2; } diff --git a/src/nyx/features/rotation.cpp b/src/nyx/features/rotation.cpp index f2b026edc..2b6c63f87 100644 --- a/src/nyx/features/rotation.cpp +++ b/src/nyx/features/rotation.cpp @@ -34,8 +34,41 @@ void Rotation::rotate_around_center( } } +void Rotation::rotate_around_center_fp ( + // in + const std::vector& P, + float angle_deg, + // out + std::vector& P_rot) +{ + P_rot.clear(); + + // Find the center + double cx = 0, cy = 0; + for (auto& p : P) + { + cx += p.x; + cy += p.y; + } + cx /= double(P.size()); + cy /= double(P.size()); + + // Rotate + float theta = angle_deg * float(M_PI) / 180.f; + double s = sin(theta), + c = cos(theta); + for (auto& p : P) + { + double x_rot = (p.x - cx) * c - (p.y - cy) * s + cx; + double y_rot = (p.y - cy) * c + (p.x - cx) * s + cy; + // FIX (caliper float-precision): store the rotated vertex in floating point (Point2f) instead of the + // integer Pixel2 that truncated it inward - the caliper chord math then integrates on exact coords. + P_rot.push_back (Point2f ((float)x_rot, (float)y_rot)); + } +} + void Rotation::rotate_cloud ( - // in + // in const std::vector& P, const double cx, const double cy, diff --git a/src/nyx/features/rotation.h b/src/nyx/features/rotation.h index faebf4b8a..5d0921715 100644 --- a/src/nyx/features/rotation.h +++ b/src/nyx/features/rotation.h @@ -10,12 +10,24 @@ class Rotation { public: static void rotate_around_center( - // [in] + // [in] const std::vector& P, float angle_deg, // [out] std::vector& P_rot); + // FIX (caliper float-precision): floating-point variant of rotate_around_center. The Pixel2 sink truncates + // each rotated vertex to integer (Point2), snapping the hull systematically inward every + // angle, which forced the loose 15% caliper-vs-imea tolerance the integer path needed. Emitting Point2f + // preserves the fractional rotated coordinates and lets the caliper tests tighten to 10% (subsumes both + // "round instead of truncate" and "float-precision hull rotation"). + static void rotate_around_center_fp( + // [in] + const std::vector& P, + float angle_deg, + // [out] + std::vector& P_rot); + static void rotate_cloud( // [in] const std::vector& P, diff --git a/src/nyx/gpu/geomoments_hu.cu b/src/nyx/gpu/geomoments_hu.cu index 58921f3a7..934ae2bc5 100644 --- a/src/nyx/gpu/geomoments_hu.cu +++ b/src/nyx/gpu/geomoments_hu.cu @@ -71,19 +71,19 @@ namespace NyxusGpu // (NormCentralMom(D, 3, 0) + NormCentralMom(D, 1, 2)) * // (pow(NormCentralMom(D, 3, 0) + NormCentralMom(D, 1, 2), 2) - 3 * pow(NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3), 2)) + // (3 * NormCentralMom(D, 2, 1) - NormCentralMom(D, 0, 3)) * (NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3)) * - // (pow(3 * (NormCentralMom(D, 3, 0) + NormCentralMom(D, 1, 2)), 2) - pow(NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3), 2)); + // (3 * pow(NormCentralMom(D, 3, 0) + NormCentralMom(D, 1, 2), 2) - pow(NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3), 2)); *h5 = (*nu30 - 3. * *nu12) * (*nu30 + *nu12) * (pow(*nu30 + *nu12, 2.0) - 3. * pow(*nu21 + *nu03, 2.0)) + (3. * *nu21 - *nu03) * (*nu21 + *nu03) * - (pow(3. * (*nu30 + *nu12), 2.0) - pow(*nu21 + *nu03, 2.0)); + (3. * pow(*nu30 + *nu12, 2.0) - pow(*nu21 + *nu03, 2.0)); // FIX: was pow(3*(nu30+nu12), 2) == 9*(eta30+eta12)^2; Hu's I5 second bracket is 3*(eta30+eta12)^2 - (eta21+eta03)^2 (GPU twin of the CPU fix) // Formula: double h6 = (NormCentralMom(D, 2, 0) - NormCentralMom(D, 0, 2)) * (pow(NormCentralMom(D, 3, 0) + NormCentralMom(D, 1, 2), 2) - - // pow(NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3), 2)) + (4 * NormCentralMom(D, 1, 1) * (NormCentralMom(D, 3, 0) + NormCentralMom(D, 1, 2)) * - // NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3)); + // pow(NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3), 2)) + 4 * NormCentralMom(D, 1, 1) * (NormCentralMom(D, 3, 0) + NormCentralMom(D, 1, 2)) * + // (NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3)); *h6 = (*nu20 - *nu02) * (pow(*nu30 + *nu12, 2.0) - - pow(*nu21 + *nu03, 2.0)) + (4. * *nu11 * (*nu30 + *nu12) * - *nu21 + *nu03); + pow(*nu21 + *nu03, 2.0)) + + 4. * *nu11 * (*nu30 + *nu12) * (*nu21 + *nu03); // FIX: was 4*nu11*(nu30+nu12)*nu21 + nu03 - precedence error left "+nu03" outside the product; Hu's I6 last term is 4*eta11*(eta30+eta12)*(eta21+eta03) (GPU twin of the CPU fix) // Formula: double h7 = (3 * NormCentralMom(D, 2, 1) - NormCentralMom(D, 0, 3)) * (NormCentralMom(D, 3, 0) + NormCentralMom(D, 1, 2)) * (pow(NormCentralMom(D, 3, 0) + NormCentralMom(D, 1, 2), 2) - // 3 * pow(NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3), 2)) - (NormCentralMom(D, 3, 0) - 3 * NormCentralMom(D, 1, 2)) * (NormCentralMom(D, 2, 1) + NormCentralMom(D, 0, 3)) * diff --git a/tests/test_3d_coverage_common.h b/tests/test_3d_coverage_common.h index 66cd705a8..bd54f0ddb 100644 --- a/tests/test_3d_coverage_common.h +++ b/tests/test_3d_coverage_common.h @@ -308,9 +308,9 @@ static const std::map>& unvetted_3d_local_regre { "3COMPACTNESS2", { 0.039449347281835329 } }, { "3COV", { 0.29486207043456802 } }, { "3COVERED_IMAGE_INTENSITY_RANGE", { 1.0002043207290587 } }, - { "3ELONGATION", { 0.80989034437672192 } }, + { "3ELONGATION", { 0.84332105599389762 } }, // FIX(axis mislabel): was 0.8099; now = MIRP morph_pca_elongation { "3EXCESS_KURTOSIS", { -1.2127631603215119 } }, - { "3FLATNESS", { 1.1857880138204875 } }, + { "3FLATNESS", { 0.68299758045903847 } }, // FIX(axis mislabel): was 1.186 (>1, impossible); now = MIRP morph_pca_flatness { "3GLCM_ACOR_AVE", { 896.29490954682888 } }, { "3GLCM_ASM_AVE", { 0.21714923037245615 } }, { "3GLCM_CLUPROM_AVE", { 5319702.1464416385 } }, @@ -325,11 +325,11 @@ static const std::map>& unvetted_3d_local_regre { "3GLCM_DIS_AVE", { 4.4826978263468824 } }, { "3GLCM_ENERGY", { 0.20619709037207257, 0.21210950671886142, 0.20619709037207257, 0.22347189327232553, 0.229768029906674, 0.22347189327232553, 0.20619709037207257, 0.21210950671886142, 0.20619709037207257, 0.21657166507454639, 0.22249608735556295, 0.21657166507454639, 0.2415813859599367 } }, { "3GLCM_ENERGY_AVE", { 0.21714923037245615 } }, - { "3GLCM_ENTROPY", { -6839097.7643487463, -7009967.2050323486, -6839097.7643487463, -7188531.59987259, -7365630.721654892, -7188531.59987259, -6839097.7643487463, -7009967.2050323486, -6839097.7643487463, -7049922.0657091141, -7222060.7506599426, -7049922.0657091141, -7731459.4186325073 } }, - { "3GLCM_ENTROPY_AVE", { -7090183.3607361866 } }, + { "3GLCM_ENTROPY", { 6.1375193094015223, 6.0251268596431471, 6.1375193094015223, 5.6356261936885357, 5.5229037686481623, 5.6356261936885357, 6.1375193094015223, 6.0251268596431471, 6.1375193094015223, 5.9202631504971848, 5.8126798565436344, 5.9202631504971848, 4.8177837873733909 } }, // FIX: were buggy unnormalized ~-6.8e6; post /sum_p fix (3d_glcm.cpp) + { "3GLCM_ENTROPY_AVE", { 5.8358059275253087 } }, // FIX: was buggy unnormalized -7.09e6; post /sum_p fix { "3GLCM_HOM1", { 0.63159286085358357, 0.67511464571418467, 0.63159286085358357, 0.675209797165082, 0.7178512712427948, 0.675209797165082, 0.63159286085358357, 0.67511464571418467, 0.63159286085358357, 0.65283716836970751, 0.69041197263012255, 0.65283716836970751, 0.77822827662266392 } }, { "3GLCM_HOM1_AVE", { 0.67070662972368189 } }, - { "3GLCM_HOM2", { 314305.05501034198, 345990.36653998197, 314305.05501034198, 340739.38462753344, 372698.80542099319, 340739.38462753344, 314305.05501034198, 345990.36653998197, 314305.05501034198, 329550.97066911863, 358264.30287407315, 329550.97066911863, 412615.52956223302 } }, + { "3GLCM_HOM2", { 0.59196175760722125, 0.64232872280698583, 0.59196175760722125, 0.63506326531940249, 0.68470533035896952, 0.63506326531940249, 0.59196175760722125, 0.64232872280698583, 0.59196175760722125, 0.61310667832992871, 0.65700403974706201, 0.61310667832992871, 0.75964624124070679 } }, // FIX: were buggy unnormalized ~3.1e5; post /sum_p fix (homogeneity now in [0,1]) { "3GLCM_IDMN_AVE", { 0.97393106857854816 } }, { "3GLCM_IDM_AVE", { 0.63463076728371071 } }, { "3GLCM_IDN_AVE", { 0.95491658377679667 } }, @@ -384,10 +384,10 @@ static const std::map>& unvetted_3d_local_regre { "3HYPERFLATNESS", { 3.8027657005973312 } }, { "3HYPERSKEWNESS", { 0.32001332615517414 } }, { "3INTEGRATED_INTENSITY", { 544286216 } }, - { "3LEAST_AXIS_LEN", { 104.70681271508683 } }, - { "3MAJOR_AXIS_LEN", { 88.301459868642283 } }, + { "3LEAST_AXIS_LEN", { 71.514499741981993 } }, // FIX(axis mislabel): was 104.7 (>MAJOR, impossible); now = MIRP morph_pca_least_axis + { "3MAJOR_AXIS_LEN", { 104.70681271508683 } }, // FIX(axis mislabel): was 88.3; now = MIRP morph_pca_maj_axis (largest) { "3MEDIAN_ABSOLUTE_DEVIATION", { 507.12380480410445 } }, - { "3MINOR_AXIS_LEN", { 71.514499741981993 } }, + { "3MINOR_AXIS_LEN", { 88.301459868642283 } }, // FIX(axis mislabel): was 71.5; now = MIRP morph_pca_min_axis { "3MODE", { 1279 } }, { "3NGLDM_DCENE", { 0.14348407632898436 } }, { "3NGLDM_DCENT", { 5.2277449211654039 } }, @@ -413,7 +413,7 @@ static const std::map>& unvetted_3d_local_regre { "3P75", { 2487.9072847682119 } }, { "3P99", { 3002.3047021943576 } }, { "3QCOD", { 0.25724851827174233 } }, - { "3ROBUST_MEAN", { 0 } }, + { "3ROBUST_MEAN", { 1977.5189642596645 } }, // FIX: baseline was pinning the bug value 0; 3ROBUST_MEAN is now computed (mean of voxels in [P10,P90]) ~ 3MEAN 1983.32, trimmed { "3SPHERICAL_DISPROPORTION", { 2.9375598657539634 } }, { "3SPHERICITY", { 0.34041859424142729 } }, { "3STANDARD_DEVIATION", { 584.80556406962933 } }, diff --git a/tests/test_3d_glcm_pyradiomics.h b/tests/test_3d_glcm_pyradiomics.h index fa1318997..8c4b3e45c 100644 --- a/tests/test_3d_glcm_pyradiomics.h +++ b/tests/test_3d_glcm_pyradiomics.h @@ -200,6 +200,71 @@ void test_compat_3glcm_feature (const Nyxus::Feature3D& expecting_fcode, const s ASSERT_TRUE(agrees_gt(atot, compat_d3glcm_GT[fname], 10.)); } +// Deep-dive: verify the 7 config-sensitive 3D GLCM features equal their already-vetted twins +// (numerically, same fixture/config), so they can be promoted by equivalence. Dumps calc_ave pairs. +void test_3dglcm_equivalence_dump() +{ + auto [ipath, mpath, label] = get_3d_compat_phantom(); + ASSERT_TRUE(fs::exists(ipath)); + ASSERT_TRUE(fs::exists(mpath)); + + Environment e; + e.dataset.dataset_props.reserve(1); + SlideProps& sp = e.dataset.dataset_props.emplace_back(ipath, mpath); + ASSERT_TRUE(scan_slide_props(sp, 3, e.anisoOptions, e.resultOptions.need_annotation())); + e.dataset.update_dataset_props_extrema(); + clear_slide_rois(e.uniqueLabels, e.roiData); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0, ipath, mpath, 0)); + std::vector batch = { label }; + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0)); + ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); + + Fsettings s; + s.resize((int)NyxSetting::__COUNT__); + s[(int)NyxSetting::SOFTNAN].rval = 0.0; + s[(int)NyxSetting::TINY].rval = 0.0; + s[(int)NyxSetting::SINGLEROI].bval = false; + s[(int)NyxSetting::GREYDEPTH].ival = 100; + s[(int)NyxSetting::PIXELSIZEUM].rval = 100; + s[(int)NyxSetting::PIXELDISTANCE].ival = 5; + s[(int)NyxSetting::USEGPU].bval = false; + s[(int)NyxSetting::VERBOSLVL].ival = 0; + s[(int)NyxSetting::IBSI].bval = false; + s[(int)NyxSetting::GLCM_GREYDEPTH].ival = -20; + s[(int)NyxSetting::GLCM_OFFSET].ival = 1; + s[(int)NyxSetting::GLCM_SPARSEINTENS].bval = true; + + LR& r = e.roiData[label]; + ASSERT_NO_THROW(r.initialize_fvals()); + D3_GLCM_feature f; + ASSERT_NO_THROW(f.calculate(r, s)); + f.save_value(r.fvals); + + using F = Nyxus::Feature3D; + struct Pair { const char* a; F fa; const char* b; F fb; }; + std::vector pairs = { + {"3GLCM_DIS", F::GLCM_DIS, "3GLCM_DIFAVE", F::GLCM_DIFAVE}, + {"3GLCM_ENERGY", F::GLCM_ENERGY, "3GLCM_ASM", F::GLCM_ASM}, + {"3GLCM_ENTROPY", F::GLCM_ENTROPY, "3GLCM_JE", F::GLCM_JE}, + {"3GLCM_HOM1", F::GLCM_HOM1, "3GLCM_ID", F::GLCM_ID}, + {"3GLCM_HOM2", F::GLCM_HOM2, "3GLCM_IDM", F::GLCM_IDM}, + {"3GLCM_SUMVARIANCE", F::GLCM_SUMVARIANCE, "3GLCM_CLUTEND", F::GLCM_CLUTEND}, + {"3GLCM_VARIANCE", F::GLCM_VARIANCE, "3GLCM_JVAR", F::GLCM_JVAR}, + }; + for (auto& p : pairs) + { + double va = f.calc_ave(r.fvals[(int)p.fa]); + double vb = f.calc_ave(r.fvals[(int)p.fb]); + double m = std::max(std::abs(va), std::abs(vb)); if (m < 1e-12) m = 1e-12; + double rel = std::abs(va - vb) / m; + std::cout << "[3DGLCM-EQ] " << p.a << "=" << va << " " << p.b << "=" << vb + << " rel=" << rel << (rel < 1e-6 ? " EQUAL" : " DIFFER") << "\n"; + // Config-sensitive 3D GLCM features are numerically identical to their pyradiomics-vetted + // twins (also guards the HOM2/ENTROPY /sum_p fix: pre-fix ENTROPY!=JE, HOM2!=IDM). + EXPECT_NEAR(va, vb, std::abs(vb) * 1e-6 + 1e-9) << p.a << " != " << p.b; + } +} + void test_compat_3glcm_ACOR() { test_compat_3glcm_feature (Nyxus::Feature3D::GLCM_ACOR, "3GLCM_ACOR"); diff --git a/tests/test_3d_glrlm_pyradiomics.h b/tests/test_3d_glrlm_pyradiomics.h index b1321f360..922c69a4a 100644 --- a/tests/test_3d_glrlm_pyradiomics.h +++ b/tests/test_3d_glrlm_pyradiomics.h @@ -212,6 +212,66 @@ void test_compat_3glrlm_feature(const Nyxus::Feature3D& expecting_fcode, const s ASSERT_TRUE(agrees_gt(atot, compat_3glrlm_GT[fname], 10.)); } +// Vet the direction-averaged (_AVE) 3D GLRLM features vs PyRadiomics. save_value stores +// fvals[X_AVE][0] = calc_ave(angled_X) -- exactly the quantity the base test asserts == PyRadiomics +// (test_compat_3glrlm_feature: atot = calc_ave(fvals[X])). So reading the _AVE slot directly and +// comparing to the same GT table is a direct PyRadiomics assertion on the _AVE feature. One workflow +// run covers all 14 (RE_AVE / RP_AVE already vetted elsewhere). +void test_compat_3glrlm_ave_features() +{ + auto [ipath, mpath, label] = get_3d_compat_phantom(); + ASSERT_TRUE(fs::exists(ipath)); + ASSERT_TRUE(fs::exists(mpath)); + + Environment e; + e.dataset.dataset_props.reserve(1); + SlideProps& sp = e.dataset.dataset_props.emplace_back(ipath, mpath); + ASSERT_TRUE(scan_slide_props(sp, 3, e.anisoOptions, e.resultOptions.need_annotation())); + e.dataset.update_dataset_props_extrema(); + clear_slide_rois(e.uniqueLabels, e.roiData); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0, ipath, mpath, 0)); + std::vector batch = { label }; + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0)); + ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); + + Fsettings s; + s.resize((int)NyxSetting::__COUNT__); + s[(int)NyxSetting::SOFTNAN].rval = 0.0; + s[(int)NyxSetting::TINY].rval = 0.0; + s[(int)NyxSetting::SINGLEROI].bval = false; + s[(int)NyxSetting::GREYDEPTH].ival = 100; + s[(int)NyxSetting::PIXELSIZEUM].rval = 100; + s[(int)NyxSetting::PIXELDISTANCE].ival = 5; + s[(int)NyxSetting::USEGPU].bval = false; + s[(int)NyxSetting::VERBOSLVL].ival = 0; + s[(int)NyxSetting::IBSI].bval = false; + s[(int)NyxSetting::GLRLM_GREYDEPTH].ival = -20; // radiomics binCount-based grey binning + + LR& r = e.roiData[label]; + ASSERT_NO_THROW(r.initialize_fvals()); + D3_GLRLM_feature f; + ASSERT_NO_THROW(f.calculate(r, s)); + f.save_value(r.fvals); + + using F = Nyxus::Feature3D; + struct AvePair { F ave; const char* gt; }; + std::vector aves = { + {F::GLRLM_GLN_AVE, "3GLRLM_GLN"}, {F::GLRLM_GLNN_AVE, "3GLRLM_GLNN"}, + {F::GLRLM_GLV_AVE, "3GLRLM_GLV"}, {F::GLRLM_HGLRE_AVE, "3GLRLM_HGLRE"}, + {F::GLRLM_LGLRE_AVE, "3GLRLM_LGLRE"}, {F::GLRLM_LRE_AVE, "3GLRLM_LRE"}, + {F::GLRLM_LRHGLE_AVE, "3GLRLM_LRHGLE"}, {F::GLRLM_LRLGLE_AVE, "3GLRLM_LRLGLE"}, + {F::GLRLM_RLN_AVE, "3GLRLM_RLN"}, {F::GLRLM_RLNN_AVE, "3GLRLM_RLNN"}, + {F::GLRLM_RV_AVE, "3GLRLM_RV"}, {F::GLRLM_SRE_AVE, "3GLRLM_SRE"}, + {F::GLRLM_SRHGLE_AVE, "3GLRLM_SRHGLE"}, {F::GLRLM_SRLGLE_AVE, "3GLRLM_SRLGLE"}, + }; + for (auto& a : aves) + { + double v = r.fvals[(int)a.ave][0]; + ASSERT_TRUE(agrees_gt(v, compat_3glrlm_GT[a.gt], 10.)) << a.gt << "_AVE = " << v + << " vs pyradiomics " << compat_3glrlm_GT[a.gt]; + } +} + void test_compat_3GLRLM_GLN() { test_compat_3glrlm_feature (Nyxus::Feature3D::GLRLM_GLN, "3GLRLM_GLN"); } diff --git a/tests/test_3d_ngldm_ibsi.h b/tests/test_3d_ngldm_regression.h similarity index 68% rename from tests/test_3d_ngldm_ibsi.h rename to tests/test_3d_ngldm_regression.h index 75a3340be..217e1d3ac 100644 --- a/tests/test_3d_ngldm_ibsi.h +++ b/tests/test_3d_ngldm_regression.h @@ -7,6 +7,43 @@ #include "../src/nyx/roi_cache.h" #include "../src/nyx/features/3d_ngldm.h" +// REGRESSION / drift-guard tests -- NOT an oracle. The `d3ngldm_GT` table below has NO external +// provenance and its numbers are NOT verified against any reference; the tests only pin current output. +// +// This file was renamed from test_3d_ngldm_ibsi.h to test_3d_ngldm_regression.h: SPEC §2 reserves an +// oracle suffix like `_ibsi` for a genuine, provenance-carrying oracle, and these values are neither. +// They have no recorded tool/version/config (SPEC 6.4 requires provenance for any oracle golden) and +// are evaluated on the Nyxus coverage phantom tests/data/nifti/phantoms/ut_inten.nii + ut_mask57.nii +// -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to them in the first place. +// (Contrast test_ngldm_ibsi.h, the 2D file, which DOES run on the IBSI digital phantom and cites IBSI +// manual page numbers per value -- that one is a real IBSI oracle. The `_ibsi` suffix is now free for a +// genuine 3D IBSI-phantom NGLDM oracle when one is written.) +// +// Verified 2026-07 with an independent MIRP NGLDM run on the SAME phantom at the SAME discretisation +// (fixed_bin_number n=64, 3D). MIRP disagrees with every comparable value, several by an order of +// magnitude: +// feature this table MIRP +// LDE 0.1 0.2559 +// HDE 261 28.07 +// LGLCE 0.00036 0.0322 +// HGLCE 740 1324 +// GLNU 115443 4350.3 +// GLNUN 0.23 0.01585 +// DCNU 115443 40745.0 +// DCNUN 0.23 0.14847 +// GLV 190 350.17 +// DCV 86.17 11.948 +// DCENT 5.23 8.676 +// DCENE 0.14 0.002875 +// Harness: morph_oracle/mirp_ngldm_3d.py (offline; CI never runs it). +// +// => Treat the tests below as REGRESSION / drift guards only. Do NOT promote features in +// oracle_coverage.csv to status=vetted on the strength of them passing. Promoting requires a +// documented, config-matched external oracle (MIRP is the candidate for 3D NGLDM). +// +// Also note 3NGLDM_GLM (grey level mean) and 3NGLDM_DCM (dependence count mean) have no counterpart +// anywhere: MIRP's NGLDM emits no gl_mean / dc_mean column, and the 2D table in test_ngldm_ibsi.h +// explicitly marks GLM "--not in IBSI--". No external oracle exists for those two. static std::unordered_map d3ngldm_GT{ { "3NGLDM_LDE", 0.1 }, { "3NGLDM_HDE", 261.0 }, diff --git a/tests/test_all.cc b/tests/test_all.cc index f8148e15e..e7ced9fd9 100644 --- a/tests/test_all.cc +++ b/tests/test_all.cc @@ -44,7 +44,7 @@ #include "test_3d_morphology_regression.h" #include "test_3d_morphology_matlab.h" #include "test_3d_gldzm_ibsi.h" -#include "test_3d_ngldm_ibsi.h" +#include "test_3d_ngldm_regression.h" #include "test_3d_firstorder_pyradiomics.h" #include "test_3d_glcm_pyradiomics.h" #include "test_3d_gldm_pyradiomics.h" @@ -259,6 +259,10 @@ TEST(TEST_NYXUS, TEST_COMPAT_3GLRLM_SRLGLE) { ASSERT_NO_THROW(test_compat_3GLRLM_SRLGLE()); } +TEST(TEST_NYXUS, TEST_COMPAT_3GLRLM_AVE_FEATURES) { + ASSERT_NO_THROW(test_compat_3glrlm_ave_features()); +} + //***** 3D GLSZM compatibility ***** @@ -391,6 +395,10 @@ TEST(TEST_NYXUS, TEST_COMPAT_3GLDM_SDLGLE) { //***** 3D GLCM compatibility ***** +TEST(TEST_NYXUS, TEST_3DGLCM_EQUIVALENCE_DUMP) { + ASSERT_NO_THROW(test_3dglcm_equivalence_dump()); +} + TEST(TEST_NYXUS, TEST_COMPAT_3GLCM_ACOR) { ASSERT_NO_THROW(test_compat_3glcm_ACOR()); } @@ -1044,6 +1052,11 @@ TEST(TEST_NYXUS, TEST_2D_SHAPE_GEOMETRIC_MOMENTS_VERIFIABLE_WITH_3P_BUILTIN_ORAC ASSERT_NO_THROW(test_2d_shape_geometric_moments_verifiable_with_3p_builtin_oracle()); } +TEST(TEST_NYXUS, TEST_MOMENTS_HU_WEDGE_SKIMAGE) +{ + ASSERT_NO_THROW(test_moments_hu_wedge_skimage()); +} + TEST(TEST_NYXUS, TEST_2D_SHAPE_GEOMETRIC_MOMENTS_UNVETTED_NO_DIRECT_ORACLE) { ASSERT_NO_THROW(test_2d_shape_geometric_moments_unvetted_no_direct_oracle()); @@ -1074,6 +1087,26 @@ TEST(TEST_NYXUS, TEST_REMAINING2D_VERIFIABLE_WITH_3P_BUILTIN_ORACLE_CALIPER_FEAT ASSERT_NO_THROW(test_remaining2d_verifiable_with_3p_builtin_oracle_caliper_features()); } +TEST(TEST_NYXUS, TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE) +{ + ASSERT_NO_THROW(test_shape2d_caliper_martin_nassenstein_imea_ellipse_oracle()); +} + +TEST(TEST_NYXUS, TEST_SHAPE2D_CALIPER_FERET_IMEA_ELLIPSE_ORACLE) +{ + ASSERT_NO_THROW(test_shape2d_caliper_feret_imea_ellipse_oracle()); +} + +TEST(TEST_NYXUS, TEST_SHAPE2D_MIN_ENCLOSING_CIRCLE_IMEA_ORACLE) +{ + ASSERT_NO_THROW(test_shape2d_min_enclosing_circle_imea_oracle()); +} + +TEST(TEST_NYXUS, TEST_SHAPE2D_DOCUMENTED_FORMULA_CONFORMANCE_NO_EXTERNAL_ORACLE) +{ + ASSERT_NO_THROW(test_shape2d_documented_formula_conformance_no_external_oracle()); +} + TEST(TEST_NYXUS, TEST_REMAINING2D_VERIFIABLE_WITH_3P_BUILTIN_ORACLE_CHORD_STAT_FEATURES) { ASSERT_NO_THROW(test_remaining2d_verifiable_with_3p_builtin_oracle_chord_stat_features()); @@ -1104,11 +1137,11 @@ TEST(TEST_NYXUS, TEST_NEIGHBORHOOD2D_COUNTS_TOUCHING) ASSERT_NO_THROW(test_neighborhood2d_counts_and_touching()); } -TEST(TEST_NYXUS, TEST_NEIGHBORHOOD2D_PERCENT_TOUCHING_ENCLOSED_ANALYTIC) -{ - ASSERT_NO_THROW(test_neighborhood2d_percent_touching_enclosed_analytic()); -} - +TEST(TEST_NYXUS, TEST_NEIGHBORHOOD2D_PERCENT_TOUCHING_ENCLOSED_ANALYTIC) +{ + ASSERT_NO_THROW(test_neighborhood2d_percent_touching_enclosed_analytic()); +} + TEST(TEST_NYXUS, TEST_NEIGHBORHOOD2D_CLOSEST_NEIGHBORS) { ASSERT_NO_THROW(test_neighborhood2d_closest_neighbors()); @@ -1260,6 +1293,16 @@ TEST(TEST_NYXUS, TEST_IBSI_GLCM_JE) ASSERT_NO_THROW(test_ibsi_glcm_JE()); } +TEST(TEST_NYXUS, TEST_IBSI_GLCM_HOM2) +{ + ASSERT_NO_THROW(test_ibsi_glcm_HOM2()); +} + +TEST(TEST_NYXUS, TEST_IBSI_GLCM_ENTROPY) +{ + ASSERT_NO_THROW(test_ibsi_glcm_ENTROPY()); +} + TEST(TEST_NYXUS, TEST_IBSI_GLCM_JMAX) { ASSERT_NO_THROW(test_ibsi_glcm_JMAX()); @@ -1705,6 +1748,13 @@ TEST(TEST_NYXUS, TEST_IBSI_GLRLM_LRHGLE) ASSERT_NO_THROW(test_ibsi_glrlm_lrhgle()); } +TEST(TEST_NYXUS, TEST_IBSI_GLRLM_LGLRE_AVE) { ASSERT_NO_THROW(test_ibsi_glrlm_lglre_ave()); } +TEST(TEST_NYXUS, TEST_IBSI_GLRLM_HGLRE_AVE) { ASSERT_NO_THROW(test_ibsi_glrlm_hglre_ave()); } +TEST(TEST_NYXUS, TEST_IBSI_GLRLM_SRLGLE_AVE) { ASSERT_NO_THROW(test_ibsi_glrlm_srlgle_ave()); } +TEST(TEST_NYXUS, TEST_IBSI_GLRLM_SRHGLE_AVE) { ASSERT_NO_THROW(test_ibsi_glrlm_srhgle_ave()); } +TEST(TEST_NYXUS, TEST_IBSI_GLRLM_LRLGLE_AVE) { ASSERT_NO_THROW(test_ibsi_glrlm_lrlgle_ave()); } +TEST(TEST_NYXUS, TEST_IBSI_GLRLM_LRHGLE_AVE) { ASSERT_NO_THROW(test_ibsi_glrlm_lrhgle_ave()); } + TEST(TEST_NYXUS, TEST_IBSI_GLRLM_GLN) { ASSERT_NO_THROW(test_ibsi_glrlm_gln()); diff --git a/tests/test_glcm_ibsi.h b/tests/test_glcm_ibsi.h index 98e2ec25f..65c79d223 100644 --- a/tests/test_glcm_ibsi.h +++ b/tests/test_glcm_ibsi.h @@ -31,6 +31,8 @@ static std::unordered_map ibsi_reference_glcm_feature_golde {"GLCM_IDMN", 0.899}, // p. 74, consensus: very strong {"GLCM_INFOMEAS1", -0.155}, // p. 80, consensus: very strong {"GLCM_INFOMEAS2", 0.487}, // p. 81, consensus: very strong + {"GLCM_HOM2", 0.619}, // = IBSI IDM (WF0Z, p.73); PyRadiomics 'Idm' twin of GLCM_IDM + {"GLCM_ENTROPY", 2.05}, // = IBSI JE (TU9B, p.63); joint entropy twin of GLCM_JE {"GLCM_IV", 0.0567}, // p. 75, consensus: very strong {"GLCM_JAVE", 2.14}, // p. 62, consensus: very strong {"GLCM_JE", 2.05}, // p. 63, consensus: very strong @@ -244,6 +246,16 @@ void test_ibsi_glcm_JAVE() test_ibsi_glcm_feature(Nyxus::Feature2D::GLCM_JAVE, "GLCM_JAVE"); } +void test_ibsi_glcm_HOM2() // regression-fix: HOM2 == IBSI IDM once /sum_p normalization is applied +{ + test_ibsi_glcm_feature(Nyxus::Feature2D::GLCM_HOM2, "GLCM_HOM2"); +} + +void test_ibsi_glcm_ENTROPY() // regression-fix: ENTROPY == IBSI JE once /sum_p normalization is applied +{ + test_ibsi_glcm_feature(Nyxus::Feature2D::GLCM_ENTROPY, "GLCM_ENTROPY"); +} + void test_ibsi_glcm_JE() { test_ibsi_glcm_feature(Nyxus::Feature2D::GLCM_JE, "GLCM_JE"); diff --git a/tests/test_glcm_regression.h b/tests/test_glcm_regression.h index f01a75640..be47dadde 100644 --- a/tests/test_glcm_regression.h +++ b/tests/test_glcm_regression.h @@ -83,8 +83,8 @@ static std::unordered_map unvetted_nyxus_convention_regress {"GLCM_CLUTEND", 1.5639042057291665e+03}, {"GLCM_CORRELATION", 0.000690135}, {"GLCM_ENERGY", 0.381801}, - {"GLCM_ENTROPY", -20.1735}, - {"GLCM_HOM2", 6.81505}, + {"GLCM_ENTROPY", 1.87602}, // FIX: was buggy unnormalized -20.1735; post /sum_p fix == GLCM_JE (joint entropy) + {"GLCM_HOM2", 0.572168}, // FIX: was buggy unnormalized 6.81505; post /sum_p fix == GLCM_IDM (homogeneity in [0,1]) {"GLCM_INFOMEAS1", -0.184406}, {"GLCM_INFOMEAS2", 0.495817}, {"GLCM_JAVE", 35.5215}, diff --git a/tests/test_glrlm_ibsi.h b/tests/test_glrlm_ibsi.h index 2c5a668d7..90dfdda6d 100644 --- a/tests/test_glrlm_ibsi.h +++ b/tests/test_glrlm_ibsi.h @@ -208,4 +208,52 @@ void test_ibsi_glrlm_rv() void test_ibsi_glrlm_re() { test_ibsi_glrlm_feature(Nyxus::Feature2D::GLRLM_RE, "GLRLM_RE"); -} \ No newline at end of file +} + +// IBSI oracle for the angle-averaged (_AVE) joint gray-level x run-length emphasis features. Nyxus +// stores the mean over the 4 directions in slot [0] of each _AVE feature; averaging that over the 4 +// phantom slices reproduces the IBSI 2D direction+slice-averaged consensus (the same grand mean the +// base-feature test pins as total/16). ibsi_key indexes the shared reference table. +void test_ibsi_glrlm_ave_feature(const Feature2D& feature_, const std::string& ibsi_key) +{ + Fsettings s; + s.resize((int)NyxSetting::__COUNT__); + s[(int)NyxSetting::SOFTNAN].rval = 0.0; + s[(int)NyxSetting::TINY].rval = 0.0; + s[(int)NyxSetting::SINGLEROI].bval = false; + s[(int)NyxSetting::GREYDEPTH].ival = 128; + s[(int)NyxSetting::PIXELSIZEUM].rval = 100; + s[(int)NyxSetting::PIXELDISTANCE].ival = 5; + s[(int)NyxSetting::USEGPU].bval = false; + s[(int)NyxSetting::VERBOSLVL].ival = 0; + s[(int)NyxSetting::IBSI].bval = true; + + int feature = int(feature_); + double total = 0; + + const NyxusPixel* intens[4] = { ibsi_phantom_z1_intensity, ibsi_phantom_z2_intensity, ibsi_phantom_z3_intensity, ibsi_phantom_z4_intensity }; + const NyxusPixel* masks[4] = { ibsi_phantom_z1_mask, ibsi_phantom_z2_mask, ibsi_phantom_z3_mask, ibsi_phantom_z4_mask }; + const size_t counts[4] = { + sizeof(ibsi_phantom_z1_mask) / sizeof(NyxusPixel), sizeof(ibsi_phantom_z2_intensity) / sizeof(NyxusPixel), + sizeof(ibsi_phantom_z3_intensity) / sizeof(NyxusPixel), sizeof(ibsi_phantom_z4_intensity) / sizeof(NyxusPixel) }; + + for (int i = 0; i < 4; i++) + { + LR roidata; + GLRLMFeature f; + load_masked_test_roi_data(roidata, intens[i], masks[i], counts[i]); + ASSERT_NO_THROW(f.calculate(roidata, s)); + roidata.initialize_fvals(); + f.save_value(roidata.fvals); + total += roidata.fvals[feature][0]; // _AVE stores the direction-mean in slot [0] + } + + ASSERT_TRUE(agrees_gt(total / 4, ibsi_reference_glrlm_feature_golden_values[ibsi_key], 100.)); +} + +void test_ibsi_glrlm_lglre_ave() { test_ibsi_glrlm_ave_feature(Nyxus::Feature2D::GLRLM_LGLRE_AVE, "GLRLM_LGLRE"); } +void test_ibsi_glrlm_hglre_ave() { test_ibsi_glrlm_ave_feature(Nyxus::Feature2D::GLRLM_HGLRE_AVE, "GLRLM_HGLRE"); } +void test_ibsi_glrlm_srlgle_ave() { test_ibsi_glrlm_ave_feature(Nyxus::Feature2D::GLRLM_SRLGLE_AVE, "GLRLM_SRLGLE"); } +void test_ibsi_glrlm_srhgle_ave() { test_ibsi_glrlm_ave_feature(Nyxus::Feature2D::GLRLM_SRHGLE_AVE, "GLRLM_SRHGLE"); } +void test_ibsi_glrlm_lrlgle_ave() { test_ibsi_glrlm_ave_feature(Nyxus::Feature2D::GLRLM_LRLGLE_AVE, "GLRLM_LRLGLE"); } +void test_ibsi_glrlm_lrhgle_ave() { test_ibsi_glrlm_ave_feature(Nyxus::Feature2D::GLRLM_LRHGLE_AVE, "GLRLM_LRHGLE"); } \ No newline at end of file diff --git a/tests/test_moments_common.h b/tests/test_moments_common.h index e991188cf..5abb0d0eb 100644 --- a/tests/test_moments_common.h +++ b/tests/test_moments_common.h @@ -98,6 +98,61 @@ static void calculate_2d_geomoment_feature_values(std::vector x) + continue; + if (roidata.aux_area == 0) + init_label_record_3(roidata, x, y, 1); + else + update_label_record_3(roidata, x, y, 1); + } + + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + { + if (5 * y > x) + continue; + roidata.raw_pixels.push_back(Pixel2(x, y, 1)); + } + + roidata.make_nonanisotropic_aabb(); + roidata.aux_image_matrix.allocate( + roidata.aabb.get_width(), + roidata.aabb.get_height()); + roidata.aux_image_matrix.calculate_from_pixelcloud(roidata.raw_pixels, roidata.aabb); +} + +static void calculate_2d_wedge_geomoment_feature_values(std::vector>& fvals) +{ + Fsettings s = make_2d_geomoment_settings(); + + LR roidata(1402); + load_wedge_fixture(roidata); + roidata.initialize_fvals(); + + ContourFeature contour; + contour.calculate(roidata, s); + contour.save_value(roidata.fvals); + + Smoms2D_feature shape_moments; + shape_moments.calculate(roidata, s); + shape_moments.save_value(roidata.fvals); + + fvals = roidata.fvals; +} + static void assert_2d_geomoment_features( const std::vector>& fvals, const std::vector& golden_values, diff --git a/tests/test_moments_regression.h b/tests/test_moments_regression.h index 478d5ed88..6edf500a7 100644 --- a/tests/test_moments_regression.h +++ b/tests/test_moments_regression.h @@ -47,8 +47,8 @@ static const std::vector unvetted_nyxus_regression_shape_g {Nyxus::Feature2D::WEIGHTED_HU_M2, "WEIGHTED_HU_M2", 0.00024306185106698786}, {Nyxus::Feature2D::WEIGHTED_HU_M3, "WEIGHTED_HU_M3", 1.1262214820995351e-05}, {Nyxus::Feature2D::WEIGHTED_HU_M4, "WEIGHTED_HU_M4", 7.674925625409561e-05}, - {Nyxus::Feature2D::WEIGHTED_HU_M5, "WEIGHTED_HU_M5", -4.0924793325555725e-09}, - {Nyxus::Feature2D::WEIGHTED_HU_M6, "WEIGHTED_HU_M6", 0.004652261067232659}, + {Nyxus::Feature2D::WEIGHTED_HU_M5, "WEIGHTED_HU_M5", -1.8387946856938181e-09}, // FIX(h5): snapshot after the calcHu_imp h5 fix (skimage moments_hu on the pinned WT_NORM_CTR_MOM_*; gen_moments_skimage.py) + {Nyxus::Feature2D::WEIGHTED_HU_M6, "WEIGHTED_HU_M6", 2.1453724299933224e-07}, // FIX(h6): snapshot after the calcHu_imp h6 fix; old pin 0.004652... == WT_NORM_CTR_MOM_03, i.e. the stray "+eta03" of the precedence bug (gen_moments_skimage.py) {Nyxus::Feature2D::WEIGHTED_HU_M7, "WEIGHTED_HU_M7", -1.3078000499389243e-09}, }; @@ -97,8 +97,8 @@ static const std::vector unvetted_nyxus_regression_intensi {Nyxus::Feature2D::IMOM_WHU2, "IMOM_WHU2", 5.6161730111679807e-09}, {Nyxus::Feature2D::IMOM_WHU3, "IMOM_WHU3", 1.1923046864432925e-12}, {Nyxus::Feature2D::IMOM_WHU4, "IMOM_WHU4", 3.1287168309169019e-12}, - {Nyxus::Feature2D::IMOM_WHU5, "IMOM_WHU5", -5.2494915279842729e-24}, - {Nyxus::Feature2D::IMOM_WHU6, "IMOM_WHU6", 4.6265447915851515e-07}, + {Nyxus::Feature2D::IMOM_WHU5, "IMOM_WHU5", -3.578057915124686e-25}, // FIX(h5): snapshot after the calcHu_imp h5 fix (skimage moments_hu on the pinned IMOM_WNCM_*; gen_moments_skimage.py) + {Nyxus::Feature2D::IMOM_WHU6, "IMOM_WHU6", 3.297985694277869e-17}, // FIX(h6): snapshot after the calcHu_imp h6 fix; old pin 4.6265447915851515e-07 == IMOM_WNCM_03, i.e. the stray "+eta03" of the precedence bug (gen_moments_skimage.py) {Nyxus::Feature2D::IMOM_WHU7, "IMOM_WHU7", -5.6202519491182231e-24}, }; diff --git a/tests/test_moments_skimage.h b/tests/test_moments_skimage.h index 64b1cf22a..48d912b67 100644 --- a/tests/test_moments_skimage.h +++ b/tests/test_moments_skimage.h @@ -43,7 +43,7 @@ static const std::vector oracle_3p_shape_geomoment_feature {Nyxus::Feature2D::HU_M2, "HU_M2", 0.0009336419753086421}, {Nyxus::Feature2D::HU_M3, "HU_M3", 0}, {Nyxus::Feature2D::HU_M4, "HU_M4", 0}, - {Nyxus::Feature2D::HU_M5, "HU_M5", 4.598098572281346e-10}, + {Nyxus::Feature2D::HU_M5, "HU_M5", 0}, // FIX(h5): oracle-exact 0 on this symmetric fixture (odd etas vanish); the old pin 4.598e-10 was summation noise of the defective h5 formula (gen_moments_skimage.py) {Nyxus::Feature2D::HU_M6, "HU_M6", 0}, {Nyxus::Feature2D::HU_M7, "HU_M7", -2.3604559630908652e-10}, }; @@ -89,11 +89,51 @@ static const std::vector oracle_3p_intensity_geomoment_fea {Nyxus::Feature2D::IMOM_HU2, "IMOM_HU2", 3.7112807351259333e-08}, {Nyxus::Feature2D::IMOM_HU3, "IMOM_HU3", 2.6788774435431954e-11}, {Nyxus::Feature2D::IMOM_HU4, "IMOM_HU4", 2.8991603200922661e-12}, - {Nyxus::Feature2D::IMOM_HU5, "IMOM_HU5", -2.1393783155778043e-23}, - {Nyxus::Feature2D::IMOM_HU6, "IMOM_HU6", -2.3976723312959013e-06}, + {Nyxus::Feature2D::IMOM_HU5, "IMOM_HU5", -1.9898126265836865e-22}, // FIX(h5): skimage-oracle value after the calcHu_imp h5 fix; old pin encoded the 9x-bracket defect (gen_moments_skimage.py) + {Nyxus::Feature2D::IMOM_HU6, "IMOM_HU6", -6.66827346717698e-17}, // FIX(h6): skimage-oracle value after the calcHu_imp h6 fix; old pin -2.3976723312959013e-06 == IMOM_NCM_03, i.e. the stray "+eta03" of the precedence bug (gen_moments_skimage.py) {Nyxus::Feature2D::IMOM_HU7, "IMOM_HU7", 2.3835594243218353e-23}, }; +// Asymmetric wedge fixture (see load_wedge_fixture in test_moments_common.h): the ONLY fixture in +// this file whose odd-order etas are big enough to vet Hu h5/h6 against skimage above the assertion +// tolerance. Provenance: scikit-image 0.26.0 / numpy 2.4.6, generator +// tests/vetting/oracles/gen_moments_skimage.py (section C), 2026-07-16. +static const std::vector wedge_shape_hu_skimage_golden_values{ + {Nyxus::Feature2D::SPAT_MOMENT_00, "SPAT_MOMENT_00", 180}, + {Nyxus::Feature2D::SPAT_MOMENT_10, "SPAT_MOMENT_10", 4560}, + {Nyxus::Feature2D::SPAT_MOMENT_01, "SPAT_MOMENT_01", 420}, + {Nyxus::Feature2D::CENTRAL_MOMENT_20, "CENTRAL_MOMENT_20", 17860}, + {Nyxus::Feature2D::CENTRAL_MOMENT_02, "CENTRAL_MOMENT_02", 699.9999999999999}, + {Nyxus::Feature2D::CENTRAL_MOMENT_11, "CENTRAL_MOMENT_11", 1750}, + {Nyxus::Feature2D::CENTRAL_MOMENT_30, "CENTRAL_MOMENT_30", -99166.66666666698}, + {Nyxus::Feature2D::CENTRAL_MOMENT_03, "CENTRAL_MOMENT_03", 793.3333333333339}, + {Nyxus::Feature2D::CENTRAL_MOMENT_21, "CENTRAL_MOMENT_21", -9916.666666666686}, + {Nyxus::Feature2D::CENTRAL_MOMENT_12, "CENTRAL_MOMENT_12", 1983.3333333333285}, + {Nyxus::Feature2D::NORM_CENTRAL_MOMENT_20, "NORM_CENTRAL_MOMENT_20", 0.5512345679012346}, + {Nyxus::Feature2D::NORM_CENTRAL_MOMENT_02, "NORM_CENTRAL_MOMENT_02", 0.021604938271604934}, + {Nyxus::Feature2D::NORM_CENTRAL_MOMENT_11, "NORM_CENTRAL_MOMENT_11", 0.05401234567901234}, + {Nyxus::Feature2D::NORM_CENTRAL_MOMENT_30, "NORM_CENTRAL_MOMENT_30", -0.22813107795136814}, + {Nyxus::Feature2D::NORM_CENTRAL_MOMENT_03, "NORM_CENTRAL_MOMENT_03", 0.0018250486236109408}, + {Nyxus::Feature2D::NORM_CENTRAL_MOMENT_21, "NORM_CENTRAL_MOMENT_21", -0.022813107795136785}, + {Nyxus::Feature2D::NORM_CENTRAL_MOMENT_12, "NORM_CENTRAL_MOMENT_12", 0.004562621559027338}, + {Nyxus::Feature2D::HU_M1, "HU_M1", 0.5728395061728395}, + {Nyxus::Feature2D::HU_M2, "HU_M2", 0.2921768785246152}, + {Nyxus::Feature2D::HU_M3, "HU_M3", 0.06341348298776381}, + {Nyxus::Feature2D::HU_M4, "HU_M4", 0.05042335332144146}, + {Nyxus::Feature2D::HU_M5, "HU_M5", 0.002851264767850148}, // defective h5 gives 0.0032935269006466807 (442x tolerance away) + {Nyxus::Feature2D::HU_M6, "HU_M6", 0.027252861297278195}, // defective h6 gives 0.02916606310377036 (1913x tolerance away) + {Nyxus::Feature2D::HU_M7, "HU_M7", 5.616461607732461e-06}, +}; + +// Vets Hu h5/h6 (and the full moment chain) against scikit-image on an asymmetric fixture -- +// the historic calcHu_imp h5 9x-bracket / h6 "+eta03"-precedence defects fail this assertion. +void test_moments_hu_wedge_skimage() +{ + std::vector> fvals; + calculate_2d_wedge_geomoment_feature_values(fvals); + assert_2d_geomoment_features(fvals, wedge_shape_hu_skimage_golden_values, "VERIFIABLE_WITH_3P_BUILTIN_ORACLE__"); +} + void test_2d_shape_geometric_moments_verifiable_with_3p_builtin_oracle() { std::vector> fvals; diff --git a/tests/test_morphology_common.h b/tests/test_morphology_common.h index dc53edfba..a72212037 100644 --- a/tests/test_morphology_common.h +++ b/tests/test_morphology_common.h @@ -102,6 +102,14 @@ static std::unordered_map oracle_3p_shape2d_feature_golden_ {"GEODETIC_LENGTH", 10.0}, {"THICKNESS", 3.0}, {"EROSIONS_2_VANISH", 1.0}, + // EXTREMA P1..P8 (X,Y) match MATLAB/Octave regionprops('Extrema') EXACTLY under the documented + // coordinate convention: MATLAB returns 1-based sub-pixel *corner* coords, Nyxus returns 0-based + // pixel *centers*. The corner is direction-specific -> the offset is per-point: left/top coords + // map as (matlab - 0.5), right/bottom coords as (matlab - 1.5). Verified on this 8x8 fixture by + // octave/matlab_oracle_tests/extrema_8x8.m: raw Extrema P1(2.5,0.5) P2(4.5,0.5) P3(6.5,2.5) + // P4(6.5,4.5) P5(5.5,7.5) P6(3.5,7.5) P7(0.5,4.5) P8(0.5,2.5) -> after the per-point offset -> + // P1(2,0) P2(3,0) P3(5,2) P4(5,3) P5(4,6) P6(3,6) P7(0,3) P8(0,2), i.e. these goldens exactly. + // (The earlier "~1px off" on the right/bottom coords was a harness bug: it used a uniform -0.5.) {"EXTREMA_P1_X", 2.0}, {"EXTREMA_P1_Y", 0.0}, {"EXTREMA_P2_X", 3.0}, diff --git a/tests/test_morphology_regression.h b/tests/test_morphology_regression.h index 16e92f7a8..aaf9a4946 100644 --- a/tests/test_morphology_regression.h +++ b/tests/test_morphology_regression.h @@ -60,6 +60,33 @@ void test_shape2d_misc_shape_features() assert_unvetted_no_direct_oracle_shape2d_feature(fvals, Nyxus::Feature2D::DIAMETER_MIN_ENCLOSING_CIRCLE, "DIAMETER_MIN_ENCLOSING_CIRCLE"); } +// Tier-2 documented-formula conformance (NO external oracle). These features have a recognized closed +// form but their VALUE uses Nyxus' own conventions (pixel-count area, contour perimeter, moment-fit +// major axis), so no third-party tool reproduces the number. What we CAN pin is that the code applies +// the published formula to its own constituents without an implementation bug -- recompute the formula +// from AREA_PIXELS_COUNT / PERIMETER / MAJOR_AXIS_LENGTH and require an exact match. This is weaker than +// external-oracle vetting and is registered as such (oracle=formula) in oracle_coverage.csv. +void test_shape2d_documented_formula_conformance_no_external_oracle() +{ + std::vector> fvals; + calculate_shape2d_feature_values(fvals); + + const double PI = 3.14159265358979323846; + const double A = fvals[static_cast(Nyxus::Feature2D::AREA_PIXELS_COUNT)][0]; + const double P = fvals[static_cast(Nyxus::Feature2D::PERIMETER)][0]; + const double major = fvals[static_cast(Nyxus::Feature2D::MAJOR_AXIS_LENGTH)][0]; + + // CIRCULARITY = sqrt(4*pi*A) / P (convex_hull_nontriv.cpp) + const double circ_formula = std::sqrt(4.0 * PI * A) / P; + ASSERT_NEAR(fvals[static_cast(Nyxus::Feature2D::CIRCULARITY)][0], circ_formula, 1e-9) + << "CIRCULARITY does not match sqrt(4*pi*A)/P"; + + // ROUNDNESS = 4*A / (pi*major^2) (ellipse_fitting.cpp) + const double round_formula = 4.0 * A / (PI * major * major); + ASSERT_NEAR(fvals[static_cast(Nyxus::Feature2D::ROUNDNESS)][0], round_formula, 1e-9) + << "ROUNDNESS does not match 4A/(pi*major^2)"; +} + void test_shape2d_unvetted_no_direct_oracle_radius_features() { std::vector> fvals; @@ -140,6 +167,61 @@ void test_remaining2d_verifiable_with_3p_builtin_oracle_caliper_features() assert_verifiable_with_3p_builtin_oracle_remaining2d_feature(fvals, Nyxus::Feature2D::STAT_NASSENSTEIN_DIAM_MODE, "STAT_NASSENSTEIN_DIAM_MODE"); } +// Vets the reimplemented Martin (area-bisecting chord) and Nassenstein (bottom-tangent vertical +// chord) diameters against imea on a clean filled ellipse (a=20, b=10). See the oracle block in +// test_remaining2d_common.h. Robust stats (min/max/mean/median) agree with imea within the +// hull-vs-raster convention tolerance; the >0 lower bound pins that the old min+max-chord bug +// (0-length Nassenstein diameters) is gone. +void test_shape2d_caliper_martin_nassenstein_imea_ellipse_oracle() +{ + std::vector> fvals; + calculate_ellipse_caliper_values(fvals); + + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_MARTIN_DIAM_MIN, "STAT_MARTIN_DIAM_MIN"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_MARTIN_DIAM_MAX, "STAT_MARTIN_DIAM_MAX"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_MARTIN_DIAM_MEAN, "STAT_MARTIN_DIAM_MEAN"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_MARTIN_DIAM_MEDIAN, "STAT_MARTIN_DIAM_MEDIAN"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_NASSENSTEIN_DIAM_MIN, "STAT_NASSENSTEIN_DIAM_MIN"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_NASSENSTEIN_DIAM_MAX, "STAT_NASSENSTEIN_DIAM_MAX"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_NASSENSTEIN_DIAM_MEAN, "STAT_NASSENSTEIN_DIAM_MEAN"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_NASSENSTEIN_DIAM_MEDIAN, "STAT_NASSENSTEIN_DIAM_MEDIAN"); + + // Bug-gone invariant: a solid shape cannot have a 0-length diameter (the old code produced 0). + ASSERT_GT(fvals[static_cast(Nyxus::Feature2D::STAT_MARTIN_DIAM_MIN)][0], 2.0); + ASSERT_GT(fvals[static_cast(Nyxus::Feature2D::STAT_NASSENSTEIN_DIAM_MIN)][0], 2.0); +} + +// Vets the Feret diameter distribution against imea on the same filled ellipse. Feret is a correct +// rotating-calipers implementation; robust stats (min/max/mean/median) agree with imea within the +// hull-vs-raster convention tolerance. (MIN/MAX_FERET_ANGLE stay regression — they are a Nyxus-frame +// angle convention with no directly comparable imea output.) +void test_shape2d_caliper_feret_imea_ellipse_oracle() +{ + std::vector> fvals; + calculate_ellipse_caliper_values(fvals); + + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_FERET_DIAM_MIN, "STAT_FERET_DIAM_MIN"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_FERET_DIAM_MAX, "STAT_FERET_DIAM_MAX"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_FERET_DIAM_MEAN, "STAT_FERET_DIAM_MEAN"); + assert_caliper_close_to_imea(fvals, Nyxus::Feature2D::STAT_FERET_DIAM_MEDIAN, "STAT_FERET_DIAM_MEDIAN"); +} + +// Vets the minimum-enclosing-circle diameter (Welzl / cv2.minEnclosingCircle) against its exact +// geometric/imea value on two clean fixtures: ellipse a=20 -> 2a=40, circle r=15 -> 30. This is +// centroid-independent, so it matches to <0.1%. (DIAMETER_CIRCUMSCRIBING/INSCRIBING_CIRCLE are left +// regression: imea's centroid-to-contour-distance approximation, convention-sensitive.) +void test_shape2d_min_enclosing_circle_imea_oracle() +{ + std::vector> ell; + calculate_ellipse_caliper_values(ell); + assert_caliper_close_to_imea(ell, Nyxus::Feature2D::DIAMETER_MIN_ENCLOSING_CIRCLE, "DIAMETER_MIN_ENCLOSING_CIRCLE", 0.05); + + std::vector> cir; + calculate_circle_shape_values(cir); + const double d = cir[static_cast(Nyxus::Feature2D::DIAMETER_MIN_ENCLOSING_CIRCLE)][0]; + ASSERT_NEAR(d, 30.0, 30.0 * 0.05) << "circle min-enclosing nyxus=" << d << " expected~30"; +} + void test_remaining2d_verifiable_with_3p_builtin_oracle_chord_stat_features() { std::vector> fvals; diff --git a/tests/test_remaining2d_common.h b/tests/test_remaining2d_common.h index 130535095..91076b9ed 100644 --- a/tests/test_remaining2d_common.h +++ b/tests/test_remaining2d_common.h @@ -11,6 +11,7 @@ #include "../src/nyx/features/basic_morphology.h" #include "../src/nyx/features/caliper.h" #include "../src/nyx/features/chords.h" +#include "../src/nyx/features/circle.h" #include "../src/nyx/features/contour.h" #include "../src/nyx/features/convex_hull.h" #include "../src/nyx/features/erosion.h" @@ -24,25 +25,38 @@ static std::unordered_map oracle_3p_remaining2d_feature_golden_values{ {"EROSIONS_2_VANISH_COMPLEMENT", 0.0}, {"MIN_FERET_ANGLE", 40.0}, - {"MAX_FERET_ANGLE", 0.0}, - {"STAT_FERET_DIAM_MIN", 4.0}, - {"STAT_FERET_DIAM_MAX", 5.0}, - {"STAT_FERET_DIAM_MEAN", 4.9473684210526319}, - {"STAT_FERET_DIAM_MEDIAN", 5.0}, - {"STAT_FERET_DIAM_STDDEV", 0.22329687826943606}, + // FIX (caliper float-precision): re-pinned to the float-precision hull-rotation values (see rotation.cpp + // rotate_around_center_fp). The old integer-Pixel2 rotation truncated every rotated vertex inward, + // so these 8x8-fixture goldens shifted when the truncation was removed. MAX_FERET_ANGLE moved 0->110 + // because the per-angle Feret ties differently once the diameters are no longer integer-quantized + // (the Feret angle is a regression-only Nyxus-frame convention, not oracle-vetted). MODE values are + // unchanged. The diameters themselves are vetted vs imea (<=10%) on the ellipse oracle below. + {"MAX_FERET_ANGLE", 110.0}, + {"STAT_FERET_DIAM_MIN", 4.47301}, + {"STAT_FERET_DIAM_MAX", 6.3222}, + {"STAT_FERET_DIAM_MEAN", 5.40848}, + {"STAT_FERET_DIAM_MEDIAN", 5.19615}, + {"STAT_FERET_DIAM_STDDEV", 0.550668}, {"STAT_FERET_DIAM_MODE", 5.0}, - {"STAT_MARTIN_DIAM_MIN", 0.79999995380639899}, - {"STAT_MARTIN_DIAM_MAX", 5.0}, - {"STAT_MARTIN_DIAM_MEAN", 3.1314814727604796}, - {"STAT_MARTIN_DIAM_MEDIAN", 3.5}, - {"STAT_MARTIN_DIAM_STDDEV", 1.6662952583978845}, - {"STAT_MARTIN_DIAM_MODE", 1.0}, - {"STAT_NASSENSTEIN_DIAM_MIN", 0.0}, - {"STAT_NASSENSTEIN_DIAM_MAX", 5.0}, - {"STAT_NASSENSTEIN_DIAM_MEAN", 2.68842592930522}, - {"STAT_NASSENSTEIN_DIAM_MEDIAN", 3.5}, - {"STAT_NASSENSTEIN_DIAM_STDDEV", 2.1985188021518653}, - {"STAT_NASSENSTEIN_DIAM_MODE", 0.0}, + // FIXED (caliper reimpl): Martin is now the area-bisecting chord and Nassenstein the bottom-tangent + // vertical chord (one diameter per angle), not the old min+max of a Y-grid of horizontal chords. + // The old goldens pinned the bug (Martin min 0.8, Nassenstein min/mode 0.0 — impossible for a solid + // shape). These are the corrected values on the 8x8 fixture; the diameters are vetted vs imea on a + // clean ellipse in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. + // FIX (caliper float-precision): re-pinned again after the float-precision hull rotation removed the inward + // integer-truncation bias (MODE unchanged). + {"STAT_MARTIN_DIAM_MIN", 4.25885}, + {"STAT_MARTIN_DIAM_MAX", 6.12801}, + {"STAT_MARTIN_DIAM_MEAN", 5.01762}, + {"STAT_MARTIN_DIAM_MEDIAN", 4.97511}, + {"STAT_MARTIN_DIAM_STDDEV", 0.553162}, + {"STAT_MARTIN_DIAM_MODE", 4.0}, + {"STAT_NASSENSTEIN_DIAM_MIN", 1.67316}, + {"STAT_NASSENSTEIN_DIAM_MAX", 6.24165}, + {"STAT_NASSENSTEIN_DIAM_MEAN", 4.77746}, + {"STAT_NASSENSTEIN_DIAM_MEDIAN", 5.03857}, + {"STAT_NASSENSTEIN_DIAM_STDDEV", 1.09628}, + {"STAT_NASSENSTEIN_DIAM_MODE", 4.0}, {"MAXCHORDS_MAX", 6.0}, {"MAXCHORDS_MIN", 3.0}, {"MAXCHORDS_MEDIAN", 4.0}, @@ -70,7 +84,9 @@ static std::unordered_map unvetted_nyxus_regression_remaini // assertions below now value-compare against them (agrees_gt) so any future drift is caught. {"POLYGONALITY_AVE", 2.0833333333333357}, {"HEXAGONALITY_AVE", 6.8823312738837217}, - {"HEXAGONALITY_STDDEV", 0.18495557498763179}, + // FIX (caliper float-precision): HEXAGONALITY_STDDEV re-pinned (depends on STAT_FERET_DIAM_MIN/MAX, which + // shifted with the float-precision hull rotation); the AVE scores stayed within tolerance. + {"HEXAGONALITY_STDDEV", 0.188079}, // FIXED (chords.cpp idxmax used iteMin): max-angle now indexes the longest chord (angle 0), not the min {"MAXCHORDS_MAX_ANG", 0.0}, {"MAXCHORDS_MIN_ANG", 0.94247779607693793}, @@ -108,6 +124,43 @@ static std::unordered_map> oracle_3p_remaining2 }}, }; +// --------------------------------------------------------------------------------------------------- +// Martin / Nassenstein caliper vetting vs imea (external oracle). +// +// The 8x8 shape2d fixture above is too small/aliased to serve as a tight caliper oracle, so the +// corrected Martin (area-bisecting chord) and Nassenstein (bottom-tangent vertical chord) diameters +// are vetted on a clean, larger convex fixture: a filled ellipse a=20, b=10 (same rasterization as +// morph_oracle/caliper_proto.py). imea (imea.measure_2d.statistical_length, dalpha=10) is the +// reference. Nyxus rotates the convex hull and measures analytically while imea rotates the filled +// raster, so the two agree only up to a ~1-2px hull-vs-raster convention gap (same gap already +// accepted for Feret) — hence a 10% relative tolerance on the robust stats. The point that this pins +// is that the diameters are now the *correct* quantities (min > 0), not the old min+max-chord bug +// that produced physically-impossible 0-length Nassenstein diameters. +static std::unordered_map imea_ellipse_caliper_oracle{ + {"STAT_MARTIN_DIAM_MIN", 19.0}, + {"STAT_MARTIN_DIAM_MAX", 41.0}, + {"STAT_MARTIN_DIAM_MEAN", 27.61}, + {"STAT_MARTIN_DIAM_MEDIAN", 25.5}, + {"STAT_NASSENSTEIN_DIAM_MIN", 16.0}, + {"STAT_NASSENSTEIN_DIAM_MAX", 41.0}, + {"STAT_NASSENSTEIN_DIAM_MEAN", 25.17}, + {"STAT_NASSENSTEIN_DIAM_MEDIAN", 21.5}, + // Feret is a correct rotating-calipers implementation (unlike the Martin/Nassenstein bug); it + // agrees with imea within the same ~1-2px hull-vs-raster convention gap. Reference from imea + // (imea.measure_2d.statistical_length feret_diameters, dalpha=10) on the same ellipse. + {"STAT_FERET_DIAM_MIN", 21.0}, + {"STAT_FERET_DIAM_MAX", 41.0}, + {"STAT_FERET_DIAM_MEAN", 31.72}, + {"STAT_FERET_DIAM_MEDIAN", 32.5}, + // Minimum enclosing circle (Welzl / cv2.minEnclosingCircle) is centroid-independent and matches + // imea/OpenCV exactly: for the ellipse a=20 its diameter = the major axis = 2a = 40. (The circle + // fixture's value 30 is asserted inline.) NOTE: DIAMETER_CIRCUMSCRIBING_CIRCLE and + // DIAMETER_INSCRIBING_CIRCLE are NOT here — they are imea's crude max/min centroid-to-contour + // distance approximation (not a true geometric circle), sensitive to Nyxus's contour convention + + // the centroid-1 offset (a symmetric circle yields 35.6/23.3, not ~30/~30), so they stay regression. + {"DIAMETER_MIN_ENCLOSING_CIRCLE", 40.0}, +}; + static Fsettings make_remaining2d_settings() { Fsettings s; @@ -236,6 +289,125 @@ static void calculate_remaining2d_polygonality_feature_values(std::unordered_map } } +// Build a filled ellipse (a=20, b=10) ROI and compute its caliper features. Mirrors the +// rasterization in morph_oracle/caliper_proto.py so the imea reference values above line up. +static void calculate_ellipse_caliper_values(std::vector>& fvals) +{ + Fsettings s = make_remaining2d_settings(); + + LR roi(1); + const double a = 20.0, b = 10.0, cx = 26.0, cy = 16.0; // pad=6, matches the prototype fixture + bool first = true; + for (int y = 0; y <= 32; y++) + for (int x = 0; x <= 52; x++) + { + double dx = (x - cx) / a, dy = (y - cy) / b; + if (dx * dx + dy * dy <= 1.0) + { + if (first) + { + init_label_record_3(roi, x, y, 1); + first = false; + } + else + update_label_record_3(roi, x, y, 1); + roi.raw_pixels.push_back(Pixel2(static_cast(x), static_cast(y), static_cast(1))); + } + } + roi.make_nonanisotropic_aabb(); + roi.aux_image_matrix = ImageMatrix(roi.raw_pixels); + roi.initialize_fvals(); + + BasicMorphologyFeatures basic; // provides CENTROID_X/Y for the circle features + basic.calculate(roi, s); + basic.save_value(roi.fvals); + + ContourFeature contour; + contour.calculate(roi, s); + contour.save_value(roi.fvals); + + ConvexHullFeature hull; + hull.calculate(roi, s); + hull.save_value(roi.fvals); + + CaliperFeretFeature feret; + feret.calculate(roi, s); + feret.save_value(roi.fvals); + + CaliperMartinFeature martin; + martin.calculate(roi, s); + martin.save_value(roi.fvals); + + CaliperNassensteinFeature nassenstein; + nassenstein.calculate(roi, s); + nassenstein.save_value(roi.fvals); + + EnclosingInscribingCircumscribingCircleFeature circle; + circle.calculate(roi, s); + circle.save_value(roi.fvals); + + fvals = roi.fvals; +} + +// Build a filled circle (r=15) ROI and compute basic morphology + contour + the 3 circle diameters. +static void calculate_circle_shape_values(std::vector>& fvals) +{ + Fsettings s = make_remaining2d_settings(); + + LR roi(2); + const double r = 15.0, cx = 21.0, cy = 21.0; // pad=6 + bool first = true; + for (int y = 0; y <= 42; y++) + for (int x = 0; x <= 42; x++) + { + double dx = (x - cx) / r, dy = (y - cy) / r; + if (dx * dx + dy * dy <= 1.0) + { + if (first) { init_label_record_3(roi, x, y, 1); first = false; } + else update_label_record_3(roi, x, y, 1); + roi.raw_pixels.push_back(Pixel2(static_cast(x), static_cast(y), static_cast(1))); + } + } + roi.make_nonanisotropic_aabb(); + roi.aux_image_matrix = ImageMatrix(roi.raw_pixels); + roi.initialize_fvals(); + + BasicMorphologyFeatures basic; + basic.calculate(roi, s); + basic.save_value(roi.fvals); + + ContourFeature contour; + contour.calculate(roi, s); + contour.save_value(roi.fvals); + + EnclosingInscribingCircumscribingCircleFeature circle; + circle.calculate(roi, s); + circle.save_value(roi.fvals); + + fvals = roi.fvals; +} + +// Assert a caliper stat agrees with imea within a relative tolerance (hull-vs-raster convention gap). +static void assert_caliper_close_to_imea( + const std::vector>& fvals, + Nyxus::Feature2D feature, + const std::string& feature_name, + // FIX (caliper float-precision): tightened 0.15 -> 0.10 after the float-precision hull rotation removed the + // integer-truncation inward bias. Measured residuals on the a=20,b=10 ellipse: Martin 1.8-4.6%, + // Feret 1.4-4.8%, Nassenstein 2.4-3.7% except its bottom-tangent MIN (8.9%) and MEDIAN (6.0%). The + // floor is the Nassenstein MIN: a near-apex vertical tangent chord measured on the convex hull vs + // imea's raster - a definitional hull-vs-raster gap, not a precision loss, so 0.10 is the honest bound. + double reltol = 0.10) +{ + SCOPED_TRACE(std::string("CALIPER_VS_IMEA__") + feature_name); + ASSERT_TRUE(imea_ellipse_caliper_oracle.count(feature_name) > 0); + const double imea_ref = imea_ellipse_caliper_oracle[feature_name]; + const double actual = fvals[static_cast(feature)][0]; + const double denom = std::max(std::abs(imea_ref), 1e-9); + ASSERT_LE(std::abs(actual - imea_ref) / denom, reltol) + << feature_name << " nyxus=" << actual << " imea=" << imea_ref; +} + static void assert_unvetted_no_direct_oracle_remaining2d_feature( const std::vector>& fvals, Nyxus::Feature2D feature, diff --git a/tests/vetting/MIGRATION.md b/tests/vetting/MIGRATION.md index 52196a7cf..0ade8ba18 100644 --- a/tests/vetting/MIGRATION.md +++ b/tests/vetting/MIGRATION.md @@ -195,11 +195,15 @@ Patterns confirmed across the family: - **Systemic orphan finding:** `test_3d_.h` is **orphaned** (never `#include`d, tests never run) for **glcm, glrlm, glszm, gldm, ngtdm** (5 families, ~130 dead 3D-regression assertions). But **ngldm and gldzm have LIVE `test_3d_*` files** (they were `#include`d) → renamed - `test_3d_ngldm_ibsi.h` / `test_3d_gldzm_ibsi.h`. This inconsistency (some native 3D texture tests + `test_3d_ngldm_regression.h` (initially `_ibsi`; corrected in PR #385, see below) / + `test_3d_gldzm_ibsi.h`. This inconsistency (some native 3D texture tests wired, most not) is flagged for a coverage-gap triage; live 3D texture coverage otherwise comes from the `test_3d__pyradiomics.h` (ex-compat) files + parameterized `test_3d_feature_coverage.h`. -- The `test_3d_ngldm_ibsi.h` / `test_3d_gldzm_ibsi.h` kind labels are provisional (IBSI-phantom-based, - not fully confirmed); the rename preserved all tests regardless. +- `test_3d_gldzm_ibsi.h`'s kind label is provisional (IBSI-phantom-based, not fully confirmed). The + matching NGLDM file was CORRECTED to `test_3d_ngldm_regression.h` (PR #385): its `d3ngldm_GT` table + has no provenance, runs on the Nyxus coverage phantom (not the IBSI digital phantom), and disagrees + with MIRP by up to ~10x, so it is a drift guard, not an `_ibsi` oracle. The `_ibsi` suffix is left + free for a genuine 3D IBSI-phantom NGLDM oracle. The rename preserved all tests regardless. **Remaining families for later waves:** moments, morphology, firstorder, intensity_histogram, neighbor, imq, gabor, zernike, radial — plus the cross-cutting `test_3d_feature_coverage.h` split (§4) and the diff --git a/tests/vetting/coverage_report.md b/tests/vetting/coverage_report.md index c2ba00546..4e31cf53f 100644 --- a/tests/vetting/coverage_report.md +++ b/tests/vetting/coverage_report.md @@ -2,22 +2,22 @@ _Generated by check_coverage.py from oracle_coverage.csv. Do not edit by hand._ -Features vetted by >=1 oracle: 455/758 (60%) -regression: 303 untested: 0 +Features vetted by >=1 oracle: 604/758 (80%) +regression: 154 untested: 0 | family | total | vetted | regression | untested | |---|---|---|---|---| -| firstorder | 72 | 53 | 19 | 0 | +| firstorder | 72 | 71 | 1 | 0 | | gabor | 1 | 0 | 1 | 0 | -| glcm | 118 | 72 | 46 | 0 | -| gldm | 28 | 14 | 14 | 0 | +| glcm | 118 | 118 | 0 | 0 | +| gldm | 28 | 28 | 0 | 0 | | gldzm | 36 | 17 | 19 | 0 | -| glrlm | 64 | 38 | 26 | 0 | -| glszm | 32 | 26 | 6 | 0 | +| glrlm | 64 | 64 | 0 | 0 | +| glszm | 32 | 32 | 0 | 0 | | imq | 6 | 0 | 6 | 0 | | intensity_histogram | 47 | 47 | 0 | 0 | -| moments | 180 | 117 | 63 | 0 | -| morphology | 113 | 39 | 74 | 0 | +| moments | 180 | 118 | 62 | 0 | +| morphology | 113 | 77 | 36 | 0 | | neighbor | 9 | 2 | 7 | 0 | | ngldm | 38 | 20 | 18 | 0 | | ngtdm | 10 | 10 | 0 | 0 | diff --git a/tests/vetting/oracle_coverage.csv b/tests/vetting/oracle_coverage.csv index bc6c44269..8882b7448 100644 --- a/tests/vetting/oracle_coverage.csv +++ b/tests/vetting/oracle_coverage.csv @@ -55,7 +55,7 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,ELONGATION,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_matlab.h,,,tracker, 2D,ECCENTRICITY,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_matlab.h,,,tracker, 2D,ORIENTATION,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Clarify convention or verify with anoth...,promote-after-deepdive;convention-mismatch,tracker, -2D,ROUNDNESS,morphology,regression,,needs_audit,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,none (Nyxus-unique formula),no-mainstream-oracle,tracker, +2D,ROUNDNESS,morphology,vetted,analytic,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,no-external-oracle,tracker,"Tier-2 documented-formula conformance, NO external oracle. Nyxus computes ROUNDNESS = 4*A/(pi*major^2) (ellipse_fitting.cpp) with A = ROI pixel count and major = moment-fit MAJOR_AXIS_LENGTH. TEST_SHAPE2D_DOCUMENTED_FORMULA_CONFORMANCE_NO_EXTERNAL_ORACLE recomputes the formula and requires an exact match (1e-9). Implementation bug-free, but the value depends on Nyxus' pixel-area + moment-fit conventions; no third-party tool reproduces it. Formula-vetted only." 2D,PERIMETER,morphology,vetted,skimage,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_features.h;test_nyxus.py;test_morphology_regression.h,test_morphology_skimage.h,,,tracker, 2D,DIAMETER_EQUAL_PERIMETER,morphology,regression,,needs_audit,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_nyxus.py;test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive,tracker, 2D,EDGE_MEAN_INTENSITY,morphology,vetted,cellprofiler,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_nyxus.py;test_morphology_regression.h,test_morphology_cellprofiler.h,,,tracker, @@ -63,33 +63,33 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,EDGE_MAX_INTENSITY,morphology,vetted,cellprofiler,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_cellprofiler.h,,,tracker, 2D,EDGE_MIN_INTENSITY,morphology,vetted,cellprofiler,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_cellprofiler.h,,,tracker, 2D,EDGE_INTEGRATED_INTENSITY,morphology,vetted,cellprofiler,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_cellprofiler.h,,,tracker, -2D,CIRCULARITY,morphology,regression,,needs_audit,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_skimage.h,test_morphology_regression.h,pyradiomics:Sphericity(2D) (formula-identical),oracle-identified,tracker, -2D,CONVEX_HULL_AREA,morphology,regression,,sus,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h;test_convex_hull_invariants.py;test_morphology_skimage.h,test_morphology_regression.h,Document Nyxus point-hull convention or revise expected value.,promote-after-deepdive;suspected-bug,tracker, -2D,SOLIDITY,morphology,regression,,sus,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_convex_hull_invariants.py;test_morphology_skimage.h,test_morphology_regression.h,Document Nyxus point-hull convention or revise expected value.,promote-after-deepdive;suspected-bug,tracker, +2D,CIRCULARITY,morphology,vetted,analytic,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_skimage.h,test_morphology_regression.h,,no-external-oracle,tracker,"Tier-2 documented-formula conformance, NO external oracle. Nyxus computes CIRCULARITY = sqrt(4*pi*A)/P (convex_hull_nontriv.cpp) with A = ROI pixel count and P = contour perimeter. TEST_SHAPE2D_DOCUMENTED_FORMULA_CONFORMANCE_NO_EXTERNAL_ORACLE recomputes the formula from AREA_PIXELS_COUNT/PERIMETER and requires an exact match (1e-9), so the implementation is bug-free -- but the value is convention-specific (pixel-area + contour-perimeter) and no third-party tool reproduces it (pyradiomics/skimage use mesh area/perimeter). Formula-vetted only." +2D,CONVEX_HULL_AREA,morphology,vetted,skimage,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h;test_convex_hull_invariants.py;test_morphology_skimage.h,test_morphology_regression.h,Document Nyxus point-hull convention or revise expected value.,,tracker,"vetted vs scikit-image (convex_hull_image offset_coordinates=False, Pick's-theorem convex_area); test_morphology_skimage.h TEST_SHAPE2D_CONVEX_HULL_FEATURES passes. The prior matlab-based 'suspected-bug' (SOLIDITY 1.0 vs matlab 0.97) was a wrong-oracle artifact: Nyxus deliberately matches skimage, not matlab regionprops ConvexArea." +2D,SOLIDITY,morphology,vetted,skimage,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_convex_hull_invariants.py;test_morphology_skimage.h,test_morphology_regression.h,Document Nyxus point-hull convention or revise expected value.,,tracker,"vetted vs scikit-image (convex_hull_image offset_coordinates=False, Pick's-theorem convex_area); test_morphology_skimage.h TEST_SHAPE2D_CONVEX_HULL_FEATURES passes. The prior matlab-based 'suspected-bug' (SOLIDITY 1.0 vs matlab 0.97) was a wrong-oracle artifact: Nyxus deliberately matches skimage, not matlab regionprops ConvexArea." 2D,EROSIONS_2_VANISH,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, 2D,EROSIONS_2_VANISH_COMPLEMENT,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,FRACT_DIM_BOXCOUNT,morphology,regression,,needs_audit,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_fractal_dim_oracle.py;test_morphology_fraclac.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive,tracker, -2D,FRACT_DIM_PERIMETER,morphology,regression,,needs_audit,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_fractal_dim_oracle.py;test_morphology_fraclac.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive,tracker, +2D,FRACT_DIM_BOXCOUNT,morphology,vetted,fraclac,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_fractal_dim_oracle.py;test_morphology_fraclac.h,test_morphology_fraclac.h,,,tracker,"vetted vs ImageJ/FracLac box-count on the 512x512 blob fixture (tests/data/fractal_blob512_seg.ome.tif) = 1.8706, same-method direct match at 1% (test_morphology_fraclac.h). Also test_fractal_dim_oracle.py recovers analytic ground-truth dimensions: filled square -> 2.0, straight line -> 1.0, Sierpinski triangle -> 1.585." +2D,FRACT_DIM_PERIMETER,morphology,vetted,fraclac,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_fractal_dim_oracle.py;test_morphology_fraclac.h,test_morphology_fraclac.h,,,tracker,"vetted vs ImageJ/FracLac edge box-count on the blob512 fixture = 1.0493, cross-method vs Nyxus' Richardson divider (1.0572, 0.8% gap), asserted at 3% (test_morphology_fraclac.h). Also test_fractal_dim_oracle.py: Koch snowflake and smooth-disk perimeter dimensions match." 2D,MIN_FERET_ANGLE,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; do not mark agreed unless a tool matches within 5%.,promote-after-deepdive;convention-mismatch,tracker, 2D,MAX_FERET_ANGLE,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; do not mark agreed unless a tool matches within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_FERET_DIAM_MIN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; do not mark agreed unless a tool matches within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_FERET_DIAM_MAX,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; do not mark agreed unless a tool matches within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_FERET_DIAM_MEAN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_FERET_DIAM_MEDIAN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_FERET_DIAM_STDDEV,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_FERET_DIAM_MODE,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_MARTIN_DIAM_MIN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_MARTIN_DIAM_MAX,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_MARTIN_DIAM_MEAN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_MARTIN_DIAM_MEDIAN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_MARTIN_DIAM_STDDEV,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_MARTIN_DIAM_MODE,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_NASSENSTEIN_DIAM_MIN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_NASSENSTEIN_DIAM_MAX,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_NASSENSTEIN_DIAM_MEAN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_NASSENSTEIN_DIAM_MEDIAN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_NASSENSTEIN_DIAM_STDDEV,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, -2D,STAT_NASSENSTEIN_DIAM_MODE,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, +2D,STAT_FERET_DIAM_MIN,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length feret_diameters, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_FERET_IMEA_ELLIPSE_ORACLE. Correct rotating-calipers impl (caliper_feret.cpp, width of the rotated convex-hull AABB per angle); no code change needed. MIN/MAX_FERET_ANGLE stay regression (Nyxus-frame angle convention, no directly comparable imea output)." +2D,STAT_FERET_DIAM_MAX,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length feret_diameters, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_FERET_IMEA_ELLIPSE_ORACLE. Correct rotating-calipers impl (caliper_feret.cpp, width of the rotated convex-hull AABB per angle); no code change needed. MIN/MAX_FERET_ANGLE stay regression (Nyxus-frame angle convention, no directly comparable imea output)." +2D,STAT_FERET_DIAM_MEAN,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length feret_diameters, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_FERET_IMEA_ELLIPSE_ORACLE. Correct rotating-calipers impl (caliper_feret.cpp, width of the rotated convex-hull AABB per angle); no code change needed. MIN/MAX_FERET_ANGLE stay regression (Nyxus-frame angle convention, no directly comparable imea output)." +2D,STAT_FERET_DIAM_MEDIAN,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length feret_diameters, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_FERET_IMEA_ELLIPSE_ORACLE. Correct rotating-calipers impl (caliper_feret.cpp, width of the rotated convex-hull AABB per angle); no code change needed. MIN/MAX_FERET_ANGLE stay regression (Nyxus-frame angle convention, no directly comparable imea output)." +2D,STAT_FERET_DIAM_STDDEV,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length feret_diameters, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_FERET_IMEA_ELLIPSE_ORACLE. Correct rotating-calipers impl (caliper_feret.cpp, width of the rotated convex-hull AABB per angle); no code change needed. MIN/MAX_FERET_ANGLE stay regression (Nyxus-frame angle convention, no directly comparable imea output)." +2D,STAT_FERET_DIAM_MODE,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length feret_diameters, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_FERET_IMEA_ELLIPSE_ORACLE. Correct rotating-calipers impl (caliper_feret.cpp, width of the rotated convex-hull AABB per angle); no code change needed. MIN/MAX_FERET_ANGLE stay regression (Nyxus-frame angle convention, no directly comparable imea output)." +2D,STAT_MARTIN_DIAM_MIN,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_MARTIN_DIAM_MAX,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_MARTIN_DIAM_MEAN,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_MARTIN_DIAM_MEDIAN,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_MARTIN_DIAM_STDDEV,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_MARTIN_DIAM_MODE,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_NASSENSTEIN_DIAM_MIN,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_NASSENSTEIN_DIAM_MAX,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_NASSENSTEIN_DIAM_MEAN,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_NASSENSTEIN_DIAM_MEDIAN,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_NASSENSTEIN_DIAM_STDDEV,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." +2D,STAT_NASSENSTEIN_DIAM_MODE,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs imea (imea.measure_2d.statistical_length, dalpha=10) on a filled ellipse a=20 b=10; robust stats (min/max/mean/median) agree within the ~1-2px hull-vs-raster convention gap (15% reltol) in TEST_SHAPE2D_CALIPER_MARTIN_NASSENSTEIN_IMEA_ELLIPSE_ORACLE. Fixed a bug: caliper_martin.cpp and caliper_nassenstein.cpp shared byte-identical code that pushed BOTH the shortest and longest of a Y-grid of horizontal chords per angle, so Nassenstein min/mode were 0.0 (impossible for a solid shape). Now Martin = area-bisecting horizontal chord and Nassenstein = bottom-tangent vertical chord, one diameter per rotation angle." 2D,MAXCHORDS_MAX,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h;test_feature_oracle.py,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, 2D,MAXCHORDS_MAX_ANG,morphology,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h;test_feature_oracle.py,test_morphology_regression.h,none (imea gives lengths not angles),no-mainstream-oracle,tracker, 2D,MAXCHORDS_MIN,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h;test_feature_oracle.py,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, @@ -109,26 +109,26 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,EULER_NUMBER,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_matlab.h,,,tracker, 2D,EXTREMA_P1_X,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker, 2D,EXTREMA_P1_Y,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker, -2D,EXTREMA_P2_X,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_regression.h,matlab,promote-after-deepdive;convention-mismatch,tracker, +2D,EXTREMA_P2_X,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker,"vetted vs MATLAB/Octave regionprops('Extrema'). These are the right/bottom-edge coordinates; the earlier '~1px off' was a harness bug (uniform -0.5). MATLAB returns 1-based sub-pixel corner coords with a direction-specific corner: right/bottom coords map to Nyxus 0-based centers as (matlab-1.5), left/top as (matlab-0.5). Under that convention Nyxus matches regionprops EXACTLY (verified octave/matlab_oracle_tests/extrema_8x8.m; goldens in test_morphology_common.h). test_morphology_matlab.h asserts all 16 EXTREMA components." 2D,EXTREMA_P2_Y,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker, -2D,EXTREMA_P3_X,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_regression.h,matlab,promote-after-deepdive;convention-mismatch,tracker, +2D,EXTREMA_P3_X,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker,"vetted vs MATLAB/Octave regionprops('Extrema'). These are the right/bottom-edge coordinates; the earlier '~1px off' was a harness bug (uniform -0.5). MATLAB returns 1-based sub-pixel corner coords with a direction-specific corner: right/bottom coords map to Nyxus 0-based centers as (matlab-1.5), left/top as (matlab-0.5). Under that convention Nyxus matches regionprops EXACTLY (verified octave/matlab_oracle_tests/extrema_8x8.m; goldens in test_morphology_common.h). test_morphology_matlab.h asserts all 16 EXTREMA components." 2D,EXTREMA_P3_Y,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker, -2D,EXTREMA_P4_X,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_regression.h,matlab,promote-after-deepdive;convention-mismatch,tracker, -2D,EXTREMA_P4_Y,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_regression.h,matlab,promote-after-deepdive;convention-mismatch,tracker, -2D,EXTREMA_P5_X,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_regression.h,matlab,promote-after-deepdive;convention-mismatch,tracker, -2D,EXTREMA_P5_Y,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_regression.h,matlab,promote-after-deepdive;convention-mismatch,tracker, +2D,EXTREMA_P4_X,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker,"vetted vs MATLAB/Octave regionprops('Extrema'). These are the right/bottom-edge coordinates; the earlier '~1px off' was a harness bug (uniform -0.5). MATLAB returns 1-based sub-pixel corner coords with a direction-specific corner: right/bottom coords map to Nyxus 0-based centers as (matlab-1.5), left/top as (matlab-0.5). Under that convention Nyxus matches regionprops EXACTLY (verified octave/matlab_oracle_tests/extrema_8x8.m; goldens in test_morphology_common.h). test_morphology_matlab.h asserts all 16 EXTREMA components." +2D,EXTREMA_P4_Y,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker,"vetted vs MATLAB/Octave regionprops('Extrema'). These are the right/bottom-edge coordinates; the earlier '~1px off' was a harness bug (uniform -0.5). MATLAB returns 1-based sub-pixel corner coords with a direction-specific corner: right/bottom coords map to Nyxus 0-based centers as (matlab-1.5), left/top as (matlab-0.5). Under that convention Nyxus matches regionprops EXACTLY (verified octave/matlab_oracle_tests/extrema_8x8.m; goldens in test_morphology_common.h). test_morphology_matlab.h asserts all 16 EXTREMA components." +2D,EXTREMA_P5_X,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker,"vetted vs MATLAB/Octave regionprops('Extrema'). These are the right/bottom-edge coordinates; the earlier '~1px off' was a harness bug (uniform -0.5). MATLAB returns 1-based sub-pixel corner coords with a direction-specific corner: right/bottom coords map to Nyxus 0-based centers as (matlab-1.5), left/top as (matlab-0.5). Under that convention Nyxus matches regionprops EXACTLY (verified octave/matlab_oracle_tests/extrema_8x8.m; goldens in test_morphology_common.h). test_morphology_matlab.h asserts all 16 EXTREMA components." +2D,EXTREMA_P5_Y,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker,"vetted vs MATLAB/Octave regionprops('Extrema'). These are the right/bottom-edge coordinates; the earlier '~1px off' was a harness bug (uniform -0.5). MATLAB returns 1-based sub-pixel corner coords with a direction-specific corner: right/bottom coords map to Nyxus 0-based centers as (matlab-1.5), left/top as (matlab-0.5). Under that convention Nyxus matches regionprops EXACTLY (verified octave/matlab_oracle_tests/extrema_8x8.m; goldens in test_morphology_common.h). test_morphology_matlab.h asserts all 16 EXTREMA components." 2D,EXTREMA_P6_X,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker, -2D,EXTREMA_P6_Y,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_regression.h,matlab,promote-after-deepdive;convention-mismatch,tracker, +2D,EXTREMA_P6_Y,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker,"vetted vs MATLAB/Octave regionprops('Extrema'). These are the right/bottom-edge coordinates; the earlier '~1px off' was a harness bug (uniform -0.5). MATLAB returns 1-based sub-pixel corner coords with a direction-specific corner: right/bottom coords map to Nyxus 0-based centers as (matlab-1.5), left/top as (matlab-0.5). Under that convention Nyxus matches regionprops EXACTLY (verified octave/matlab_oracle_tests/extrema_8x8.m; goldens in test_morphology_common.h). test_morphology_matlab.h asserts all 16 EXTREMA components." 2D,EXTREMA_P7_X,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker, -2D,EXTREMA_P7_Y,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_regression.h,matlab,promote-after-deepdive;convention-mismatch,tracker, +2D,EXTREMA_P7_Y,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker,"vetted vs MATLAB/Octave regionprops('Extrema'). These are the right/bottom-edge coordinates; the earlier '~1px off' was a harness bug (uniform -0.5). MATLAB returns 1-based sub-pixel corner coords with a direction-specific corner: right/bottom coords map to Nyxus 0-based centers as (matlab-1.5), left/top as (matlab-0.5). Under that convention Nyxus matches regionprops EXACTLY (verified octave/matlab_oracle_tests/extrema_8x8.m; goldens in test_morphology_common.h). test_morphology_matlab.h asserts all 16 EXTREMA components." 2D,EXTREMA_P8_X,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker, 2D,EXTREMA_P8_Y,morphology,vetted,matlab,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_matlab.h,test_morphology_matlab.h,,,tracker, 2D,POLYGONALITY_AVE,morphology,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,none (Nyxus/WIPP-unique),no-mainstream-oracle,tracker, 2D,HEXAGONALITY_AVE,morphology,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,none (Nyxus/WIPP-unique),no-mainstream-oracle,tracker, 2D,HEXAGONALITY_STDDEV,morphology,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,none (Nyxus/WIPP-unique),no-mainstream-oracle,tracker, -2D,DIAMETER_MIN_ENCLOSING_CIRCLE,morphology,regression,,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,imea:min_enclosing_circle (returns diameter),oracle-identified,tracker, -2D,DIAMETER_CIRCUMSCRIBING_CIRCLE,morphology,regression,,needs_audit,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive,tracker, -2D,DIAMETER_INSCRIBING_CIRCLE,morphology,regression,,needs_audit,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive,tracker, +2D,DIAMETER_MIN_ENCLOSING_CIRCLE,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,,tracker,"vetted vs OpenCV/imea minimum enclosing circle (Welzl; cv2.minEnclosingCircle, imea min_enclosing_circle) on clean fixtures: ellipse a=20 -> 2a=40, circle r=15 -> 30; centroid-independent, matches to <0.1% in TEST_SHAPE2D_MIN_ENCLOSING_CIRCLE_IMEA_ORACLE." +2D,DIAMETER_CIRCUMSCRIBING_CIRCLE,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,convention-mismatch,tracker,"Left regression: Nyxus (circle.cpp, ported from imea macro.py) computes this as imea's max/min centroid-to-contour distance approximation, NOT a true geometric circle. It is convention-sensitive (Nyxus contour tracing + the centroid-1 offset): a symmetric circle r=15 yields circ=35.6/insc=23.3 instead of ~30/~30, so no external numeric oracle agrees -> documented-convention, not promoted. (DIAMETER_MIN_ENCLOSING_CIRCLE IS vetted vs cv2/imea.)" +2D,DIAMETER_INSCRIBING_CIRCLE,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,,convention-mismatch,tracker,"Left regression: Nyxus (circle.cpp, ported from imea macro.py) computes this as imea's max/min centroid-to-contour distance approximation, NOT a true geometric circle. It is convention-sensitive (Nyxus contour tracing + the centroid-1 offset): a symmetric circle r=15 yields circ=35.6/insc=23.3 instead of ~30/~30, so no external numeric oracle agrees -> documented-convention, not promoted. (DIAMETER_MIN_ENCLOSING_CIRCLE IS vetted vs cv2/imea.)" 2D,GEODETIC_LENGTH,morphology,regression,,benign,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Definition review; keep as verifiable convention mismatch unless another oracle agrees within 5%.,promote-after-deepdive;convention-mismatch,tracker, 2D,THICKNESS,morphology,vetted,imea,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_imea.h,,,tracker, 2D,ROI_RADIUS_MEAN,morphology,regression,,needs_audit,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_morphology_regression.h,test_morphology_regression.h,Contour-convention audit,promote-after-deepdive,tracker, @@ -144,9 +144,9 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,ANG_BW_NEIGHBORS_STDDEV,neighbor,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_neighbor_regression.h,test_neighbor_regression.h,none (CP AngleBetweenNeighbors differs) -> scipy analytic,no-mainstream-oracle,tracker, 2D,ANG_BW_NEIGHBORS_MODE,neighbor,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_neighbor_regression.h,test_neighbor_regression.h,none (CP AngleBetweenNeighbors differs) -> scipy analytic,no-mainstream-oracle,tracker, 2D,GLCM_ASM,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h;test_nyxus.py,test_glcm_matlab.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -2D,GLCM_ACOR,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_pyradiomics.py;test_glcm_ibsi.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +2D,GLCM_ACOR,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_pyradiomics.py;test_glcm_ibsi.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_CLUPROM,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -2D,GLCM_CLUSHADE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +2D,GLCM_CLUSHADE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_CLUTEND,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_CONTRAST,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_pyradiomics.py;test_glcm_ibsi.h;test_nyxus.py,test_glcm_matlab.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_CORRELATION,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h;test_nyxus.py,test_glcm_matlab.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A @@ -155,28 +155,28 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,GLCM_DIFVAR,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h;test_nyxus.py,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_DIS,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_ENERGY,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_nyxus.py,test_glcm_matlab.h,,,tracker, -2D,GLCM_ENTROPY,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_nyxus.py,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, +2D,GLCM_ENTROPY,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_nyxus.py,test_glcm_regression.h,MIRP/IBSI check,,tracker,"FIXED (missing /sum_p normalization added to f_GLCM_HOM2/f_entropy) + vetted vs MIRP/IBSI: HOM2==IDM 0.619, ENTROPY==JE 2.05. New IBSI gtests TEST_IBSI_GLCM_HOM2/ENTROPY pin it; regression goldens updated" 2D,GLCM_HOM1,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_nyxus.py,test_glcm_matlab.h,,,tracker, -2D,GLCM_HOM2,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, +2D,GLCM_HOM2,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"FIXED (missing /sum_p normalization added to f_GLCM_HOM2/f_entropy) + vetted vs MIRP/IBSI: HOM2==IDM 0.619, ENTROPY==JE 2.05. New IBSI gtests TEST_IBSI_GLCM_HOM2/ENTROPY pin it; regression goldens updated" 2D,GLCM_ID,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_IDN,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_pyradiomics.py;test_glcm_ibsi.h,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_IDM,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h;test_nyxus.py,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_IDMN,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_pyradiomics.py;test_glcm_ibsi.h,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -2D,GLCM_INFOMEAS1,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h;test_nyxus.py,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +2D,GLCM_INFOMEAS1,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h;test_nyxus.py,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_INFOMEAS2,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h;test_nyxus.py,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_IV,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -2D,GLCM_JAVE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +2D,GLCM_JAVE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_JE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_JMAX,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -2D,GLCM_JVAR,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -2D,GLCM_SUMAVERAGE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_pyradiomics.py;test_glcm_ibsi.h;test_nyxus.py,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +2D,GLCM_JVAR,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" +2D,GLCM_SUMAVERAGE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_pyradiomics.py;test_glcm_ibsi.h;test_nyxus.py,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_SUMENTROPY,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_glcm_regression.h;test_glcm_ibsi.h;test_nyxus.py,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_SUMVARIANCE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glcm.h;test_glcm_regression.h;test_glcm_ibsi.h;test_nyxus.py,test_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 2D,GLCM_VARIANCE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_nyxus.py,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_ASM_AVE,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_matlab.h,,,tracker, -2D,GLCM_ACOR_AVE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_glcm_pyradiomics.py,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, +2D,GLCM_ACOR_AVE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_glcm_pyradiomics.py,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_CLUPROM_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, -2D,GLCM_CLUSHADE_AVE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, +2D,GLCM_CLUSHADE_AVE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_CLUTEND_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_CONTRAST_AVE,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_glcm_pyradiomics.py,test_glcm_matlab.h,,,tracker, 2D,GLCM_CORRELATION_AVE,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_matlab.h,,,tracker, @@ -185,21 +185,21 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,GLCM_DIFVAR_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_DIS_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_ENERGY_AVE,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_matlab.h,,,tracker, -2D,GLCM_ENTROPY_AVE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, +2D,GLCM_ENTROPY_AVE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"FIXED (missing /sum_p normalization added to f_GLCM_HOM2/f_entropy) + vetted vs MIRP/IBSI: HOM2==IDM 0.619, ENTROPY==JE 2.05. New IBSI gtests TEST_IBSI_GLCM_HOM2/ENTROPY pin it; regression goldens updated" 2D,GLCM_HOM1_AVE,glcm,vetted,matlab,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_matlab.h,,,tracker, 2D,GLCM_ID_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_IDN_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_glcm_pyradiomics.py,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_IDM_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_IDMN_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_glcm_pyradiomics.py,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_IV_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, -2D,GLCM_JAVE_AVE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, +2D,GLCM_JAVE_AVE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_JE_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, -2D,GLCM_INFOMEAS1_AVE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, +2D,GLCM_INFOMEAS1_AVE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_INFOMEAS2_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_VARIANCE_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_JMAX_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, -2D,GLCM_JVAR_AVE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, -2D,GLCM_SUMAVERAGE_AVE,glcm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_glcm_pyradiomics.py,test_glcm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, +2D,GLCM_JVAR_AVE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" +2D,GLCM_SUMAVERAGE_AVE,glcm,vetted,mirp,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h;test_glcm_pyradiomics.py,test_glcm_regression.h,MIRP/IBSI check,,tracker,"vetted vs MIRP (IBSI digital phantom, glcm_oracle/); MIRP reproduces IBSI consensus exactly and Nyxus IBSI gtest passes (ACOR 5.09, CLUSHADE 7, INFOMEAS1 -0.155, JAVE 2.14, JVAR 2.69, SUMAVERAGE 4.28); _AVE = angle-average of the vetted base" 2D,GLCM_SUMENTROPY_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, 2D,GLCM_SUMVARIANCE_AVE,glcm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glcm_regression.h,test_glcm_pyradiomics.h,,,tracker, 2D,GLRLM_SRE,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A @@ -212,12 +212,12 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,GLRLM_GLV,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A 2D,GLRLM_RV,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A 2D,GLRLM_RE,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -2D,GLRLM_LGLRE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -2D,GLRLM_HGLRE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -2D,GLRLM_SRLGLE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -2D,GLRLM_SRHGLE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -2D,GLRLM_LRLGLE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -2D,GLRLM_LRHGLE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +2D,GLRLM_LGLRE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM LGLRE = 0.604) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions. IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 0.6044 (matches the IBSI reference); ibsi=false gives 0.1147 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_HGLRE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM HGLRE = 9.82) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions. IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 9.824 (matches the IBSI reference); ibsi=false gives 4087.74 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_SRLGLE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM SRLGLE = 0.294) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions. IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 0.2940 (matches the IBSI reference); ibsi=false gives 0.1036 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_SRHGLE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM SRHGLE = 8.57) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions. IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 8.573 (matches the IBSI reference); ibsi=false gives 3545.21 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_LRLGLE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM LRLGLE = 3.14) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions. IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 3.144 (matches the IBSI reference); ibsi=false gives 0.1614 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_LRHGLE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_glrlm_regression.h;test_glrlm_ibsi.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM LRHGLE = 17.4) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions. IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 17.387 (matches the IBSI reference); ibsi=false gives 7358.78 on the same fixture -- default-mode values are NOT vetted." 2D,GLRLM_SRE_AVE,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_pyradiomics.h,,,tracker, 2D,GLRLM_LRE_AVE,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_pyradiomics.h,,,tracker, 2D,GLRLM_GLN_AVE,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_pyradiomics.h,,,tracker, @@ -228,12 +228,12 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,GLRLM_GLV_AVE,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_pyradiomics.h,,,tracker, 2D,GLRLM_RV_AVE,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_pyradiomics.h,,,tracker, 2D,GLRLM_RE_AVE,glrlm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_pyradiomics.h,,,tracker, -2D,GLRLM_LGLRE_AVE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, -2D,GLRLM_HGLRE_AVE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, -2D,GLRLM_SRLGLE_AVE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, -2D,GLRLM_SRHGLE_AVE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, -2D,GLRLM_LRLGLE_AVE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, -2D,GLRLM_LRHGLE_AVE,glrlm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_glrlm_regression.h,test_glrlm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker, +2D,GLRLM_LGLRE_AVE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_glrlm_regression.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM LGLRE = 0.604) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions (_AVE direction-mean, same grand mean). IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 0.6044 (matches the IBSI reference); ibsi=false gives 0.1147 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_HGLRE_AVE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_glrlm_regression.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM HGLRE = 9.82) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions (_AVE direction-mean, same grand mean). IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 9.824 (matches the IBSI reference); ibsi=false gives 4087.74 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_SRLGLE_AVE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_glrlm_regression.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM SRLGLE = 0.294) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions (_AVE direction-mean, same grand mean). IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 0.2940 (matches the IBSI reference); ibsi=false gives 0.1036 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_SRHGLE_AVE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_glrlm_regression.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM SRHGLE = 8.57) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions (_AVE direction-mean, same grand mean). IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 8.573 (matches the IBSI reference); ibsi=false gives 3545.21 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_LRLGLE_AVE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_glrlm_regression.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM LRLGLE = 3.14) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions (_AVE direction-mean, same grand mean). IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 3.144 (matches the IBSI reference); ibsi=false gives 0.1614 on the same fixture -- default-mode values are NOT vetted." +2D,GLRLM_LRHGLE_AVE,glrlm,vetted,ibsi,agreed,"glrlm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over 4 slices x 4 directions. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_glrlm_regression.h,test_glrlm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLRLM LRHGLE = 17.4) at 1% in test_glrlm_ibsi.h, ibsi=True Ng=128, averaged over the 4 phantom slices x 4 directions (_AVE direction-mean, same grand mean). IBSI is the reference table (§4); no tool run at CI time. CONFIG SCOPE (measured): ibsi=true gives 17.387 (matches the IBSI reference); ibsi=false gives 7358.78 on the same fixture -- default-mode values are NOT vetted." 2D,GLDZM_SDE,gldzm,vetted,mirp,agreed,Mode-specific 2D texture row. MIRP checks use IBSI phantom slices with no discretization and by_slice=True; agreement is for that mode/scope.,,test_3d_gldzm_ibsi.h;test_gldzm_ibsi.h,test_gldzm_mirp.h,,,tracker, 2D,GLDZM_LDE,gldzm,vetted,mirp,agreed,Mode-specific 2D texture row. MIRP checks use IBSI phantom slices with no discretization and by_slice=True; agreement is for that mode/scope.,,test_3d_gldzm_ibsi.h;test_gldzm_ibsi.h,test_gldzm_mirp.h,,,tracker, 2D,GLDZM_LGLZE,gldzm,vetted,mirp,agreed,Mode-specific 2D texture row. MIRP checks use IBSI phantom slices with no discretization and by_slice=True; agreement is for that mode/scope.,,test_3d_gldzm_ibsi.h;test_gldzm_ibsi.h,test_gldzm_mirp.h,,,tracker, @@ -259,48 +259,48 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,GLSZM_SZN,glszm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A 2D,GLSZM_SZNN,glszm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A 2D,GLSZM_ZP,glszm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -2D,GLSZM_GLV,glszm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +2D,GLSZM_GLV,glszm,vetted,ibsi,agreed,"glszm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLSZM GLV = 3.97) at 1% in test_glszm_ibsi.h (ibsi=True Ng=128, averaged over the 4 phantom slices). The wired test already passed; reconciled from regression. IBSI reference table (SPEC §4). CONFIG SCOPE (measured): ibsi=true gives 3.9695 (matches the IBSI reference); ibsi=false gives 2227.54 on the same fixture -- default-mode values are NOT vetted." 2D,GLSZM_ZV,glszm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A 2D,GLSZM_ZE,glszm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -2D,GLSZM_LGLZE,glszm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -2D,GLSZM_HGLZE,glszm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +2D,GLSZM_LGLZE,glszm,vetted,ibsi,agreed,"glszm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLSZM LGLZE = 0.371) at 1% in test_glszm_ibsi.h (ibsi=True Ng=128, averaged over the 4 phantom slices). The wired test already passed; reconciled from regression. IBSI reference table (SPEC §4). CONFIG SCOPE (measured): ibsi=true gives 0.3712 (matches the IBSI reference); ibsi=false gives 0.1944 on the same fixture -- default-mode values are NOT vetted." +2D,GLSZM_HGLZE,glszm,vetted,ibsi,agreed,"glszm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLSZM HGLZE = 16.4) at 1% in test_glszm_ibsi.h (ibsi=True Ng=128, averaged over the 4 phantom slices). The wired test already passed; reconciled from regression. IBSI reference table (SPEC §4). CONFIG SCOPE (measured): ibsi=true gives 16.441 (matches the IBSI reference); ibsi=false gives 5985.62 on the same fixture -- default-mode values are NOT vetted." 2D,GLSZM_SALGLE,glszm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -2D,GLSZM_SAHGLE,glszm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -2D,GLSZM_LALGLE,glszm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -2D,GLSZM_LAHGLE,glszm,regression,,needs_audit,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_regression.h,MIRP/IBSI check,promote-after-deepdive,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_SDE,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_LDE,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_pyradiomics.py;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_GLN,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_pyradiomics.py;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_DN,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_pyradiomics.py;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_DNN,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_GLV,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_DV,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_DE,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_LGLE,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker, -2D,GLDM_HGLE,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_SDLGLE,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_SDHGLE,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_LDLGLE,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,GLDM_LDHGLE,gldm,regression,,sus,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_regression.h,MIRP/IBSI check,promote-after-deepdive;suspected-bug,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -2D,NGLDM_LDE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_HDE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_LGLCE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_HGLCE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_LDLGLE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_LDHGLE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_HDLGLE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_HDHGLE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_GLNU,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_GLNUN,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_DCNU,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_DCNUN,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_DCP,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_GLM,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Primitive-chain MIRP MatrixNGLDM checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_GLV,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_DCM,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Primitive-chain MIRP MatrixNGLDM checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_DCV,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_DCENT,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, -2D,NGLDM_DCENE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_ibsi.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,GLSZM_SAHGLE,glszm,vetted,ibsi,agreed,"glszm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLSZM SAHGLE = 10.3) at 1% in test_glszm_ibsi.h (ibsi=True Ng=128, averaged over the 4 phantom slices). The wired test already passed; reconciled from regression. IBSI reference table (SPEC §4). CONFIG SCOPE (measured): ibsi=true gives 10.278 (matches the IBSI reference); ibsi=false gives 3521.46 on the same fixture -- default-mode values are NOT vetted." +2D,GLSZM_LALGLE,glszm,vetted,ibsi,agreed,"glszm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLSZM LALGLE = 40.4) at 1% in test_glszm_ibsi.h (ibsi=True Ng=128, averaged over the 4 phantom slices). The wired test already passed; reconciled from regression. IBSI reference table (SPEC §4). CONFIG SCOPE (measured): ibsi=true gives 40.398 (matches the IBSI reference); ibsi=false gives 0.5908 on the same fixture -- default-mode values are NOT vetted." +2D,GLSZM_LAHGLE,glszm,vetted,ibsi,agreed,"glszm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights these grey-level-dependent features by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_glszm_regression.h;test_glszm_ibsi.h,test_glszm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom consensus (GLSZM LAHGLE = 113) at 1% in test_glszm_ibsi.h (ibsi=True Ng=128, averaged over the 4 phantom slices). The wired test already passed; reconciled from regression. IBSI reference table (SPEC §4). CONFIG SCOPE (measured): ibsi=true gives 112.52 (matches the IBSI reference); ibsi=false gives 44190.2 on the same fixture -- default-mode values are NOT vetted." +2D,GLDM_SDE,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 (MODE-INVARIANT) -- vetted vs IBSI consensus at ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4. MEASURED to be bit-identical at ibsi=false on the same fixture, so this feature is structure-only (no grey-level weighting) and the vetting holds in Nyxus' default mode too -- unconditional.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_SDE = 0.158) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true and ibsi=false both give 0.15807 -- MODE-INVARIANT, so the vetting also covers default mode." +2D,GLDM_LDE,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 (MODE-INVARIANT) -- vetted vs IBSI consensus at ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4. MEASURED to be bit-identical at ibsi=false on the same fixture, so this feature is structure-only (no grey-level weighting) and the vetting holds in Nyxus' default mode too -- unconditional.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_pyradiomics.py;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_LDE = 19.2) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true and ibsi=false both give 19.1738 -- MODE-INVARIANT, so the vetting also covers default mode." +2D,GLDM_GLN,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 (MODE-INVARIANT) -- vetted vs IBSI consensus at ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4. MEASURED to be bit-identical at ibsi=false on the same fixture, so this feature is structure-only (no grey-level weighting) and the vetting holds in Nyxus' default mode too -- unconditional.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_pyradiomics.py;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_GLN = 10.2) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true and ibsi=false both give 10.2464 -- MODE-INVARIANT, so the vetting also covers default mode." +2D,GLDM_DN,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 (MODE-INVARIANT) -- vetted vs IBSI consensus at ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4. MEASURED to be bit-identical at ibsi=false on the same fixture, so this feature is structure-only (no grey-level weighting) and the vetting holds in Nyxus' default mode too -- unconditional.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_pyradiomics.py;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_DN = 3.96) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true and ibsi=false both give 3.96465 -- MODE-INVARIANT, so the vetting also covers default mode." +2D,GLDM_DNN,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 (MODE-INVARIANT) -- vetted vs IBSI consensus at ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4. MEASURED to be bit-identical at ibsi=false on the same fixture, so this feature is structure-only (no grey-level weighting) and the vetting holds in Nyxus' default mode too -- unconditional.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_mechanics.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_DNN = 0.212) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true and ibsi=false both give 0.211772 -- MODE-INVARIANT, so the vetting also covers default mode." +2D,GLDM_GLV,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights this grey-level-dependent feature by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_GLV = 2.7) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true gives 2.7037 (matches the IBSI reference); ibsi=false gives 1221.07 on the same fixture -- default-mode values are NOT vetted." +2D,GLDM_DV,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 (MODE-INVARIANT) -- vetted vs IBSI consensus at ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4. MEASURED to be bit-identical at ibsi=false on the same fixture, so this feature is structure-only (no grey-level weighting) and the vetting holds in Nyxus' default mode too -- unconditional.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_DV = 2.73) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true and ibsi=false both give 2.7295 -- MODE-INVARIANT, so the vetting also covers default mode." +2D,GLDM_DE,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 (MODE-INVARIANT) -- vetted vs IBSI consensus at ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4. MEASURED to be bit-identical at ibsi=false on the same fixture, so this feature is structure-only (no grey-level weighting) and the vetting holds in Nyxus' default mode too -- unconditional.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_DE = 2.71) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true and ibsi=false both give 2.71215 -- MODE-INVARIANT, so the vetting also covers default mode." +2D,GLDM_LGLE,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights this grey-level-dependent feature by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_LGLE = 0.702) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true gives 0.70175 (matches the IBSI reference); ibsi=false gives 0.0014516 on the same fixture -- default-mode values are NOT vetted." +2D,GLDM_HGLE,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights this grey-level-dependent feature by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_HGLE = 7.49) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true gives 7.48695 (matches the IBSI reference); ibsi=false gives 3452.84 on the same fixture -- default-mode values are NOT vetted." +2D,GLDM_SDLGLE,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights this grey-level-dependent feature by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_SDLGLE = 0.0473) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true gives 0.0472905 (matches the IBSI reference); ibsi=false gives 9.8277e-05 on the same fixture -- default-mode values are NOT vetted." +2D,GLDM_SDHGLE,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights this grey-level-dependent feature by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_SDHGLE = 3.06) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true gives 3.06491 (matches the IBSI reference); ibsi=false gives 1402.44 on the same fixture -- default-mode values are NOT vetted." +2D,GLDM_LDLGLE,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights this grey-level-dependent feature by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_LDLGLE = 17.6) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true gives 17.5997 (matches the IBSI reference); ibsi=false gives 0.0363725 on the same fixture -- default-mode values are NOT vetted." +2D,GLDM_LDHGLE,gldm,vetted,ibsi,agreed,"gldm.ibsi_ng128 -- ibsi=true, GREYDEPTH=128, IBSI digital phantom z1..z4, mean over the 4 slices. VETTED ONLY ON THIS RECIPE: Nyxus' default mode (ibsi=false) weights this grey-level-dependent feature by RAW intensity instead of the grey-level index, and is NOT covered by this assertion.",,test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_gldm_regression.h;test_gldm_ibsi.h,test_gldm_ibsi.h,,,tracker,"vetted vs IBSI digital-phantom NGLDM consensus (GLDM_LDHGLE = 49.5) at 1% in test_gldm_ibsi.h (ibsi=True Ng=128, 4 phantom slices). Nyxus GLDM is the pyradiomics-lineage naming of the neighbouring grey-level-dependence matrix that IBSI calls NGLDM (same quantity; verified the gldm and ngldm IBSI reference tables are identical). The wired test already passed; reconciled from regression. CONFIG SCOPE (measured): ibsi=true gives 49.4777 (matches the IBSI reference); ibsi=false gives 23183.9 on the same fixture -- default-mode values are NOT vetted." +2D,NGLDM_LDE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_HDE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_LGLCE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_HGLCE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_LDLGLE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_LDHGLE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_HDLGLE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_HDHGLE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_GLNU,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_GLNUN,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_DCNU,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_DCNUN,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_DCP,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_GLM,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Primitive-chain MIRP MatrixNGLDM checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_GLV,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_DCM,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Primitive-chain MIRP MatrixNGLDM checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_DCV,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_DCENT,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, +2D,NGLDM_DCENE,ngldm,vetted,mirp,agreed,"Mode-specific 2D texture row. Direct MIRP checks use Nyxus IBSI=true/no-binning IBSI phantom slices with MIRP base_discretisation_method=""none"", by_slice=True; GLM/DCM remain no-direct-oracle.",,test_3d_ngldm_regression.h;test_ngldm_ibsi.h,test_ngldm_mirp.h,,,tracker, 2D,NGTDM_COARSENESS,ngtdm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_ngtdm_ibsi.h;test_ngtdm_regression.h,test_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A 2D,NGTDM_CONTRAST,ngtdm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_ngtdm_ibsi.h;test_ngtdm_regression.h,test_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A 2D,NGTDM_BUSYNESS,ngtdm,vetted,pyradiomics,agreed,"Mode-specific 2D texture row. PyRadiomics/MIRP comparison is tied to verifier settings such as force2D/by_slice, binWidth/binCount or no discretization, symmetry, and aggregation.",,test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_ngtdm_ibsi.h;test_ngtdm_regression.h,test_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A @@ -367,8 +367,8 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,HU_M2,moments,vetted,skimage,agreed,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_moments_skimage.h,test_moments_skimage.h,,,tracker, 2D,HU_M3,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix 2D,HU_M4,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix -2D,HU_M5,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix -2D,HU_M6,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix +2D,HU_M5,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix; h5 9x-bracket formula fixed and vetted on the asymmetric wedge (test_moments_hu_wedge_skimage; gen_moments_skimage.py) +2D,HU_M6,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix; h6 stray-eta03 precedence bug fixed and vetted on the asymmetric wedge (test_moments_hu_wedge_skimage; gen_moments_skimage.py) 2D,HU_M7,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix 2D,WEIGHTED_SPAT_MOMENT_00,moments,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_moments_regression.h,test_moments_regression.h,none (Nyxus-specific weighted moment),oracle-identified,tracker,"no mainstream oracle: skimage weighted moments correspond to Nyxus IMOM_ (non-W) family, already vetted; the W-variants are a distinct Nyxus/WIPP-specific weighting (IMOM_WRM_00=512893 vs sum-intensity=346635)" 2D,WEIGHTED_SPAT_MOMENT_01,moments,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_moments_regression.h,test_moments_regression.h,none (Nyxus-specific weighted moment),oracle-identified,tracker,"no mainstream oracle: skimage weighted moments correspond to Nyxus IMOM_ (non-W) family, already vetted; the W-variants are a distinct Nyxus/WIPP-specific weighting (IMOM_WRM_00=512893 vs sum-intensity=346635)" @@ -458,7 +458,7 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,IMOM_HU3,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix 2D,IMOM_HU4,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix 2D,IMOM_HU5,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix -2D,IMOM_HU6,moments,regression,,needs_audit,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_moments_regression.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,skimage flag rel=35956418749.65425 (residual near-zero) +2D,IMOM_HU6,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted after the calcHu_imp h6 precedence fix; the old skimage flag rel=3.6e10 WAS the bug (h6 emitted the raw eta03 == IMOM_NCM_03; gen_moments_skimage.py) 2D,IMOM_HU7,moments,vetted,skimage,exact (rel=0.0e+00),moments.skimage_regionprops,exact,test_moments_regression.h;test_moments_skimage.h,test_moments_regression.h,Document convention or find another direct built-in oracle.,promote-after-deepdive,tracker,PROMOTED regression->vetted vs skimage regionprops after centroid-truncation fix 2D,IMOM_WRM_00,moments,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_moments_regression.h,test_moments_regression.h,none (Nyxus-specific weighted moment),oracle-identified,tracker,"no mainstream oracle: skimage weighted moments correspond to Nyxus IMOM_ (non-W) family, already vetted; the W-variants are a distinct Nyxus/WIPP-specific weighting (IMOM_WRM_00=512893 vs sum-intensity=346635)" 2D,IMOM_WRM_01,moments,regression,,na,"Not mode-specific. Any disagreement is a definition, coordinate-frame, spacing, aggregation, or implementation issue rather than Nyxus IBSI/radiomics mode.",,test_moments_regression.h,test_moments_regression.h,none (Nyxus-specific weighted moment),oracle-identified,tracker,"no mainstream oracle: skimage weighted moments correspond to Nyxus IMOM_ (non-W) family, already vetted; the W-variants are a distinct Nyxus/WIPP-specific weighting (IMOM_WRM_00=512893 vs sum-intensity=346635)" @@ -538,56 +538,56 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,IH_ROBUST_MEAN_IDX,intensity_histogram,vetted,analytic,,ih.ibsi_fbn,,test_intensity_histogram_ibsi.h,,mirp (IBSI Intensity-Histogram family); vet _IDX,not-in-tracker;oracle-identified,audit,"vetted by test_ih_dispersion_robust_analytic (hand-computed on the robust-window discriminating fixture; no clean IBSI anchor - robust-mean has no IBSI feature; QCoD_VAL/IQR_VAL are Nyxus continuous-percentile extensions of the IBSI discrete-percentile IQR/QCoD, so they have no IBSI counterpart by design)" 2D,IH_NUM_BINS,intensity_histogram,vetted,analytic,,,,test_intensity_histogram_regression.h;test_intensity_histogram.py,test_intensity_histogram_analytic.h,numpy/scipy analytic,not-in-tracker,audit, 2D,IH_BIN_SIZE,intensity_histogram,vetted,analytic,,,,test_intensity_histogram_regression.h;test_intensity_histogram.py,test_intensity_histogram_analytic.h,numpy/scipy analytic,not-in-tracker,audit, -3D,3COV,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,mirp:stat_cov (IBSI 7TET; no pyradiomics equiv),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3COVERED_IMAGE_INTENSITY_RANGE,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,none (Nyxus-specific: uses image dynamic range),no-mainstream-oracle,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3COV,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,mirp:stat_cov (IBSI 7TET; no pyradiomics equiv),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3COVERED_IMAGE_INTENSITY_RANGE,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,none (Nyxus-specific: (roi_max-roi_min)/(slide_max-slide_min)),bug-degenerate,tracker,BUG: 3D segmented workflow never sets roi.slide_idx (workflow_3d_segmented.cpp) so 3d_intensity.cpp takes the else branch and always emits degenerate 1; unvettable until slide_idx+min/max_preroi_inten are wired through the 3D pipeline (2D sets it via pixel_feed.cpp). Oracle (octave/oracle_3d) confirms intended value = (roi_range)/(slide_range) 3D,3ENERGY,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A 3D,3ENTROPY,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3EXCESS_KURTOSIS,firstorder,regression,,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3HYPERFLATNESS,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,none (scipy.moment order 5/6 only),no-mainstream-oracle,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3HYPERSKEWNESS,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,none (scipy.moment order 5/6 only),no-mainstream-oracle,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3INTEGRATED_INTENSITY,firstorder,regression,,needs_audit,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),analytic-trivial,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3EXCESS_KURTOSIS,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3HYPERFLATNESS,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,none (scipy.moment order 5/6 only),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3HYPERSKEWNESS,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,none (scipy.moment order 5/6 only),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3INTEGRATED_INTENSITY,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) 3D,3INTERQUARTILE_RANGE,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A 3D,3KURTOSIS,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A 3D,3MAX,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A 3D,3MEAN,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A 3D,3MEAN_ABSOLUTE_DEVIATION,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A 3D,3MEDIAN,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3MEDIAN_ABSOLUTE_DEVIATION,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,mirp:stat_medad (IBSI N72L),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3MEDIAN_ABSOLUTE_DEVIATION,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,mirp:stat_medad (IBSI N72L),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) 3D,3MIN,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3MODE,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,scipy.stats.mode on raw voxels (IBSI mode is on histogram),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3P01,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),analytic-trivial,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3MODE,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,scipy.stats.mode on raw voxels (IBSI mode is on histogram),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3P01,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) 3D,3P10,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3P25,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),analytic-trivial,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3P75,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),analytic-trivial,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3P25,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) +3D,3P75,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) 3D,3P90,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3P99,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),analytic-trivial,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3QCOD,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3P99,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) +3D,3QCOD,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) 3D,3RANGE,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3ROBUST_MEAN,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3ROBUST_MEAN,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,octave/MATLAB (mean of voxels in [P10,P90]),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; rel<1e-9) after fixing 3ROBUST_MEAN which was hardcoded 0 in both 3d_intensity.cpp paths 3D,3ROBUST_MEAN_ABSOLUTE_DEVIATION,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A 3D,3ROOT_MEAN_SQUARED,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A 3D,3SKEWNESS,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3STANDARD_DEVIATION,firstorder,regression,,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3STANDARD_DEVIATION_BIASED,firstorder,regression,,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3STANDARD_ERROR,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3STANDARD_DEVIATION,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3STANDARD_DEVIATION_BIASED,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3STANDARD_ERROR,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) 3D,3VARIANCE,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3VARIANCE_BIASED,firstorder,regression,,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3VARIANCE_BIASED,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) 3D,3UNIFORMITY,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3UNIFORMITY_PIU,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),oracle-identified,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3AREA,morphology,regression,,needs_audit,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,matlab,promote-after-deepdive,tracker, -3D,3AREA_2_VOLUME,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:SurfaceVolumeRatio(3D),oracle-identified,tracker, -3D,3COMPACTNESS1,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:Compactness1/Compactness2(3D),oracle-identified,tracker, -3D,3COMPACTNESS2,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:Compactness1/Compactness2(3D),oracle-identified,tracker, +3D,3UNIFORMITY_PIU,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3AREA,morphology,regression,,needs_audit,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,matlab,convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3AREA_2_VOLUME,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:SurfaceVolumeRatio(3D),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3COMPACTNESS1,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:Compactness1/Compactness2(3D),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3COMPACTNESS2,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:Compactness1/Compactness2(3D),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." 3D,3MESH_VOLUME,morphology,vetted,matlab,agreed,,,test_3d_morphology_coverage.h;test_3d_morphology_matlab.h,test_3d_morphology_matlab.h,,,tracker, -3D,3SPHERICAL_DISPROPORTION,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:SphericalDisproportion(3D shape),oracle-identified,tracker, -3D,3SPHERICITY,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:Sphericity(3D shape),oracle-identified,tracker, +3D,3SPHERICAL_DISPROPORTION,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:SphericalDisproportion(3D shape),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3SPHERICITY,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:Sphericity(3D shape),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." 3D,3VOLUME_CONVEXHULL,morphology,vetted,matlab,agreed,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_matlab.h,,,tracker, 3D,3VOXEL_VOLUME,morphology,vetted,matlab,agreed,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_matlab.h,,,tracker, -3D,3MAJOR_AXIS_LEN,morphology,regression,,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:MajorAxisLength(3D shape),oracle-identified,tracker, -3D,3MINOR_AXIS_LEN,morphology,regression,,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:MinorAxisLength(3D shape),oracle-identified,tracker, -3D,3LEAST_AXIS_LEN,morphology,regression,,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:LeastAxisLength(3D shape),oracle-identified,tracker, -3D,3ELONGATION,morphology,regression,,benign,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:Elongation(3D shape),oracle-identified,tracker, -3D,3FLATNESS,morphology,regression,,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:Flatness(3D shape),oracle-identified,tracker, +3D,3MAJOR_AXIS_LEN,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:MajorAxisLength(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" +3D,3MINOR_AXIS_LEN,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:MinorAxisLength(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" +3D,3LEAST_AXIS_LEN,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:LeastAxisLength(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" +3D,3ELONGATION,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:Elongation(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" +3D,3FLATNESS,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:Flatness(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" 3D,3GLCM_ACOR,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 3D,3GLCM_ASM,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 3D,3GLCM_CLUPROM,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A @@ -598,11 +598,11 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 3D,3GLCM_DIFAVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 3D,3GLCM_DIFENTRO,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 3D,3GLCM_DIFVAR,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_DIS,glcm,regression,,na,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",oracle-identified-config-sensitive,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_ENERGY,glcm,regression,,na,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",oracle-identified-config-sensitive,tracker, -3D,3GLCM_ENTROPY,glcm,regression,,na,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",oracle-identified-config-sensitive,tracker, -3D,3GLCM_HOM1,glcm,regression,,na,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",oracle-identified-config-sensitive,tracker, -3D,3GLCM_HOM2,glcm,regression,,na,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",oracle-identified-config-sensitive,tracker, +3D,3GLCM_DIS,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_DIFAVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ENERGY,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ASM (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ENTROPY,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_HOM1,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ID (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_HOM2,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_IDM (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" 3D,3GLCM_ID,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 3D,3GLCM_IDN,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 3D,3GLCM_IDM,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A @@ -616,37 +616,37 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 3D,3GLCM_JVAR,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 3D,3GLCM_SUMAVERAGE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A 3D,3GLCM_SUMENTROPY,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_SUMVARIANCE,glcm,regression,,na,,,test_3d_glcm_coverage.h;test_3d_glcm.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",oracle-identified-config-sensitive,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_VARIANCE,glcm,regression,,na,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",oracle-identified-config-sensitive,tracker, -3D,3GLCM_ASM_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_ACOR_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_CLUPROM_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_CLUSHADE_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_CLUTEND_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_CONTRAST_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_CORRELATION_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLCM_SUMVARIANCE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_CLUTEND (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_VARIANCE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JVAR (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ASM_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_ACOR_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CLUPROM_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CLUSHADE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CLUTEND_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CONTRAST_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CORRELATION_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" 3D,3GLCM_DIFAVE_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_DIFENTRO_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_DIFVAR_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLCM_DIFENTRO_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_DIFVAR_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" 3D,3GLCM_DIS_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_ENERGY_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_ENTROPY_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_HOM1_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_ID_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLCM_ENERGY_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ASM_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ENTROPY_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JE_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_HOM1_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ID_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ID_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" 3D,3GLCM_IDN_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_IDM_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLCM_IDM_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" 3D,3GLCM_IDMN_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_IV_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_JAVE_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_JE_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_INFOMEAS1_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLCM_IV_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_JAVE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_JE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_INFOMEAS1_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" 3D,3GLCM_INFOMEAS2_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_VARIANCE_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_JMAX_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_JVAR_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_SUMAVERAGE_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_SUMENTROPY_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLCM_SUMVARIANCE_AVE,glcm,regression,,needs_audit,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLCM_VARIANCE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JVAR_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_JMAX_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_JVAR_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_SUMAVERAGE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_SUMENTROPY_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_SUMVARIANCE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_CLUTEND_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" 3D,3GLDM_SDE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A 3D,3GLDM_LDE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A 3D,3GLDM_GLN,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A @@ -679,25 +679,25 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 3D,3GLDZM_ZDM,gldzm,regression,,na,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,none (intermediate mean; indirect via GLV/ZDV/DCV variances),no-mainstream-oracle,tracker, 3D,3GLDZM_ZDV,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, 3D,3GLDZM_ZDE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_LDE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_HDE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_LGLCE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_HGLCE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_LDLGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_LDHGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_HDLGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_HDHGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_GLNU,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_GLNUN,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_DCNU,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_DCNUN,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_DCP,ngldm,vetted,mirp,agreed,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_mirp.h,,,tracker, -3D,3NGLDM_GLM,ngldm,regression,,na,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,none (intermediate mean; indirect via GLV/ZDV/DCV variances),no-mainstream-oracle,tracker, -3D,3NGLDM_GLV,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_DCM,ngldm,regression,,na,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,none (intermediate mean; indirect via GLV/ZDV/DCV variances),no-mainstream-oracle,tracker, -3D,3NGLDM_DCV,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_DCENT,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_DCENE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_ibsi.h,test_3d_ngldm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3NGLDM_LDE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LDE: table=0.1 vs MIRP=0.2559." +3D,3NGLDM_HDE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HDE: table=261 vs MIRP=28.07." +3D,3NGLDM_LGLCE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LGLCE: table=0.00036 vs MIRP=0.0322." +3D,3NGLDM_HGLCE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HGLCE: table=740 vs MIRP=1324." +3D,3NGLDM_LDLGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LDLGLE: table=5.8e-05 vs MIRP=0.000685." +3D,3NGLDM_LDHGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LDHGLE: table=74 vs MIRP=474.8." +3D,3NGLDM_HDLGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HDLGLE: table=0.025 vs MIRP=8.714." +3D,3NGLDM_HDHGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HDHGLE: table=20030 vs MIRP=14942.8." +3D,3NGLDM_GLNU,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_GLNU: table=115443 vs MIRP=4350.3." +3D,3NGLDM_GLNUN,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_GLNUN: table=0.23 vs MIRP=0.01585." +3D,3NGLDM_DCNU,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCNU: table=115443 vs MIRP=40745.0." +3D,3NGLDM_DCNUN,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCNUN: table=0.23 vs MIRP=0.14847." +3D,3NGLDM_DCP,ngldm,vetted,mirp,agreed,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_mirp.h,,,tracker, +3D,3NGLDM_GLM,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,no-external-oracle,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. Additionally 3NGLDM_GLM has NO counterpart in IBSI/MIRP at all (MIRP's NGLDM emits no grey-level-mean or dependence-count-mean column; the 2D table in test_ngldm_ibsi.h likewise marks GLM '--not in IBSI--'), so no external oracle exists for it." +3D,3NGLDM_GLV,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_GLV: table=190 vs MIRP=350.17." +3D,3NGLDM_DCM,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,no-external-oracle,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. Additionally 3NGLDM_DCM has NO counterpart in IBSI/MIRP at all (MIRP's NGLDM emits no grey-level-mean or dependence-count-mean column; the 2D table in test_ngldm_ibsi.h likewise marks GLM '--not in IBSI--'), so no external oracle exists for it." +3D,3NGLDM_DCV,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCV: table=86.17 vs MIRP=11.948." +3D,3NGLDM_DCENT,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCENT: table=5.23 vs MIRP=8.676." +3D,3NGLDM_DCENE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCENE: table=0.14 vs MIRP=0.002875." 3D,3NGTDM_COARSENESS,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A 3D,3NGTDM_CONTRAST,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A 3D,3NGTDM_BUSYNESS,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A @@ -735,22 +735,22 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 3D,3GLRLM_SRHGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A 3D,3GLRLM_LRLGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A 3D,3GLRLM_LRHGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_SRE_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_LRE_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_GLN_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_GLNN_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_RLN_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_RLNN_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLRLM_SRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_LRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_GLN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_GLNN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_RLN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_RLNN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." 3D,3GLRLM_RP_AVE,glrlm,vetted,mirp,agreed,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_mirp.h,,,tracker, -3D,3GLRLM_GLV_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_RV_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLRLM_GLV_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_RV_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." 3D,3GLRLM_RE_AVE,glrlm,vetted,mirp,agreed,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_mirp.h,,,tracker, -3D,3GLRLM_LGLRE_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_HGLRE_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_SRLGLE_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_SRHGLE_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_LRLGLE_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLRLM_LRHGLE_AVE,glrlm,regression,,needs_audit,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLRLM_LGLRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_HGLRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_SRLGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_SRHGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_LRLGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_LRHGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." IMQ,FOCUS_SCORE,imq,regression,,,,,test_imq_regression.h,test_imq_regression.h,reference IQ library (e.g. skimage/BRISQUE) or analytic,not-in-tracker,audit, IMQ,LOCAL_FOCUS_SCORE,imq,regression,,,,,test_imq_regression.h,test_imq_regression.h,reference IQ library (e.g. skimage/BRISQUE) or analytic,not-in-tracker,audit, IMQ,POWER_SPECTRUM_SLOPE,imq,regression,,,,,test_imq_regression.h,test_imq_regression.h,reference IQ library (e.g. skimage/BRISQUE) or analytic,not-in-tracker,audit, diff --git a/tests/vetting/oracles/gen_moments_skimage.py b/tests/vetting/oracles/gen_moments_skimage.py new file mode 100644 index 000000000..b7083505f --- /dev/null +++ b/tests/vetting/oracles/gen_moments_skimage.py @@ -0,0 +1,260 @@ +"""OFFLINE golden generator for the Hu-invariant goldens in test_moments_skimage.h / +test_moments_regression.h (SPEC 6.4), refreshed for the calcHu_imp h5/h6 formula fix. + +Context. Nyxus's calcHu_imp had two formula defects (both also present in the _nt and CUDA +twins, fixed together): + - h5: the second bracket computed (3*(eta30+eta12))^2 == 9*(eta30+eta12)^2 instead of + Hu's 3*(eta30+eta12)^2. + - h6: a precedence error left "+eta03" outside the product -- 4*eta11*(eta30+eta12)*eta21 + eta03 + instead of 4*eta11*(eta30+eta12)*(eta21+eta03) -- so the stray raw eta03 dominated h6. + (Tell-tale in the old pinned goldens: IMOM_HU6 == IMOM_NCM_03, WEIGHTED_HU_M6 == + WT_NORM_CTR_MOM_03, IMOM_WHU6 == IMOM_WNCM_03.) + +This generator: + A. Rebuilds the pinned 48x40 rectangle fixture of test_moments_common.h, recomputes every + moment family with scikit-image in Nyxus's coordinate convention, verifies all goldens + UNAFFECTED by the fix still match (validating that the oracle reproduces Nyxus's + conventions), verifies the OLD h5/h6 goldens equal the buggy formulas (closing the loop + that they encoded the defect), and prints the corrected goldens. + B. Recomputes the Nyxus-specific weighted Hu snapshot values (no external oracle for the + W-weighting itself) by feeding the PINNED weighted normalized central moments through + skimage.measure.moments_hu -- i.e., skimage executes the Hu formula on Nyxus's eta. + C. Emits goldens for a NEW asymmetric right-triangle fixture whose odd-order eta are large + enough that the buggy h5/h6 formulas fail the gtest tolerance -- the regression-proof + test the symmetric rectangle cannot provide -- and proves discriminance by printing + |buggy - correct| against the gtest tolerance. + +Provenance: tool=scikit-image, version=0.26.0; numpy 2.4.6; env=nyxus_mirp (conda); +generator=tests/vetting/oracles/gen_moments_skimage.py. Run offline; CI never invokes it. + +Coordinate convention: Nyxus m_pq has p on x, q on y. Arrays here are indexed A[x, y] so +skimage's moments(A)[i, j] lands on i==p, j==q with no transposition (and no h7 sign flip). +""" + +import numpy as np +from skimage.measure import moments, moments_central, moments_normalized, moments_hu + +W, H = 48, 40 +REL_TOL = 1e-9 # validation gate against pinned goldens (gtest itself uses 1e-6) + + +# ---------------------------------------------------------------- fixtures + +def rect_shape(): + """Binary 48x40 rectangle, A[x, y] convention.""" + return np.ones((W, H), dtype=np.float64) + + +def rect_intensity(): + """test_moments_common.h: I(x,y) = 10 + 3x + 5y + (x*y)%7, A[x, y] convention.""" + a = np.zeros((W, H), dtype=np.float64) + for x in range(W): + for y in range(H): + a[x, y] = 10.0 + 3.0 * x + 5.0 * y + float((x * y) % 7) + return a + + +WEDGE_W, WEDGE_H = 40, 8 + +def wedge_shape(): + """Thin right wedge: pixels (x, y) with 0<=x<40, 0<=y<8, 5*y <= x (A[x, y] convention). + Elongated AND skewed, so the odd-order etas are large (eta30 ~ -0.23) and the buggy + h5/h6 formulas miss the correct values by ~440x / ~1900x the gtest tolerance -- the + regression-proof discriminance a symmetric fixture cannot provide. (A compact + symmetric-ish shape fails: e.g. a 20x20 diagonal half-square discriminates h5 by only + 0.35x the tolerance.)""" + a = np.zeros((WEDGE_W, WEDGE_H), dtype=np.float64) + for x in range(WEDGE_W): + for y in range(WEDGE_H): + if 5 * y <= x: + a[x, y] = 1.0 + return a + + +# ---------------------------------------------------------------- Hu formulas + +def hu_correct(nu): + """Standard Hu invariants (== skimage.measure.moments_hu), from eta indexed nu[p, q].""" + return moments_hu(nu) + + +def hu_buggy_h5_h6(nu): + """The two DEFECTIVE pre-fix Nyxus formulas, for closing the loop on old goldens.""" + _20, _02, _11 = nu[2, 0], nu[0, 2], nu[1, 1] + _30, _03, _12, _21 = nu[3, 0], nu[0, 3], nu[1, 2], nu[2, 1] + h5 = (_30 - 3 * _12) * (_30 + _12) * ((_30 + _12) ** 2 - 3 * (_21 + _03) ** 2) + \ + (3 * _21 - _03) * (_21 + _03) * ((3 * (_30 + _12)) ** 2 - (_21 + _03) ** 2) + h6 = (_20 - _02) * ((_30 + _12) ** 2 - (_21 + _03) ** 2) + \ + (4 * _11 * (_30 + _12) * _21 + _03) + return h5, h6 + + +def nu_matrix_from_pinned(vals): + """Build a nu matrix (nu[p, q]) from pinned Nyxus eta goldens {(p,q): value}.""" + nu = np.zeros((4, 4), dtype=np.float64) + nu[0, 0] = 1.0 + for (p, q), v in vals.items(): + nu[p, q] = v + return nu + + +def full_stack(a): + """raw m, central mu, normalized eta (nu[p, q]), Hu -- all in Nyxus convention.""" + m = moments(a, order=3) + mu = moments_central(a, order=3) + nu = moments_normalized(mu, order=3) + hu = hu_correct(np.nan_to_num(nu)) + return m, mu, nu, hu + + +def check(name, got, pinned, tol=REL_TOL): + scale = max(1.0, abs(pinned), abs(got)) + ok = abs(got - pinned) <= tol * scale + print(f" {'OK ' if ok else 'FAIL'} {name}: oracle={got!r} pinned={pinned!r}") + return ok + + +def main(): + all_ok = True + + # ---------------------------------------------------------------- A. rectangle fixture + print("=== A. 48x40 rectangle fixture (test_moments_common.h) ===") + shp = rect_shape() + inten = rect_intensity() + + m_s, mu_s, nu_s, hu_s = full_stack(shp) + m_i, mu_i, nu_i, hu_i = full_stack(inten) + + print("-- validation: goldens UNAFFECTED by the h5/h6 fix (oracle must reproduce them) --") + # shape: raw + central + eta + Hu 1-4,7 (test_moments_skimage.h) + for name, got, pinned in [ + ("SPAT_MOMENT_00", m_s[0, 0], 1920.0), + ("SPAT_MOMENT_10", m_s[1, 0], 45120.0), + ("SPAT_MOMENT_01", m_s[0, 1], 37440.0), + ("CENTRAL_MOMENT_20", mu_s[2, 0], 368480.0), + ("CENTRAL_MOMENT_02", mu_s[0, 2], 255840.0), + ("NORM_CENTRAL_MOMENT_20", nu_s[2, 0], 0.09995659722222222), + ("NORM_CENTRAL_MOMENT_02", nu_s[0, 2], 0.06940104166666666), + ("HU_M1", hu_s[0], 0.16935763888888888), + ("HU_M2", hu_s[1], 0.0009336419753086421), + ("HU_M3", hu_s[2], 0.0), + ("HU_M4", hu_s[3], 0.0), + ("HU_M6", hu_s[5], 0.0), + ("HU_M7", hu_s[6], 0.0), # pinned -2.36e-10 is summation noise; oracle exact 0 + # intensity: raw + central + eta + Hu 1-4 (test_moments_skimage.h) + ("IMOM_RM_00", m_i[0, 0], 346635.0), + ("IMOM_RM_10", m_i[1, 0], 9253494.0), + ("IMOM_CM_20", mu_i[2, 0], 62976163.595638104), + ("IMOM_CM_03", mu_i[0, 3], -169617579.29906213), + ("IMOM_CM_30", mu_i[3, 0], -232054083.1110376), + ("IMOM_NCM_20", nu_i[2, 0], 0.0005241207783805112), + ("IMOM_NCM_03", nu_i[0, 3], -2.3976723321608506e-06), + ("IMOM_NCM_30", nu_i[3, 0], -3.280259374880525e-06), + ("IMOM_HU1", hu_i[0], 0.0008690300706446306), + ("IMOM_HU2", hu_i[1], 3.7112807351259333e-08), + ("IMOM_HU3", hu_i[2], 2.6788774435431954e-11), + ("IMOM_HU4", hu_i[3], 2.8991603200922661e-12), + ]: + all_ok &= check(name, got, pinned, tol=1e-6) # gtest tolerance; noise-level pins + + print("-- validation: OLD h5/h6 goldens equal the BUGGY formulas (they encoded the bug) --") + b5_i, b6_i = hu_buggy_h5_h6(nu_i) + all_ok &= check("IMOM_HU5(buggy)", b5_i, -2.1393783155778043e-23, tol=1e-6) + all_ok &= check("IMOM_HU6(buggy)", b6_i, -2.3976723312959013e-06, tol=1e-6) + + print("-- corrected goldens for test_moments_skimage.h --") + print(f" HU_M5 = {hu_s[4]!r} (was 4.598098572281346e-10 summation noise; oracle exact 0)") + print(f" IMOM_HU5 = {hu_i[4]!r}") + print(f" IMOM_HU6 = {hu_i[5]!r}") + + # ---------------------------------------------------------------- B. weighted (regression) + print("\n=== B. weighted Hu snapshots from PINNED Nyxus eta (no external W-weighting oracle) ===") + # WT_NORM_CTR_MOM_* pinned in test_moments_regression.h + nu_w = nu_matrix_from_pinned({ + (0, 2): 0.017193451534902194, + (0, 3): 0.00465210928951103, + (1, 1): 0.0005158752090264311, + (1, 2): 0.0008429820076480387, + (2, 0): 0.032749715293974795, + (2, 1): 0.0011795045377311554, + (3, 0): 0.005694719149793572, + }) + hu_w = hu_correct(nu_w) + b5_w, b6_w = hu_buggy_h5_h6(nu_w) + print("-- validation: unchanged weighted Hu 1-4,7 + old buggy 5,6 --") + all_ok &= check("WEIGHTED_HU_M1", hu_w[0], 0.04994316682887699, tol=1e-6) + all_ok &= check("WEIGHTED_HU_M2", hu_w[1], 0.00024306185106698786, tol=1e-6) + all_ok &= check("WEIGHTED_HU_M3", hu_w[2], 1.1262214820995351e-05, tol=1e-6) + all_ok &= check("WEIGHTED_HU_M4", hu_w[3], 7.674925625409561e-05, tol=1e-6) + all_ok &= check("WEIGHTED_HU_M7", hu_w[6], -1.3078000499389243e-09, tol=1e-6) + all_ok &= check("WEIGHTED_HU_M5(buggy)", b5_w, -4.0924793325555725e-09, tol=1e-6) + all_ok &= check("WEIGHTED_HU_M6(buggy)", b6_w, 0.004652261067232659, tol=1e-6) + print("-- corrected snapshots for test_moments_regression.h --") + print(f" WEIGHTED_HU_M5 = {hu_w[4]!r}") + print(f" WEIGHTED_HU_M6 = {hu_w[5]!r}") + + # IMOM_WNCM_* pinned in test_moments_regression.h + nu_iw = nu_matrix_from_pinned({ + (0, 2): 0.00011186764375543395, + (0, 3): 4.6265447913743816e-07, + (1, 1): 9.098620671498624e-06, + (1, 2): -4.1575256057220215e-08, + (2, 0): 0.0001835830638933708, + (2, 1): -1.4928319462526036e-08, + (3, 0): 7.484780551139263e-07, + }) + hu_iw = hu_correct(nu_iw) + b5_iw, b6_iw = hu_buggy_h5_h6(nu_iw) + all_ok &= check("IMOM_WHU1", hu_iw[0], 0.00029545070764880473, tol=1e-6) + all_ok &= check("IMOM_WHU2", hu_iw[1], 5.6161730111679807e-09, tol=1e-6) + all_ok &= check("IMOM_WHU3", hu_iw[2], 1.1923046864432925e-12, tol=1e-6) + all_ok &= check("IMOM_WHU4", hu_iw[3], 3.1287168309169019e-12, tol=1e-6) + all_ok &= check("IMOM_WHU7", hu_iw[6], -5.6202519491182231e-24, tol=1e-6) + all_ok &= check("IMOM_WHU5(buggy)", b5_iw, -5.2494915279842729e-24, tol=1e-6) + all_ok &= check("IMOM_WHU6(buggy)", b6_iw, 4.6265447915851515e-07, tol=1e-6) + print("-- corrected snapshots for test_moments_regression.h --") + print(f" IMOM_WHU5 = {hu_iw[4]!r}") + print(f" IMOM_WHU6 = {hu_iw[5]!r}") + + # ---------------------------------------------------------------- C. asymmetric wedge + print(f"\n=== C. thin right-wedge fixture (5*y <= x, {WEDGE_W}x{WEDGE_H}) for the new oracle test ===") + tri = wedge_shape() + m_t, mu_t, nu_t, hu_t = full_stack(tri) + b5_t, b6_t = hu_buggy_h5_h6(nu_t) + + print("-- goldens for test_moments_skimage.h (wedge_shape_hu_skimage_golden_values) --") + print(f" area (SPAT_MOMENT_00) = {m_t[0, 0]!r}") + print(f" SPAT_MOMENT_10 = {m_t[1, 0]!r}") + print(f" SPAT_MOMENT_01 = {m_t[0, 1]!r}") + print(f" CENTRAL_MOMENT_20 = {mu_t[2, 0]!r}") + print(f" CENTRAL_MOMENT_02 = {mu_t[0, 2]!r}") + print(f" CENTRAL_MOMENT_11 = {mu_t[1, 1]!r}") + print(f" CENTRAL_MOMENT_30 = {mu_t[3, 0]!r}") + print(f" CENTRAL_MOMENT_03 = {mu_t[0, 3]!r}") + print(f" CENTRAL_MOMENT_21 = {mu_t[2, 1]!r}") + print(f" CENTRAL_MOMENT_12 = {mu_t[1, 2]!r}") + print(f" NORM_CENTRAL_MOMENT_20 = {nu_t[2, 0]!r}") + print(f" NORM_CENTRAL_MOMENT_02 = {nu_t[0, 2]!r}") + print(f" NORM_CENTRAL_MOMENT_11 = {nu_t[1, 1]!r}") + print(f" NORM_CENTRAL_MOMENT_30 = {nu_t[3, 0]!r}") + print(f" NORM_CENTRAL_MOMENT_03 = {nu_t[0, 3]!r}") + print(f" NORM_CENTRAL_MOMENT_21 = {nu_t[2, 1]!r}") + print(f" NORM_CENTRAL_MOMENT_12 = {nu_t[1, 2]!r}") + for k in range(7): + print(f" HU_M{k + 1} = {hu_t[k]!r}") + + print("-- discriminance: |buggy - correct| must exceed the gtest tolerance 1e-6*scale --") + for name, buggy, correct in [("HU_M5", b5_t, hu_t[4]), ("HU_M6", b6_t, hu_t[5])]: + scale = max(1.0, abs(buggy), abs(correct)) + tol = 1e-6 * scale + disc = abs(buggy - correct) + verdict = "DISCRIMINATES" if disc > 10 * tol else "TOO WEAK" + print(f" {name}: correct={correct!r} buggy={buggy!r} |diff|={disc:.3e} tol={tol:.3e} -> {verdict}") + all_ok &= disc > 10 * tol + + print(f"\n{'ALL CHECKS PASSED' if all_ok else 'SOME CHECKS FAILED -- do not paste goldens'}") + return 0 if all_ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 3f50759ec6f613d6a33910db1626a16cb69e9a19 Mon Sep 17 00:00:00 2001 From: Demian Vladi Date: Thu, 16 Jul 2026 16:59:57 -0700 Subject: [PATCH 2/4] feat(io): native OME-TIFF/OME-Zarr metadata parsing and axis-aware volumetric reads Step 1 of the IO project: lays the groundwork for native 5D (X,Y,Z,C,T) OME input. Adds the OmeAxes descriptor and OME metadata parsers with unified format detection, axis-aware tile loading (channel/timeframe), native OME-TIFF and OME-Zarr reads honoring the file's own DimensionOrder, ImageLoader::load_volume to assemble a whole X*Y*Z volume for the volumetric pipeline, and wires all 3D volumetric consumers onto it. A NIfTI load_volume equivalence guard protects the existing consumer path through the rewrite. Absorbed commits (oldest to newest): c916752 feat(io): OmeAxes descriptor, OME metadata parsers, unified format detection 364e802 feat(io): axis-aware loadTileFromFile (channel/timeframe) + 5D OME-Zarr addressability tests 33686dd feat(io): OME-Zarr native axis-order read via OmeAxes + rank/order/negative tests a0d88be feat(io): OME-TIFF native read, full DimensionOrder permutations, adversarial hardening eba3d4a feat(io): ImageLoader::load_volume - assemble whole X*Y*Z volume for the volumetric pipeline 958e1e1 test(io): NIfTI load_volume equivalence guard for the volumetric consumer rewrite 3e613cf feat(io): wire all 3D volumetric consumers onto load_volume + end-to-end consumer tests --- CMakeLists.txt | 4 + src/nyx/abs_tile_loader.h | 6 +- src/nyx/grayscale_tiff.h | 54 ++- src/nyx/image_loader.cpp | 89 +++- src/nyx/image_loader.h | 32 ++ src/nyx/image_loader1x.cpp | 14 +- src/nyx/nyxus_dicom_loader.h | 4 +- src/nyx/ome/format_detect.cpp | 88 ++++ src/nyx/ome/format_detect.h | 26 ++ src/nyx/ome/ome_axes.h | 175 ++++++++ src/nyx/ome/ome_tiff_meta.cpp | 166 +++++++ src/nyx/ome/ome_tiff_meta.h | 19 + src/nyx/ome/ome_zarr_meta.cpp | 123 +++++ src/nyx/ome/ome_zarr_meta.h | 34 ++ src/nyx/omezarr.h | 122 +++-- src/nyx/phase1.cpp | 42 +- src/nyx/phase2_3d.cpp | 109 ++--- src/nyx/raw_dicom.h | 2 + src/nyx/raw_format.h | 8 +- src/nyx/raw_image_loader.cpp | 37 +- src/nyx/raw_image_loader.h | 10 + src/nyx/raw_nifti.h | 4 + src/nyx/raw_omezarr.h | 112 +++-- src/nyx/raw_tiff.h | 49 +- tests/data/ometiff/bad_corrupt.tif | Bin 0 -> 39 bytes tests/data/ometiff/bad_rgb.ome.tif | Bin 0 -> 1039 bytes tests/data/ometiff/dim2_yx.ome.tif | Bin 0 -> 957 bytes tests/data/ometiff/dim3_plain.tif | Bin 0 -> 1138 bytes tests/data/ometiff/dim3_zyx.ome.tif | Bin 0 -> 1743 bytes tests/data/ometiff/dim4_czyx.ome.tif | Bin 0 -> 3976 bytes tests/data/ometiff/dim4_tzyx.ome.tif | Bin 0 -> 2791 bytes tests/data/ometiff/dim5.ome.tif | Bin 0 -> 7120 bytes tests/data/ometiff/dim5_ctzyx.ome.tif | Bin 0 -> 7120 bytes tests/data/ometiff/dim5_cztyx.ome.tif | Bin 0 -> 7120 bytes tests/data/ometiff/dim5_tzcyx.ome.tif | Bin 0 -> 7120 bytes tests/data/ometiff/dim5_zctyx.ome.tif | Bin 0 -> 7120 bytes tests/data/ometiff/dim5_ztcyx.ome.tif | Bin 0 -> 7120 bytes tests/data/ometiff/gen_ome_tiff.py | 98 ++++ tests/data/omezarr/README.md | 20 + .../omezarr/bad_axes_count.ome.zarr/.zattrs | 46 ++ .../omezarr/bad_axes_count.ome.zarr/.zgroup | 3 + .../omezarr/bad_axes_count.ome.zarr/0/.zarray | 19 + .../omezarr/bad_axes_count.ome.zarr/0/.zattrs | 1 + tests/data/omezarr/bad_no_xy.ome.zarr/.zattrs | 36 ++ tests/data/omezarr/bad_no_xy.ome.zarr/.zgroup | 3 + .../data/omezarr/bad_no_xy.ome.zarr/0/.zarray | 19 + .../data/omezarr/bad_no_xy.ome.zarr/0/.zattrs | 1 + tests/data/omezarr/dim2_yx.ome.zarr/.zattrs | 31 ++ tests/data/omezarr/dim2_yx.ome.zarr/.zgroup | 3 + tests/data/omezarr/dim2_yx.ome.zarr/0/.zarray | 17 + tests/data/omezarr/dim2_yx.ome.zarr/0/.zattrs | 1 + tests/data/omezarr/dim2_yx.ome.zarr/0/0.0 | Bin 0 -> 96 bytes tests/data/omezarr/dim3_zyx.ome.zarr/.zattrs | 36 ++ tests/data/omezarr/dim3_zyx.ome.zarr/.zgroup | 3 + .../data/omezarr/dim3_zyx.ome.zarr/0/.zarray | 19 + .../data/omezarr/dim3_zyx.ome.zarr/0/.zattrs | 1 + tests/data/omezarr/dim3_zyx.ome.zarr/0/0.0.0 | Bin 0 -> 96 bytes tests/data/omezarr/dim3_zyx.ome.zarr/0/1.0.0 | Bin 0 -> 96 bytes tests/data/omezarr/dim3_zyx.ome.zarr/0/2.0.0 | Bin 0 -> 96 bytes tests/data/omezarr/dim3_zyx.ome.zarr/0/3.0.0 | Bin 0 -> 96 bytes tests/data/omezarr/dim4_czyx.ome.zarr/.zattrs | 41 ++ tests/data/omezarr/dim4_czyx.ome.zarr/.zgroup | 3 + .../data/omezarr/dim4_czyx.ome.zarr/0/.zarray | 21 + .../data/omezarr/dim4_czyx.ome.zarr/0/.zattrs | 1 + .../data/omezarr/dim4_czyx.ome.zarr/0/0.0.0.0 | Bin 0 -> 288 bytes .../data/omezarr/dim4_czyx.ome.zarr/0/0.1.0.0 | Bin 0 -> 288 bytes .../data/omezarr/dim4_czyx.ome.zarr/0/0.2.0.0 | Bin 0 -> 288 bytes .../data/omezarr/dim4_czyx.ome.zarr/0/0.3.0.0 | Bin 0 -> 288 bytes tests/data/omezarr/dim4_tzyx.ome.zarr/.zattrs | 41 ++ tests/data/omezarr/dim4_tzyx.ome.zarr/.zgroup | 3 + .../data/omezarr/dim4_tzyx.ome.zarr/0/.zarray | 21 + .../data/omezarr/dim4_tzyx.ome.zarr/0/.zattrs | 1 + .../data/omezarr/dim4_tzyx.ome.zarr/0/0.0.0.0 | Bin 0 -> 192 bytes .../data/omezarr/dim4_tzyx.ome.zarr/0/0.1.0.0 | Bin 0 -> 192 bytes .../data/omezarr/dim4_tzyx.ome.zarr/0/0.2.0.0 | Bin 0 -> 192 bytes .../data/omezarr/dim4_tzyx.ome.zarr/0/0.3.0.0 | Bin 0 -> 192 bytes tests/data/omezarr/dim5.ome.zarr/.zattrs | 46 ++ tests/data/omezarr/dim5.ome.zarr/.zgroup | 3 + tests/data/omezarr/dim5.ome.zarr/0/.zarray | 23 + tests/data/omezarr/dim5.ome.zarr/0/.zattrs | 1 + tests/data/omezarr/dim5.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 576 bytes tests/data/omezarr/dim5.ome.zarr/0/0.0.1.0.0 | Bin 0 -> 576 bytes tests/data/omezarr/dim5.ome.zarr/0/0.0.2.0.0 | Bin 0 -> 576 bytes tests/data/omezarr/dim5.ome.zarr/0/0.0.3.0.0 | Bin 0 -> 576 bytes .../data/omezarr/dim5_ctzyx.ome.zarr/.zattrs | 46 ++ .../data/omezarr/dim5_ctzyx.ome.zarr/.zgroup | 3 + .../omezarr/dim5_ctzyx.ome.zarr/0/.zarray | 23 + .../omezarr/dim5_ctzyx.ome.zarr/0/.zattrs | 1 + .../omezarr/dim5_ctzyx.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_ctzyx.ome.zarr/0/0.0.1.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_ctzyx.ome.zarr/0/0.0.2.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_ctzyx.ome.zarr/0/0.0.3.0.0 | Bin 0 -> 576 bytes .../data/omezarr/dim5_cztyx.ome.zarr/.zattrs | 46 ++ .../data/omezarr/dim5_cztyx.ome.zarr/.zgroup | 3 + .../omezarr/dim5_cztyx.ome.zarr/0/.zarray | 23 + .../omezarr/dim5_cztyx.ome.zarr/0/.zattrs | 1 + .../omezarr/dim5_cztyx.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_cztyx.ome.zarr/0/0.1.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_cztyx.ome.zarr/0/0.2.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_cztyx.ome.zarr/0/0.3.0.0.0 | Bin 0 -> 576 bytes .../data/omezarr/dim5_noaxes.ome.zarr/.zattrs | 24 + .../data/omezarr/dim5_noaxes.ome.zarr/.zgroup | 3 + .../omezarr/dim5_noaxes.ome.zarr/0/.zarray | 23 + .../omezarr/dim5_noaxes.ome.zarr/0/.zattrs | 1 + .../omezarr/dim5_noaxes.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_noaxes.ome.zarr/0/0.0.1.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_noaxes.ome.zarr/0/0.0.2.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_noaxes.ome.zarr/0/0.0.3.0.0 | Bin 0 -> 576 bytes .../data/omezarr/dim5_tzcyx.ome.zarr/.zattrs | 46 ++ .../data/omezarr/dim5_tzcyx.ome.zarr/.zgroup | 3 + .../omezarr/dim5_tzcyx.ome.zarr/0/.zarray | 23 + .../omezarr/dim5_tzcyx.ome.zarr/0/.zattrs | 1 + .../omezarr/dim5_tzcyx.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_tzcyx.ome.zarr/0/0.1.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_tzcyx.ome.zarr/0/0.2.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_tzcyx.ome.zarr/0/0.3.0.0.0 | Bin 0 -> 576 bytes .../data/omezarr/dim5_zctyx.ome.zarr/.zattrs | 46 ++ .../data/omezarr/dim5_zctyx.ome.zarr/.zgroup | 3 + .../omezarr/dim5_zctyx.ome.zarr/0/.zarray | 23 + .../omezarr/dim5_zctyx.ome.zarr/0/.zattrs | 1 + .../omezarr/dim5_zctyx.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_zctyx.ome.zarr/0/1.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_zctyx.ome.zarr/0/2.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_zctyx.ome.zarr/0/3.0.0.0.0 | Bin 0 -> 576 bytes .../data/omezarr/dim5_ztcyx.ome.zarr/.zattrs | 46 ++ .../data/omezarr/dim5_ztcyx.ome.zarr/.zgroup | 3 + .../omezarr/dim5_ztcyx.ome.zarr/0/.zarray | 23 + .../omezarr/dim5_ztcyx.ome.zarr/0/.zattrs | 1 + .../omezarr/dim5_ztcyx.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_ztcyx.ome.zarr/0/1.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_ztcyx.ome.zarr/0/2.0.0.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_ztcyx.ome.zarr/0/3.0.0.0.0 | Bin 0 -> 576 bytes tests/data/omezarr/gen_dim5.py | 115 +++++ tests/test_3d_nifti_mechanics.h | 40 +- tests/test_all.cc | 145 +++++- tests/test_hu_mechanics.h | 4 +- tests/test_morphology_common.h | 2 +- tests/test_ome_meta.h | 422 ++++++++++++++++++ tests/test_ometiff_mechanics.h | 195 ++++++++ tests/test_omezarr_mechanics.h | 197 +++++++- tests/test_tiff_loader_mechanics.h | 2 +- 141 files changed, 3213 insertions(+), 216 deletions(-) create mode 100644 src/nyx/ome/format_detect.cpp create mode 100644 src/nyx/ome/format_detect.h create mode 100644 src/nyx/ome/ome_axes.h create mode 100644 src/nyx/ome/ome_tiff_meta.cpp create mode 100644 src/nyx/ome/ome_tiff_meta.h create mode 100644 src/nyx/ome/ome_zarr_meta.cpp create mode 100644 src/nyx/ome/ome_zarr_meta.h create mode 100644 tests/data/ometiff/bad_corrupt.tif create mode 100644 tests/data/ometiff/bad_rgb.ome.tif create mode 100644 tests/data/ometiff/dim2_yx.ome.tif create mode 100644 tests/data/ometiff/dim3_plain.tif create mode 100644 tests/data/ometiff/dim3_zyx.ome.tif create mode 100644 tests/data/ometiff/dim4_czyx.ome.tif create mode 100644 tests/data/ometiff/dim4_tzyx.ome.tif create mode 100644 tests/data/ometiff/dim5.ome.tif create mode 100644 tests/data/ometiff/dim5_ctzyx.ome.tif create mode 100644 tests/data/ometiff/dim5_cztyx.ome.tif create mode 100644 tests/data/ometiff/dim5_tzcyx.ome.tif create mode 100644 tests/data/ometiff/dim5_zctyx.ome.tif create mode 100644 tests/data/ometiff/dim5_ztcyx.ome.tif create mode 100644 tests/data/ometiff/gen_ome_tiff.py create mode 100644 tests/data/omezarr/bad_axes_count.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/bad_axes_count.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/bad_axes_count.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/bad_axes_count.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/bad_no_xy.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/bad_no_xy.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/bad_no_xy.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/bad_no_xy.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim2_yx.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim2_yx.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim2_yx.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim2_yx.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim2_yx.ome.zarr/0/0.0 create mode 100644 tests/data/omezarr/dim3_zyx.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim3_zyx.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim3_zyx.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim3_zyx.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim3_zyx.ome.zarr/0/0.0.0 create mode 100644 tests/data/omezarr/dim3_zyx.ome.zarr/0/1.0.0 create mode 100644 tests/data/omezarr/dim3_zyx.ome.zarr/0/2.0.0 create mode 100644 tests/data/omezarr/dim3_zyx.ome.zarr/0/3.0.0 create mode 100644 tests/data/omezarr/dim4_czyx.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim4_czyx.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim4_czyx.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim4_czyx.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim4_czyx.ome.zarr/0/0.0.0.0 create mode 100644 tests/data/omezarr/dim4_czyx.ome.zarr/0/0.1.0.0 create mode 100644 tests/data/omezarr/dim4_czyx.ome.zarr/0/0.2.0.0 create mode 100644 tests/data/omezarr/dim4_czyx.ome.zarr/0/0.3.0.0 create mode 100644 tests/data/omezarr/dim4_tzyx.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim4_tzyx.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim4_tzyx.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim4_tzyx.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim4_tzyx.ome.zarr/0/0.0.0.0 create mode 100644 tests/data/omezarr/dim4_tzyx.ome.zarr/0/0.1.0.0 create mode 100644 tests/data/omezarr/dim4_tzyx.ome.zarr/0/0.2.0.0 create mode 100644 tests/data/omezarr/dim4_tzyx.ome.zarr/0/0.3.0.0 create mode 100644 tests/data/omezarr/dim5.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5.ome.zarr/0/0.0.1.0.0 create mode 100644 tests/data/omezarr/dim5.ome.zarr/0/0.0.2.0.0 create mode 100644 tests/data/omezarr/dim5.ome.zarr/0/0.0.3.0.0 create mode 100644 tests/data/omezarr/dim5_ctzyx.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5_ctzyx.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5_ctzyx.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5_ctzyx.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5_ctzyx.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_ctzyx.ome.zarr/0/0.0.1.0.0 create mode 100644 tests/data/omezarr/dim5_ctzyx.ome.zarr/0/0.0.2.0.0 create mode 100644 tests/data/omezarr/dim5_ctzyx.ome.zarr/0/0.0.3.0.0 create mode 100644 tests/data/omezarr/dim5_cztyx.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5_cztyx.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5_cztyx.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5_cztyx.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5_cztyx.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_cztyx.ome.zarr/0/0.1.0.0.0 create mode 100644 tests/data/omezarr/dim5_cztyx.ome.zarr/0/0.2.0.0.0 create mode 100644 tests/data/omezarr/dim5_cztyx.ome.zarr/0/0.3.0.0.0 create mode 100644 tests/data/omezarr/dim5_noaxes.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5_noaxes.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5_noaxes.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5_noaxes.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5_noaxes.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_noaxes.ome.zarr/0/0.0.1.0.0 create mode 100644 tests/data/omezarr/dim5_noaxes.ome.zarr/0/0.0.2.0.0 create mode 100644 tests/data/omezarr/dim5_noaxes.ome.zarr/0/0.0.3.0.0 create mode 100644 tests/data/omezarr/dim5_tzcyx.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5_tzcyx.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5_tzcyx.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5_tzcyx.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5_tzcyx.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_tzcyx.ome.zarr/0/0.1.0.0.0 create mode 100644 tests/data/omezarr/dim5_tzcyx.ome.zarr/0/0.2.0.0.0 create mode 100644 tests/data/omezarr/dim5_tzcyx.ome.zarr/0/0.3.0.0.0 create mode 100644 tests/data/omezarr/dim5_zctyx.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5_zctyx.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5_zctyx.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5_zctyx.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5_zctyx.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_zctyx.ome.zarr/0/1.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_zctyx.ome.zarr/0/2.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_zctyx.ome.zarr/0/3.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_ztcyx.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5_ztcyx.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5_ztcyx.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5_ztcyx.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5_ztcyx.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_ztcyx.ome.zarr/0/1.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_ztcyx.ome.zarr/0/2.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_ztcyx.ome.zarr/0/3.0.0.0.0 create mode 100644 tests/data/omezarr/gen_dim5.py create mode 100644 tests/test_ome_meta.h create mode 100644 tests/test_ometiff_mechanics.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 71318bc1d..ae0a70163 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,10 @@ set(SOURCE src/nyx/raw_tiff.h # sources src/nyx/3rdparty/dsyevj3.cpp + # native OME metadata parsing + unified format detection + src/nyx/ome/format_detect.cpp + src/nyx/ome/ome_tiff_meta.cpp + src/nyx/ome/ome_zarr_meta.cpp src/nyx/features/basic_morphology.cpp src/nyx/features/caliper_feret.cpp src/nyx/features/caliper_martin.cpp diff --git a/src/nyx/abs_tile_loader.h b/src/nyx/abs_tile_loader.h index c4f509c60..60d745819 100644 --- a/src/nyx/abs_tile_loader.h +++ b/src/nyx/abs_tile_loader.h @@ -32,12 +32,16 @@ class AbstractTileLoader /// @param tile Tile to load /// @param indexRowGlobalTile Tile's row index in the file to load /// @param indexColGlobalTile Tile's col index in the file to load - /// @param indexLayerGlobalTile Tile's layer index in the file to load + /// @param indexLayerGlobalTile Tile's layer (Z) index in the file to load + /// @param indexChannel Channel (C) index to load; 0 for single-channel data + /// @param indexTimeframe Time (T) index to load; 0 for non-time-series data /// @param level Tile's pyramidal level in the file to load virtual void loadTileFromFile(std::shared_ptr> tile, size_t indexRowGlobalTile, size_t indexColGlobalTile, size_t indexLayerGlobalTile, + size_t indexChannel, + size_t indexTimeframe, size_t level) = 0; /// \brief Getter to full Height diff --git a/src/nyx/grayscale_tiff.h b/src/nyx/grayscale_tiff.h index 931c11597..d6b253714 100644 --- a/src/nyx/grayscale_tiff.h +++ b/src/nyx/grayscale_tiff.h @@ -14,7 +14,8 @@ #include #include // std::is_signed_v to guard signed->unsigned wraparound in loadTile #include -#include // for INT_MAX +#include // for INT_MAX +#include "ome/ome_tiff_meta.h" // parse_ome_xml -> OmeAxes (OME-TIFF plane->IFD mapping) constexpr size_t STRIP_TILE_HEIGHT = 1024; constexpr size_t STRIP_TILE_WIDTH = 1024; @@ -105,6 +106,8 @@ class NyxusGrayscaleTiffTileLoader : public AbstractTileLoader size_t indexRowGlobalTile, size_t indexColGlobalTile, size_t indexLayerGlobalTile, + [[maybe_unused]] size_t indexChannel, // plain 2D TIFF: single channel + [[maybe_unused]] size_t indexTimeframe, // plain 2D TIFF: no time series size_t level) override { std::string err; @@ -430,6 +433,23 @@ class NyxusGrayscaleTiffStripLoader : public AbstractTileLoader fullDepth_ = TIFFNumberOfDirectories(tiff_); + // OME-TIFF: the IFDs are a (z,c,t) rasterization, not a plain Z-stack. + // Parse the OME-XML (IFD-0 ImageDescription) so (z,c,t) map to the right + // IFD and fullDepth reflects SizeZ, not the total page count. + { + char* desc = nullptr; + if (TIFFGetField(tiff_, TIFFTAG_IMAGEDESCRIPTION, &desc) == 1 && desc + && std::string(desc).find(" void loadTileFromFile(std::shared_ptr> tile, size_t indexRowGlobalTile, size_t indexColGlobalTile, - size_t indexLayerGlobalTile, - [[maybe_unused]] size_t level) override + size_t indexLayerGlobalTile, // Z (plane page for non-OME multi-page TIFF) + size_t indexChannel, // C plane (OME-TIFF only) + size_t indexTimeframe, // T plane (OME-TIFF only) + [[maybe_unused]] size_t level) override { // Get ahold of the logical (feature extraction facing) tile buffer from its smart pointer std::vector& tileDataVec = *tile; @@ -484,6 +506,16 @@ class NyxusGrayscaleTiffStripLoader : public AbstractTileLoader buf = _TIFFmalloc(TIFFScanlineSize(tiff_)); + // OME plane indices are computed into an IFD (not iterated), so validate the + // range explicitly — an out-of-range Z would otherwise skip the layer loop + // below and silently return a zero tile. + if (is_ome_ && (indexLayerGlobalTile >= ome_.sizeZ + || indexChannel >= ome_.sizeC || indexTimeframe >= ome_.sizeT)) + { + _TIFFfree(buf); + throw std::runtime_error("NyxusGrayscaleTiffStripLoader: (z,c,t) plane out of range"); + } + size_t startLayer = indexLayerGlobalTile * tileDepth_, endLayer = std::min((indexLayerGlobalTile + 1) * tileDepth_, fullDepth_), @@ -492,9 +524,18 @@ class NyxusGrayscaleTiffStripLoader : public AbstractTileLoader startCol = indexColGlobalTile * tileWidth_, endCol = std::min((indexColGlobalTile + 1) * tileWidth_, fullWidth_); - for (layer = startLayer; layer < endLayer; ++layer) + for (layer = startLayer; layer < endLayer; ++layer) { - TIFFSetDirectory(tiff_, layer); + // OME-TIFF: page = (z,c,t) IFD per DimensionOrder; plain multi-page: page = Z. + size_t ifd = is_ome_ ? ome_.ifdForPlane(layer, indexChannel, indexTimeframe) : layer; + // Check the return so an out-of-range plane throws instead of silently + // leaving the previous directory current and reading the wrong page. + if (TIFFSetDirectory(tiff_, (uint16_t)ifd) != 1) + { + _TIFFfree(buf); + throw std::runtime_error("NyxusGrayscaleTiffStripLoader: TIFFSetDirectory(ifd=" + + std::to_string(ifd) + ") failed (out-of-range plane?)"); + } for (row = startRow; row < endRow; row++) { TIFFReadScanline(tiff_, buf, row); @@ -692,4 +733,7 @@ class NyxusGrayscaleTiffStripLoader : public AbstractTileLoader sampleFormat_ = 0, ///< Sample format as defined by libtiff bitsPerSample_ = 0; ///< Bit Per Sample as defined by libtiff + bool is_ome_ = false; ///< true when IFD-0 carries an OME-XML block + Nyxus::OmeAxes ome_; ///< parsed OME dimensions (drives the plane->IFD map) + }; diff --git a/src/nyx/image_loader.cpp b/src/nyx/image_loader.cpp index 6e99b9e83..e9d8236f0 100644 --- a/src/nyx/image_loader.cpp +++ b/src/nyx/image_loader.cpp @@ -8,6 +8,7 @@ #include "dirs_and_files.h" #include "helpers/fsystem.h" #include "raw_nifti.h" +#include "ome/format_detect.h" // FIX: unified content-sniffing loader dispatch ImageLoader::ImageLoader() {} @@ -22,9 +23,11 @@ bool ImageLoader::open (SlideProps & p, const FpImageOptions & fpopts) try { - std::string ext = Nyxus::get_big_extension (int_fpath); + // FIX: classify by detect_input_format() (extension + OME content sniff) instead of + // raw extension compares, so dispatch is identical across all 3 loaders and OME is recognized. + Nyxus::InputFormat fmt = Nyxus::detect_input_format (int_fpath); - if (ext == ".zarr" || ext == ".ome.zarr") + if (fmt.kind == Nyxus::ContainerKind::OmeZarr) // FIX: was `ext==".zarr"||".ome.zarr"` { #ifdef OMEZARR_SUPPORT intFL = new NyxusOmeZarrLoader(n_threads, int_fpath); @@ -36,8 +39,8 @@ bool ImageLoader::open (SlideProps & p, const FpImageOptions & fpopts) std::cerr << erm << "\n"; #endif } - else - if (ext == ".dcm" || ext == ".dicom") + else + if (fmt.kind == Nyxus::ContainerKind::Dicom) // FIX: was `ext==".dcm"||".dicom"` { #ifdef DICOM_SUPPORT // HU offset base must be the scanned (HU-domain) slide min. In preserve_hu @@ -57,7 +60,7 @@ bool ImageLoader::open (SlideProps & p, const FpImageOptions & fpopts) #endif } else - if (ext == ".nii" || ext == ".nii.gz") + if (fmt.kind == Nyxus::ContainerKind::Nifti) // FIX: was `ext==".nii"||".nii.gz"` { intFL = new NiftiLoader (int_fpath, (fpopts.preserve_hu() || fpopts.empty()) ? p.min_preroi_inten : (double)fpopts.min_intensity(), // HU offset base = scanned HU-domain slide min; ignore fp min in preserve_hu mode (else negative HU clamps to 0) @@ -133,9 +136,11 @@ bool ImageLoader::open (SlideProps & p, const FpImageOptions & fpopts) try { - std::string ext = Nyxus::get_big_extension(seg_fpath); + // FIX: unify seg dispatch with detect_input_format(). Defect fixed: the seg path only + // matched ".zarr", so a ".ome.zarr" mask mis-routed to the TIFF path (intensity path matched both). + Nyxus::InputFormat fmt = Nyxus::detect_input_format (seg_fpath); - if (ext == ".zarr") + if (fmt.kind == Nyxus::ContainerKind::OmeZarr) // FIX: was `ext==".zarr"` only (dropped .ome.zarr) { #ifdef OMEZARR_SUPPORT segFL = new NyxusOmeZarrLoader(n_threads, seg_fpath); @@ -143,8 +148,8 @@ bool ImageLoader::open (SlideProps & p, const FpImageOptions & fpopts) std::cout << "This version of Nyxus was not build with OmeZarr support." <(n_threads, seg_fpath); @@ -153,7 +158,7 @@ bool ImageLoader::open (SlideProps & p, const FpImageOptions & fpopts) #endif } else - if (ext == ".nii" || ext == ".nii.gz") + if (fmt.kind == Nyxus::ContainerKind::Nifti) // FIX: was `ext==".nii"||".nii.gz"` { segFL = new NiftiLoader (seg_fpath); } @@ -236,12 +241,12 @@ bool ImageLoader::load_tile(size_t tile_idx) auto tRow = tile_idx / ntw; auto tCol = tile_idx % ntw; - intFL->loadTileFromFile (ptrI, tRow, tCol, lyr, lvl); + intFL->loadTileFromFile (ptrI, tRow, tCol, lyr, cur_channel, cur_timeframe, lvl); // segmentation loader is not available in wholeslide if (segFL) - segFL->loadTileFromFile (ptrL, tRow, tCol, lyr, lvl); - + segFL->loadTileFromFile (ptrL, tRow, tCol, lyr, cur_channel, cur_timeframe, lvl); + return true; } @@ -250,14 +255,68 @@ bool ImageLoader::load_tile (size_t tile_row, size_t tile_col) if (tile_row >= nth || tile_col >= ntw) return false; - intFL->loadTileFromFile (ptrI, tile_row, tile_col, lyr, lvl); + intFL->loadTileFromFile (ptrI, tile_row, tile_col, lyr, cur_channel, cur_timeframe, lvl); // segmentation loader is not available in wholeslide if (segFL) - segFL->loadTileFromFile (ptrL, tile_row, tile_col, lyr, lvl); + segFL->loadTileFromFile (ptrL, tile_row, tile_col, lyr, cur_channel, cur_timeframe, lvl); return true; } + +void ImageLoader::assemble_volume (AbstractTileLoader* fl, + std::shared_ptr>& ptr, + std::vector& dst, size_t channel, size_t timeframe) +{ + const size_t sliceSize = (size_t)fw * fh; + + // Use THIS loader's own layout: per-plane loaders (OME-Zarr, multi-page TIFF) + // deliver one Z-plane per read (tileDepth==1, tileTimestamps==1) with the + // timeframe chosen by the loadTileFromFile arg; a whole-4D loader (NIfTI) + // delivers the entire x*y*z*t blob ([t][z][y][x]) in one read and ignores the + // timeframe arg, so the requested frame is slabbed out via frameBase. + const size_t ltd = fl->tileDepth (lvl); + const size_t ltt = fl->tileTimestamps (lvl); + const size_t lntd = fl->numberTileDepth (lvl); + const size_t frameStride = ltd * th * tw; + const size_t frameBase = (ltt > 1) ? timeframe * frameStride : 0; + + for (size_t lz = 0; lz < lntd; ++lz) + { + fl->loadTileFromFile (ptr, 0, 0, lz, channel, timeframe, lvl); + for (size_t pz = 0; pz < ltd && (lz * ltd + pz) < fd; ++pz) + { + const size_t gz = lz * ltd + pz; + for (size_t row = 0; row < fh; ++row) + { + const size_t src = frameBase + (pz * th + row) * tw; + const size_t d = gz * sliceSize + row * fw; + std::copy (ptr->begin() + src, ptr->begin() + src + fw, dst.begin() + d); + } + } + } +} + +bool ImageLoader::load_volume (size_t channel, size_t timeframe, size_t mask_timeframe) +{ + cur_channel = channel; + cur_timeframe = timeframe; + + const size_t volSize = (size_t)fw * fh * fd; // the whole X*Y*Z volume + + if (vol_int_.size() != volSize) + vol_int_.resize (volSize); + assemble_volume (intFL, ptrI, vol_int_, channel, timeframe); + + if (segFL != nullptr) + { + if (vol_seg_.size() != volSize) + vol_seg_.resize (volSize); + assemble_volume (segFL, ptrL, vol_seg_, channel, mask_timeframe); // mask may be on a different frame + } + return true; +} + const std::vector& ImageLoader::get_int_tile_buffer() { return *ptrI; diff --git a/src/nyx/image_loader.h b/src/nyx/image_loader.h index 6802fb3a2..01acb8a1e 100644 --- a/src/nyx/image_loader.h +++ b/src/nyx/image_loader.h @@ -19,6 +19,19 @@ class ImageLoader void close(); bool load_tile (size_t tile_idx); bool load_tile (size_t tile_row, size_t tile_col); + + // Assemble the whole X*Y*Z volume for one (channel, timeframe) into an + // internal buffer by looping over Z-planes. This is what lets plane-by-plane + // loaders (OME-Zarr, multi-page/OME-TIFF) feed the volumetric pipeline, which + // otherwise assumes the whole volume arrives in one read (the NIfTI model). + // The mask may live on a different timeframe than the intensity (the 1-mask : + // N-intensity case), so `mask_timeframe` is separable; the 2-arg overload uses + // the same frame for both. + bool load_volume (size_t channel, size_t timeframe, size_t mask_timeframe); + bool load_volume (size_t channel, size_t timeframe) { return load_volume(channel, timeframe, timeframe); } + const std::vector& get_int_volume_buffer() const { return vol_int_; } + const std::vector& get_seg_volume_buffer() const { return vol_seg_; } + const std::vector& get_int_tile_buffer(); const std::vector& get_seg_tile_buffer(); const std::shared_ptr>& get_seg_tile_sptr(); @@ -36,6 +49,13 @@ class ImageLoader size_t get_inten_time(); size_t get_mask_time(); + // Select which channel (C) / timeframe (T) plane subsequent load_tile() calls + // read. Default 0/0 preserves the single-channel, single-timepoint behavior. + void set_channel (size_t c) { cur_channel = c; } + void set_timeframe (size_t t) { cur_timeframe = t; } + size_t get_channel() const { return cur_channel; } + size_t get_timeframe() const { return cur_timeframe; } + private: AbstractTileLoader *segFL = nullptr, *intFL = nullptr; @@ -64,5 +84,17 @@ class ImageLoader int lvl = 0, // Pyramid level lyr = 0; // Layer + + size_t cur_channel = 0, // Currently selected channel (C) plane + cur_timeframe = 0; // Currently selected timeframe (T) plane + + // Whole-volume (X*Y*Z) assembly buffers filled by load_volume() + std::vector vol_int_, vol_seg_; + + // Assemble one loader's X*Y*Z volume (for the given channel/timeframe) into dst, + // honoring that loader's own tileDepth/tileTimestamps (per-plane vs whole-4D). + void assemble_volume (AbstractTileLoader* fl, + std::shared_ptr>& ptr, + std::vector& dst, size_t channel, size_t timeframe); }; diff --git a/src/nyx/image_loader1x.cpp b/src/nyx/image_loader1x.cpp index 147bf7f44..06777ba6a 100644 --- a/src/nyx/image_loader1x.cpp +++ b/src/nyx/image_loader1x.cpp @@ -9,6 +9,7 @@ #include "nyxus_dicom_loader.h" #include "dirs_and_files.h" #include "helpers/fsystem.h" +#include "ome/format_detect.h" // FIX: unified content-sniffing loader dispatch ImageLoader1x::ImageLoader1x() {} @@ -18,7 +19,12 @@ bool ImageLoader1x::open (const std::string& fpath, const FpImageOptions & fpopt try { - if (fs::path(fpath).extension() == ".zarr") + // FIX: dispatch via detect_input_format() for parity with the other loaders (and to fix + // the bitwise-OR on the DICOM test). This loader carries no NIfTI backend, so Nifti still falls to the + // TIFF branch as before — behavior unchanged there. + Nyxus::InputFormat fmt = Nyxus::detect_input_format (fpath); + + if (fmt.kind == Nyxus::ContainerKind::OmeZarr) // FIX: was `fs::path(fpath).extension()==".zarr"` { #ifdef OMEZARR_SUPPORT FL = std::make_unique>(n_threads, fpath); @@ -26,7 +32,7 @@ bool ImageLoader1x::open (const std::string& fpath, const FpImageOptions & fpopt std::cout << "This version of Nyxus was not build with OmeZarr support." <>(n_threads, fpath); #else @@ -95,7 +101,7 @@ bool ImageLoader1x::load_tile(size_t tile_idx) auto row = tile_idx / ntw; auto col = tile_idx % ntw; - FL->loadTileFromFile(ptr, row, col, lyr, lvl); + FL->loadTileFromFile(ptr, row, col, lyr, 0/*channel*/, 0/*timeframe*/, lvl); return true; } @@ -105,7 +111,7 @@ bool ImageLoader1x::load_tile(size_t tile_row, size_t tile_col) if (tile_row >= nth || tile_col >= ntw) return false; - FL->loadTileFromFile(ptr, tile_row, tile_col, lyr, lvl); + FL->loadTileFromFile(ptr, tile_row, tile_col, lyr, 0/*channel*/, 0/*timeframe*/, lvl); return true; } diff --git a/src/nyx/nyxus_dicom_loader.h b/src/nyx/nyxus_dicom_loader.h index d4cb9d43f..8c29953fa 100644 --- a/src/nyx/nyxus_dicom_loader.h +++ b/src/nyx/nyxus_dicom_loader.h @@ -135,7 +135,9 @@ class NyxusGrayscaleDicomLoader : public AbstractTileLoader size_t indexRowGlobalTile, size_t indexColGlobalTile, size_t indexLayerGlobalTile, - [[maybe_unused]] size_t level) override + [[maybe_unused]] size_t indexChannel, // DICOM grayscale: single channel + [[maybe_unused]] size_t indexTimeframe, // DICOM: frames are WSI tiles, not time + [[maybe_unused]] size_t level) override { // Get ahold of the logical (feature extraction facing) tile buffer from its smart pointer std::vector& tileDataVec = *tile; diff --git a/src/nyx/ome/format_detect.cpp b/src/nyx/ome/format_detect.cpp new file mode 100644 index 000000000..394abb17f --- /dev/null +++ b/src/nyx/ome/format_detect.cpp @@ -0,0 +1,88 @@ +#define NOMINMAX + +#include "format_detect.h" + +#include +#include +#include + +#include + +#include "../helpers/fsystem.h" + +namespace Nyxus +{ + const char* to_string(ContainerKind k) + { + switch (k) + { + case ContainerKind::TiffPlain: return "TiffPlain"; + case ContainerKind::OmeTiff: return "OmeTiff"; + case ContainerKind::OmeZarr: return "OmeZarr"; + case ContainerKind::Dicom: return "Dicom"; + case ContainerKind::Nifti: return "Nifti"; + default: return "Unknown"; + } + } + + // Does this TIFF carry an OME-XML block in IFD-0's ImageDescription? + static bool tiff_is_ome(const std::string& path) + { + // silence libtiff's stderr chatter on non-OME / odd TIFFs during the sniff + TIFFErrorHandler oldE = TIFFSetErrorHandler(nullptr); + TIFFErrorHandler oldW = TIFFSetWarningHandler(nullptr); + bool ome = false; + TIFF* t = TIFFOpen(path.c_str(), "r"); + if (t) + { + char* desc = nullptr; // libtiff-owned, do not free + if (TIFFGetField(t, TIFFTAG_IMAGEDESCRIPTION, &desc) == 1 && desc) + { + std::string d(desc); + ome = d.find("(f)), std::istreambuf_iterator()); + if (content.find("multiscales") != std::string::npos) + return true; + } + return false; + } + + InputFormat detect_input_format(const std::string& path) + { + std::string ext = Nyxus::get_big_extension(path); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return (char)std::tolower(c); }); + + if (ext == ".zarr" || ext == ".ome.zarr") + return { ContainerKind::OmeZarr, zarr_has_multiscales(path) }; + if (ext == ".dcm" || ext == ".dicom") + return { ContainerKind::Dicom, false }; + if (ext == ".nii" || ext == ".nii.gz") + return { ContainerKind::Nifti, false }; + + // Everything else is a TIFF flavor (.tif/.tiff/.ome.tif/.ome.tiff/.tf2/.tf8/.btf). + if (tiff_is_ome(path)) + return { ContainerKind::OmeTiff, true }; + return { ContainerKind::TiffPlain, false }; + } +} diff --git a/src/nyx/ome/format_detect.h b/src/nyx/ome/format_detect.h new file mode 100644 index 000000000..f45eb42e8 --- /dev/null +++ b/src/nyx/ome/format_detect.h @@ -0,0 +1,26 @@ +#pragma once + +// Single source of truth for "what kind of file is this?" used by all three +// loader dispatch sites (image_loader, raw_image_loader, image_loader1x). Adds +// the OME content-sniff that is absent today and unifies the extension handling +// (image_loader1x currently uses single-extension matching and cannot see +// .ome.zarr / .nii.gz). STL + libtiff + only. + +#include + +namespace Nyxus +{ + enum class ContainerKind { TiffPlain, OmeTiff, OmeZarr, Dicom, Nifti, Unknown }; + + struct InputFormat + { + ContainerKind kind = ContainerKind::Unknown; + bool is_ome = false; // OME metadata actually present (OME-XML / NGFF multiscales) + }; + + // Classify by big-extension, then content-sniff: OME-XML in TIFF IFD-0 + // ImageDescription, or NGFF `multiscales` in a Zarr group. Never throws. + InputFormat detect_input_format(const std::string& path); + + const char* to_string(ContainerKind k); +} diff --git a/src/nyx/ome/ome_axes.h b/src/nyx/ome/ome_axes.h new file mode 100644 index 000000000..e064ddea8 --- /dev/null +++ b/src/nyx/ome/ome_axes.h @@ -0,0 +1,175 @@ +#pragma once + +// ----------------------------------------------------------------------------- +// OmeAxes — one format-agnostic dimension descriptor for native 3D/4D/5D OME +// input. Both container families populate this from real metadata: +// - OME-TIFF : parse_ome_xml() (ome_tiff_meta.h) — OME-XML block +// - OME-Zarr : parse_ome_zarr() (ome_zarr_meta.h) — multiscales/axes/datasets +// Everything downstream (loaders, SlideProps, output schema) consumes OmeAxes +// instead of assuming C=T=1 and a hard-coded TCZYX axis order. +// +// This header is STL-only so it compiles in every build configuration. +// ----------------------------------------------------------------------------- + +#include +#include +#include +#include + +namespace Nyxus +{ + // Role of an axis, mirroring the OME-NGFF 'type' field (space|channel|time). + enum class AxisKind { Space, Channel, Time }; + + // Resolved pixel/element type (consolidates the ad-hoc dtype codes that + // omezarr.h / raw_omezarr.h and the TIFF sampleFormat switch use today). + enum class PixelType { UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64, Float32, Float64, Unknown }; + + inline unsigned bits_of(PixelType t) + { + switch (t) + { + case PixelType::UInt8: case PixelType::Int8: return 8; + case PixelType::UInt16: case PixelType::Int16: return 16; + case PixelType::UInt32: case PixelType::Int32: case PixelType::Float32: return 32; + case PixelType::UInt64: case PixelType::Int64: case PixelType::Float64: return 64; + default: return 0; + } + } + inline bool is_float(PixelType t) { return t == PixelType::Float32 || t == PixelType::Float64; } + inline bool is_signed(PixelType t) + { + return t == PixelType::Int8 || t == PixelType::Int16 || t == PixelType::Int32 || t == PixelType::Int64 || is_float(t); + } + + // OME 'Type' attribute (OME-XML) -> PixelType. + inline PixelType pixel_type_from_ome_type(const std::string& s) + { + if (s == "uint8") return PixelType::UInt8; + if (s == "uint16") return PixelType::UInt16; + if (s == "uint32") return PixelType::UInt32; + if (s == "uint64") return PixelType::UInt64; + if (s == "int8") return PixelType::Int8; + if (s == "int16") return PixelType::Int16; + if (s == "int32") return PixelType::Int32; + if (s == "int64") return PixelType::Int64; + if (s == "float") return PixelType::Float32; + if (s == "double") return PixelType::Float64; + return PixelType::Unknown; + } + + // Zarr dtype -> PixelType. Accepts v2 numpy strings ("i4", "|u1", ...) + // and v3 names ("uint16", "int32", "float32", ...). + inline PixelType pixel_type_from_zarr_dtype(const std::string& in) + { + std::string s = in; + // strip a leading byte-order char for the v2 form + if (!s.empty() && (s[0] == '<' || s[0] == '>' || s[0] == '|' || s[0] == '=')) + s = s.substr(1); + if (s == "u1" || s == "uint8") return PixelType::UInt8; + if (s == "u2" || s == "uint16") return PixelType::UInt16; + if (s == "u4" || s == "uint32") return PixelType::UInt32; + if (s == "u8" || s == "uint64") return PixelType::UInt64; + if (s == "i1" || s == "int8") return PixelType::Int8; + if (s == "i2" || s == "int16") return PixelType::Int16; + if (s == "i4" || s == "int32") return PixelType::Int32; + if (s == "i8" || s == "int64") return PixelType::Int64; + if (s == "f2") return PixelType::Float32; // half -> promote to float32 + if (s == "f4" || s == "float32" || s == "float") return PixelType::Float32; + if (s == "f8" || s == "float64" || s == "double") return PixelType::Float64; + return PixelType::Unknown; + } + + // One axis in on-disk (storage) order. + struct OmeAxis + { + AxisKind kind = AxisKind::Space; + char label = 'X'; // one of 'X','Y','Z','C','T' + std::size_t size = 1; // extent along this axis + double physical = 1.0; // voxel spacing / frame interval; 1.0 if unknown + std::string unit; // e.g. "micrometer","second"; empty if unknown + }; + + // One resolution level of a pyramid (level 0 == full-res). + struct OmePyramidLevel + { + std::size_t index = 0; + std::size_t sizeX = 0, sizeY = 0, sizeZ = 1; // 0 == extent not yet resolved for this sub-level + double scaleX = 1, scaleY = 1, scaleZ = 1; // coordinateTransformations 'scale' + std::string path; // Zarr dataset path ("0","1",...) / TIFF SubIFD ordinal + }; + + struct OmeAxes + { + // Sizes resolved by axis role (independent of on-disk order). + std::size_t sizeX = 1, sizeY = 1, sizeZ = 1, sizeC = 1, sizeT = 1; + + // On-disk axis order (numpy/slowest-first), e.g. "TCZYX" or "CTZYX" or "ZYX". + std::string storageOrder; + // OME DimensionOrder (fastest-first, always begins "XY"), e.g. "XYZCT". + std::string omeDimensionOrder; + std::vector storageAxes; // in storageOrder order + + // Physical calibration (convenience mirror of the space/time axes). + double physX = 1, physY = 1, physZ = 1, timeIncrement = 1; + std::string unitXY, unitZ, unitT; + + PixelType dtype = PixelType::UInt16; + unsigned bitsPerSample = 16; + + std::vector levels; // >=1; levels[0] full-res + std::vector channelNames; // optional (OME 'Channel'/omero) + + bool valid = false; // set true by a successful parse + + // Position of a label in storageAxes; -1 if absent. + int storageIndexOf(char lbl) const + { + for (std::size_t i = 0; i < storageAxes.size(); ++i) + if (storageAxes[i].label == lbl) return static_cast(i); + return -1; + } + + // Map a (z,c,t) plane to its IFD / plane ordinal under the OME + // DimensionOrder (default rasterization: the axes after XY, fastest to + // slowest). E.g. XYZCT -> ifd = z + c*sizeZ + t*sizeZ*sizeC. + std::size_t ifdForPlane(std::size_t z, std::size_t c, std::size_t t) const + { + std::size_t idx = 0, stride = 1; + for (char ax : omeDimensionOrder) + { + if (ax == 'X' || ax == 'Y') continue; + std::size_t coord = (ax == 'Z') ? z : (ax == 'C') ? c : t; + std::size_t sz = (ax == 'Z') ? sizeZ : (ax == 'C') ? sizeC : sizeT; + idx += coord * stride; + stride *= sz; + } + return idx; + } + + std::size_t numberPyramidLevels() const { return levels.empty() ? 1 : levels.size(); } + bool isVolumetric() const { return sizeZ > 1; } + bool isMultiChannel() const { return sizeC > 1; } + bool isTimeSeries() const { return sizeT > 1; } + }; + + // ---- small shared helpers ---------------------------------------------- + + inline AxisKind axis_kind_of(char label) + { + return (label == 'C') ? AxisKind::Channel : (label == 'T') ? AxisKind::Time : AxisKind::Space; + } + + // OME DimensionOrder (fastest->slowest) from an on-disk storage order + // (slowest-first). For a C-order array the last axis varies fastest, so the + // OME order is the reverse of storageOrder, padded with any absent axes kept + // in OME's canonical relative order so the result is always a legal value. + inline std::string ome_dimension_order_from_storage(const std::string& storageOrder) + { + std::string rev(storageOrder.rbegin(), storageOrder.rend()); + for (char ax : std::string("XYZCT")) + if (rev.find(ax) == std::string::npos) + rev.push_back(ax); + return rev; + } +} diff --git a/src/nyx/ome/ome_tiff_meta.cpp b/src/nyx/ome/ome_tiff_meta.cpp new file mode 100644 index 000000000..c648fde74 --- /dev/null +++ b/src/nyx/ome/ome_tiff_meta.cpp @@ -0,0 +1,166 @@ +#include "ome_tiff_meta.h" + +#include +#include +#include +#include + +namespace Nyxus +{ + // Extract the start-tag text "" of the first element called `name`. + // Returns "" if not found. Matches a name boundary so "Pixels" does not match + // a hypothetical "PixelsFoo". + static std::string element_start_tag(const std::string& xml, const std::string& name) + { + const std::string open = "<" + name; + std::size_t p = 0; + while ((p = xml.find(open, p)) != std::string::npos) + { + char after = (p + open.size() < xml.size()) ? xml[p + open.size()] : '\0'; + if (after == ' ' || after == '\t' || after == '\n' || after == '\r' || after == '>' || after == '/') + { + std::size_t gt = xml.find('>', p); + if (gt == std::string::npos) return ""; + return xml.substr(p, gt - p + 1); + } + p += open.size(); + } + return ""; + } + + // Read attribute `name` from a single start-tag string. Boundary-checked so + // "SizeX" does not match inside "PhysicalSizeX". Handles ' and " quoting. + static bool get_attr(const std::string& tag, const std::string& name, std::string& out) + { + std::size_t pos = 0; + while ((pos = tag.find(name, pos)) != std::string::npos) + { + bool ok_before = (pos == 0) || std::isspace((unsigned char)tag[pos - 1]); + std::size_t q = pos + name.size(); + while (q < tag.size() && std::isspace((unsigned char)tag[q])) ++q; + if (ok_before && q < tag.size() && tag[q] == '=') + { + ++q; + while (q < tag.size() && std::isspace((unsigned char)tag[q])) ++q; + if (q < tag.size() && (tag[q] == '"' || tag[q] == '\'')) + { + char quote = tag[q]; + std::size_t start = q + 1, end = tag.find(quote, start); + if (end != std::string::npos) { out = tag.substr(start, end - start); return true; } + } + } + pos += name.size(); + } + return false; + } + + OmeAxes parse_ome_xml(const std::string& xml) + { + OmeAxes ax; + const std::string pix = element_start_tag(xml, "Pixels"); + if (pix.empty()) + return ax; // valid stays false + + auto s = [&](const char* n, const std::string& def) { std::string v; return get_attr(pix, n, v) ? v : def; }; + // Size reader (std::from_chars): absent / non-numeric / negative / overflow + // -> default; "0" -> 1. OME extents are >=1; from_chars rejects a leading + // '-' and overflow via errc, so no strtoull wrap or divide-by-zero leaks out. + auto i = [&](const char* n, std::size_t def) -> std::size_t { + std::string v; if (!get_attr(pix, n, v)) return def; + std::size_t r = 0; + auto [ptr, ec] = std::from_chars(v.data(), v.data() + v.size(), r); + if (ec != std::errc() || ptr == v.data()) return def; + return r < 1 ? std::size_t(1) : r; + }; + // Double reader (std::from_chars): absent / non-numeric / non-finite -> + // default. from_chars still accepts "nan"/"inf" prefixes (e.g. "NaNsense"), + // so the isfinite check keeps those out of physical-calibration math. + auto d = [&](const char* n, double def) -> double { + std::string v; if (!get_attr(pix, n, v)) return def; + double r = 0; + auto [ptr, ec] = std::from_chars(v.data(), v.data() + v.size(), r); + if (ec != std::errc() || ptr == v.data() || !std::isfinite(r)) return def; + return r; + }; + + ax.sizeX = i("SizeX", 1); + ax.sizeY = i("SizeY", 1); + ax.sizeZ = i("SizeZ", 1); + ax.sizeC = i("SizeC", 1); + ax.sizeT = i("SizeT", 1); + + // DimensionOrder must be a permutation of the 5 canonical axes; a missing + // or malformed value falls back to the OME default so storageOrder stays sane. + std::string dord = s("DimensionOrder", "XYZCT"); + bool perm = dord.size() == 5; + for (char c : std::string("XYZCT")) + if (std::count(dord.begin(), dord.end(), c) != 1) perm = false; + ax.omeDimensionOrder = perm ? dord : "XYZCT"; + + ax.dtype = pixel_type_from_ome_type(s("Type", "uint16")); + if (ax.dtype == PixelType::Unknown) ax.dtype = PixelType::UInt16; + ax.bitsPerSample = bits_of(ax.dtype); + + ax.physX = d("PhysicalSizeX", 1.0); + ax.physY = d("PhysicalSizeY", 1.0); + ax.physZ = d("PhysicalSizeZ", 1.0); + ax.timeIncrement = d("TimeIncrement", 1.0); + ax.unitXY = s("PhysicalSizeXUnit", ""); + ax.unitZ = s("PhysicalSizeZUnit", ""); + ax.unitT = s("TimeIncrementUnit", ""); + + // On-disk order = reverse(DimensionOrder) with singleton axes dropped + // (planes are XY, so the result always ends in "YX"). + auto size_of = [&](char c) -> std::size_t { + switch (c) { case 'X': return ax.sizeX; case 'Y': return ax.sizeY; + case 'Z': return ax.sizeZ; case 'C': return ax.sizeC; case 'T': return ax.sizeT; } + return 1; + }; + std::string rev(ax.omeDimensionOrder.rbegin(), ax.omeDimensionOrder.rend()); + for (char c : rev) + if (c == 'X' || c == 'Y' || size_of(c) > 1) + ax.storageOrder.push_back(c); + + for (char c : ax.storageOrder) + { + OmeAxis a; + a.label = c; + a.kind = axis_kind_of(c); + a.size = size_of(c); + if (c == 'X') { a.physical = ax.physX; a.unit = ax.unitXY; } + else if (c == 'Y') { a.physical = ax.physY; a.unit = ax.unitXY; } + else if (c == 'Z') { a.physical = ax.physZ; a.unit = ax.unitZ; } + else if (c == 'T') { a.physical = ax.timeIncrement; a.unit = ax.unitT; } + else /* C */ { a.physical = 1.0; } + ax.storageAxes.push_back(a); + } + + // Single full-res level (SubIFDs pyramid parsing not implemented yet). + OmePyramidLevel lv; + lv.index = 0; + lv.sizeX = ax.sizeX; lv.sizeY = ax.sizeY; lv.sizeZ = ax.sizeZ; + lv.scaleX = ax.physX; lv.scaleY = ax.physY; lv.scaleZ = ax.physZ; + lv.path = "0"; + ax.levels.push_back(lv); + + // Optional channel names. + std::size_t cp = 0; + while ((cp = xml.find("') + { + std::size_t gt = xml.find('>', cp); + if (gt == std::string::npos) break; + std::string ctag = xml.substr(cp, gt - cp + 1); + std::string nm; + if (get_attr(ctag, "Name", nm)) ax.channelNames.push_back(nm); + cp = gt + 1; + } + else cp += 8; + } + + ax.valid = true; + return ax; + } +} diff --git a/src/nyx/ome/ome_tiff_meta.h b/src/nyx/ome/ome_tiff_meta.h new file mode 100644 index 000000000..dd0316138 --- /dev/null +++ b/src/nyx/ome/ome_tiff_meta.h @@ -0,0 +1,19 @@ +#pragma once + +// OME-TIFF metadata -> OmeAxes. Parses the dimensional attributes of the OME-XML +// element (SizeX/Y/Z/C/T, DimensionOrder, Type, PhysicalSize*, +// TimeIncrement) plus names. STL-only. +// +// Currently resolves dimensions + calibration + dtype. The TiffData plane->IFD +// map and SubIFDs pyramids are not parsed yet; levels here is a single full-res +// level. + +#include +#include "ome_axes.h" + +namespace Nyxus +{ + // Parse an OME-XML document (the string from a TIFF IFD-0 ImageDescription). + // Returns an OmeAxes with .valid==false if no element is found. + OmeAxes parse_ome_xml(const std::string& omeXml); +} diff --git a/src/nyx/ome/ome_zarr_meta.cpp b/src/nyx/ome/ome_zarr_meta.cpp new file mode 100644 index 000000000..0665b40e3 --- /dev/null +++ b/src/nyx/ome/ome_zarr_meta.cpp @@ -0,0 +1,123 @@ +#ifdef OMEZARR_SUPPORT + +#include "ome_zarr_meta.h" +#include + +namespace Nyxus +{ + // Pull the 'scale' vector out of a dataset's coordinateTransformations (the + // entry whose type == "scale"); empty if the key/entry is absent or malformed. + // Safe on datasets that lack coordinateTransformations entirely (else a const + // operator[] on a missing key throws). + static std::vector scale_of(const nlohmann::json& dset) + { + std::vector s; + if (!dset.is_object() || !dset.contains("coordinateTransformations")) + return s; + const auto& cts = dset["coordinateTransformations"]; + if (cts.is_array()) + for (const auto& ct : cts) + if (ct.is_object() && ct.value("type", std::string()) == "scale" && + ct.contains("scale") && ct["scale"].is_array()) + { + for (const auto& v : ct["scale"]) s.push_back(v.is_number() ? v.get() : 1.0); + break; + } + return s; + } + + OmeAxes parse_ome_zarr(const nlohmann::json& groupAttrs, + const std::vector& level0Shape, + const std::string& dtypeStr) + { + OmeAxes ax; + + // 0.5 nests the model under "ome"; 0.4 puts it at the group root. + const nlohmann::json* root = nullptr; + if (groupAttrs.contains("ome") && groupAttrs["ome"].contains("multiscales")) + root = &groupAttrs["ome"]; + else if (groupAttrs.contains("multiscales")) + root = &groupAttrs; + if (!root) + return ax; + + const auto& multiscales = (*root)["multiscales"]; + if (!multiscales.is_array() || multiscales.empty()) + return ax; + const auto& ms = multiscales[0]; + if (!ms.contains("axes") || !ms.contains("datasets")) + return ax; + + const auto& axes = ms["axes"]; + const auto& datasets = ms["datasets"]; + if (!axes.is_array() || !datasets.is_array() || datasets.empty()) + return ax; + + // Level-0 scale (per-axis), from the first dataset's transformations. + std::vector scale0 = scale_of(datasets[0]); + if (scale0.size() != axes.size()) + scale0.assign(axes.size(), 1.0); + + ax.dtype = pixel_type_from_zarr_dtype(dtypeStr); + if (ax.dtype == PixelType::Unknown) ax.dtype = PixelType::UInt16; + ax.bitsPerSample = bits_of(ax.dtype); + + for (std::size_t k = 0; k < axes.size(); ++k) + { + const auto& a = axes[k]; + if (!a.is_object()) // a malformed axis entry -> skip (keeps role sizes at default) + continue; + std::string name = a.value("name", std::string()); + std::string type = a.value("type", std::string()); + std::string unit = a.value("unit", std::string()); + char label = name.empty() ? '?' : (char)std::toupper((unsigned char)name[0]); + + OmeAxis oa; + oa.label = label; + if (type == "channel") oa.kind = AxisKind::Channel; + else if (type == "time") oa.kind = AxisKind::Time; + else oa.kind = AxisKind::Space; + oa.size = (k < level0Shape.size()) ? level0Shape[k] : 1; + oa.physical = (k < scale0.size()) ? scale0[k] : 1.0; + oa.unit = unit; + ax.storageAxes.push_back(oa); + ax.storageOrder.push_back(label); + + switch (label) + { + case 'X': ax.sizeX = oa.size; ax.physX = oa.physical; ax.unitXY = unit; break; + case 'Y': ax.sizeY = oa.size; ax.physY = oa.physical; if (ax.unitXY.empty()) ax.unitXY = unit; break; + case 'Z': ax.sizeZ = oa.size; ax.physZ = oa.physical; ax.unitZ = unit; break; + case 'C': ax.sizeC = oa.size; break; + case 'T': ax.sizeT = oa.size; ax.timeIncrement = oa.physical; ax.unitT = unit; break; + default: break; + } + } + + ax.omeDimensionOrder = ome_dimension_order_from_storage(ax.storageOrder); + + // Pyramid levels: one per dataset entry. + int ix = ax.storageIndexOf('X'), iy = ax.storageIndexOf('Y'), iz = ax.storageIndexOf('Z'); + for (std::size_t li = 0; li < datasets.size(); ++li) + { + const auto& dset = datasets[li]; + OmePyramidLevel lv; + lv.index = li; + lv.path = dset.is_object() ? dset.value("path", std::to_string(li)) : std::to_string(li); + std::vector sc = scale_of(dset); + if (ix >= 0 && (std::size_t)ix < sc.size()) lv.scaleX = sc[ix]; + if (iy >= 0 && (std::size_t)iy < sc.size()) lv.scaleY = sc[iy]; + if (iz >= 0 && (std::size_t)iz < sc.size()) lv.scaleZ = sc[iz]; + if (li == 0) // only level-0 extents are known from the supplied shape + { + lv.sizeX = ax.sizeX; lv.sizeY = ax.sizeY; lv.sizeZ = ax.sizeZ; + } + ax.levels.push_back(lv); + } + + ax.valid = true; + return ax; + } +} + +#endif // OMEZARR_SUPPORT diff --git a/src/nyx/ome/ome_zarr_meta.h b/src/nyx/ome/ome_zarr_meta.h new file mode 100644 index 000000000..08d085966 --- /dev/null +++ b/src/nyx/ome/ome_zarr_meta.h @@ -0,0 +1,34 @@ +#pragma once + +// OME-Zarr (NGFF) metadata -> OmeAxes. Parses the group `multiscales` +// block: `axes` (order + type + unit), `datasets` (pyramid paths + +// coordinateTransformations), for both 0.4 (metadata at the group root) and 0.5 +// (metadata under the "ome" key). The caller supplies the level-0 array shape +// and dtype (read from the dataset's .zarray/zarr.json via z5) so this parser +// stays decoupled from the storage backend. +// +// Guarded by OMEZARR_SUPPORT because it depends on nlohmann::json (vendored with +// z5). Only meaningful when Nyxus is built with OME-Zarr support. + +#ifdef OMEZARR_SUPPORT + +#include +#include +#include +#include "nlohmann/json.hpp" +#include "ome_axes.h" + +namespace Nyxus +{ + // Parse NGFF group attributes into an OmeAxes. + // groupAttrs : the group's attributes JSON (.zattrs contents for v2, or the + // "attributes" object of zarr.json for v3). + // level0Shape : shape of the level-0 dataset, in on-disk axis order. + // dtypeStr : the level-0 dtype string (v2 "& level0Shape, + const std::string& dtypeStr); +} + +#endif // OMEZARR_SUPPORT diff --git a/src/nyx/omezarr.h b/src/nyx/omezarr.h index 5222657c3..1c96f6952 100644 --- a/src/nyx/omezarr.h +++ b/src/nyx/omezarr.h @@ -3,8 +3,11 @@ #ifdef OMEZARR_SUPPORT #include +#include +#include #include "abs_tile_loader.h" #include "nlohmann/json.hpp" +#include "ome/ome_zarr_meta.h" // parse_ome_zarr -> OmeAxes (axis-role resolution) // factory functions to create files, groups and datasets #include "z5/factory.hxx" @@ -40,20 +43,59 @@ class NyxusOmeZarrLoader : public AbstractTileLoader nlohmann::json file_attributes, ds_attributes; z5::readAttributes(*zarr_ptr_, file_attributes); - // assume only one dataset is present + // open the highest-resolution dataset (level 0) ds_name_ = file_attributes["multiscales"][0]["datasets"][0]["path"].get(); const auto ds_handle = z5::filesystem::handle::Dataset(*zarr_ptr_, ds_name_); fs::path metadata_path; auto success = z5::filesystem::metadata_detail::getMetadataPath(ds_handle, metadata_path); z5::filesystem::metadata_detail::readMetadata(metadata_path, ds_attributes); - - full_depth_ = ds_attributes["shape"][2].get(); - full_height_ = ds_attributes["shape"][3].get(); - full_width_ = ds_attributes["shape"][4].get(); - tile_depth_ = ds_attributes["chunks"][2].get(); - tile_height_ = ds_attributes["chunks"][3].get(); - tile_width_ = ds_attributes["chunks"][4].get(); + + std::vector level0Shape, chunkShape; + for (const auto& d : ds_attributes["shape"]) level0Shape.push_back(d.get()); + for (const auto& d : ds_attributes["chunks"]) chunkShape.push_back(d.get()); std::string dtype_str = ds_attributes["dtype"].get(); + + // Resolve axis roles from the NGFF 'axes' metadata instead of assuming a + // fixed [T,C,Z,Y,X] order. Falls back to legacy 5D TCZYX if 'axes' is absent. + Nyxus::OmeAxes axes = Nyxus::parse_ome_zarr(file_attributes, level0Shape, dtype_str); + if (axes.valid) + { + // Reject self-inconsistent metadata rather than guess: if the 'axes' + // count disagrees with the array rank, indexing the shape by axis role + // would read out of bounds. + if (axes.storageAxes.size() != level0Shape.size()) + throw std::runtime_error("OME-Zarr: 'axes' count " + std::to_string(axes.storageAxes.size()) + + " does not match array rank " + std::to_string(level0Shape.size())); + ndim_ = axes.storageAxes.size(); + ix_ = axes.storageIndexOf('X'); iy_ = axes.storageIndexOf('Y'); + iz_ = axes.storageIndexOf('Z'); ic_ = axes.storageIndexOf('C'); + it_ = axes.storageIndexOf('T'); + n_levels_ = axes.numberPyramidLevels(); + } + else + { + // No usable 'axes': map by position (X,Y last; Z,C,T before) — rank-safe. + ndim_ = level0Shape.size(); + int n = (int)ndim_; + ix_ = n - 1; iy_ = n - 2; + iz_ = (n >= 3) ? n - 3 : -1; + ic_ = (n >= 4) ? n - 4 : -1; + it_ = (n >= 5) ? n - 5 : -1; + n_levels_ = 1; + } + // X and Y must resolve to real dimensions, else the read would index OOB. + if (ix_ < 0 || iy_ < 0 || (size_t)ix_ >= level0Shape.size() || (size_t)iy_ >= level0Shape.size()) + throw std::runtime_error("OME-Zarr: cannot resolve X/Y axes from metadata"); + + full_width_ = level0Shape[ix_]; + full_height_ = level0Shape[iy_]; + full_depth_ = (iz_ >= 0) ? level0Shape[iz_] : 1; + tile_width_ = chunkShape[ix_]; + tile_height_ = chunkShape[iy_]; + tile_depth_ = (iz_ >= 0) ? chunkShape[iz_] : 1; + bits_per_sample_ = Nyxus::bits_of(Nyxus::pixel_type_from_zarr_dtype(dtype_str)); + + // dtype -> internal format code for the read-template dispatch if (dtype_str == " size_t indexRowGlobalTile, size_t indexColGlobalTile, size_t indexLayerGlobalTile, - [[maybe_unused]] size_t level) override + size_t indexChannel, // C plane to read (offset into the channel axis) + size_t indexTimeframe, // T plane to read (offset into the time axis) + [[maybe_unused]] size_t level) override { size_t pixel_row_index = indexRowGlobalTile*tile_height_; size_t pixel_col_index = indexColGlobalTile*tile_width_; size_t pixel_layer_index = indexLayerGlobalTile*tile_depth_; - + switch (data_format_) { case 1: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 2: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 3: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 4: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 5: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 6: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 7: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 8: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 9: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 10: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; default: - loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(tile, pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; } } - + template - void loadTile(std::shared_ptr> &dest, size_t pixel_row_index, - size_t pixel_col_index, size_t pixel_layer_index) { + void loadTile(std::shared_ptr> &dest, size_t pixel_row_index, + size_t pixel_col_index, size_t pixel_layer_index, + size_t pixel_channel_index, size_t pixel_timeframe_index) { size_t data_height = tile_height_, data_width = tile_width_; if (pixel_row_index + data_height > full_height_) { data_height = full_height_ - pixel_row_index; @@ -148,11 +193,16 @@ class NyxusOmeZarrLoader : public AbstractTileLoader // Create a buffer to hold the read data std::vector buffer(data_height * data_width); - - // Create an ArrayView into the buffer (z5 3.0.1 uses ArrayView instead of xtensor) - z5::types::ShapeType shape = {1, 1, 1, data_height, data_width}; + + // Build the read window by axis ROLE (honoring the resolved 'axes' order), + // reading one Y*X plane at the requested Z/C/T. z5 3.0.1 uses ArrayView. + z5::types::ShapeType shape(ndim_, 1), offset(ndim_, 0); + shape[iy_] = data_height; offset[iy_] = pixel_row_index; + shape[ix_] = data_width; offset[ix_] = pixel_col_index; + if (iz_ >= 0) offset[iz_] = pixel_layer_index; + if (ic_ >= 0) offset[ic_] = pixel_channel_index; + if (it_ >= 0) offset[it_] = pixel_timeframe_index; auto view = z5::multiarray::makeView(buffer.data(), shape); - z5::types::ShapeType offset = {0, 0, pixel_layer_index, pixel_row_index, pixel_col_index}; // Read subarray from the cached z5 dataset z5::multiarray::readSubarray(*ds_, view, offset.begin()); @@ -191,12 +241,10 @@ class NyxusOmeZarrLoader : public AbstractTileLoader /// @return Tile depth [[nodiscard]] size_t tileDepth([[maybe_unused]] size_t level) const override { return tile_depth_; } - /// @brief Tiff bits per sample - /// @return Size of a sample in bits - [[nodiscard]] short bitsPerSample() const override { return 1; } - /// @brief Level accessor - /// @return 1 - [[nodiscard]] size_t numberPyramidLevels() const override { return 1; } + /// @brief Bits per sample (resolved from the dataset dtype) + [[nodiscard]] short bitsPerSample() const override { return bits_per_sample_; } + /// @brief Number of resolution (pyramid) levels declared in multiscales + [[nodiscard]] size_t numberPyramidLevels() const override { return n_levels_; } private: @@ -208,6 +256,12 @@ class NyxusOmeZarrLoader : public AbstractTileLoader tile_height_ = 0, ///< Tile height tile_depth_ = 0; ///< Tile depth + // Storage-dimension index of each axis role (-1 if the axis is absent). + int ix_ = 4, iy_ = 3, iz_ = 2, ic_ = 1, it_ = 0; + size_t ndim_ = 5; ///< Number of on-disk dimensions (2..5) + short bits_per_sample_ = 16; ///< Real bit depth + size_t n_levels_ = 1; ///< Pyramid level count + short data_format_ = 0; std::unique_ptr zarr_ptr_; std::string ds_name_; diff --git a/src/nyx/phase1.cpp b/src/nyx/phase1.cpp index 51f487247..00dded8bb 100644 --- a/src/nyx/phase1.cpp +++ b/src/nyx/phase1.cpp @@ -279,47 +279,37 @@ namespace Nyxus int cnt = 1; - // Fetch a tile - bool ok = env.theImLoader.load_tile (0/*row*/ , 0/*col*/); + // FIX (IO): assemble the whole X*Y*Z volume for this timeframe (load_volume + // loops Z), so plane-by-plane loaders (OME-Zarr, multi-page/OME-TIFF) yield a + // real 3D volume here too — Phase-1 ROI metrics must see the same voxels the + // Phase-2 scan does. Mask on frame t_index (1:1) or frame 0 (1 mask : N int). + const size_t maskFrame = (nVoxI == nVoxM) ? t_index : 0; + bool ok = env.theImLoader.load_volume (0/*channel*/, t_index, maskFrame); if (!ok) { std::string erm = "Error fetching tile (0,0) from I:" + intens_fpath + " M:" + mask_fpath; #ifdef WITH_PYTHON_H throw erm; - #endif + #endif std::cerr << erm << "\n"; return false; } - // Get ahold of tile's pixel buffer - auto dataI = env.theImLoader.get_int_tile_buffer(), - dataL = env.theImLoader.get_seg_tile_buffer(); + // Both buffers hold exactly this timeframe's X*Y*Z volume (frame already selected). + const std::vector& dataI = env.theImLoader.get_int_volume_buffer(); + const std::vector& dataL = env.theImLoader.get_seg_volume_buffer(); - size_t baseI, baseM; - if (nVoxI == nVoxM) - { - baseM = baseI = t_index * timeFrameSize; - } - else // nVoxI > nVoxM - { - baseM = 0; - baseI = t_index * timeFrameSize; - } - - // Iterate voxels + // Iterate the single-frame volume's voxels for (size_t i=0; i= w || y >= h || z >= d) @@ -330,7 +320,7 @@ namespace Nyxus label = 1; // Update pixel's ROI metrics - feed_pixel_2_metrics_3D (env.uniqueLabels, env.roiData, x, y, z, dataI[j], label, sidx); + feed_pixel_2_metrics_3D (env.uniqueLabels, env.roiData, x, y, z, dataI[i], label, sidx); } #ifdef WITH_PYTHON_H diff --git a/src/nyx/phase2_3d.cpp b/src/nyx/phase2_3d.cpp index f081ebd0e..d942a8511 100644 --- a/src/nyx/phase2_3d.cpp +++ b/src/nyx/phase2_3d.cpp @@ -83,41 +83,33 @@ namespace Nyxus int cnt = 1; - // fetch 3D data - bool ok = env.theImLoader.load_tile (0/*row*/, 0/*col*/); + // FIX (IO): assemble the whole X*Y*Z volume for this timeframe via load_volume, + // which loops Z internally. The previous load_tile(0,0) delivered only plane + // z=0 for plane-by-plane loaders (OME-Zarr, multi-page/OME-TIFF), so those never + // read a real 3D volume. The mask sits on frame t_index (1:1) or frame 0 (the + // 1-mask : N-intensity case); the intensity sits on frame t_index. + const size_t maskFrame = (timeI == timeM) ? t_index : 0; + bool ok = env.theImLoader.load_volume (0/*channel*/, t_index, maskFrame); if (!ok) { std::string erm = "Error fetching segmented data from " + intens_fpath + "(I) " + label_fpath + "(M)"; #ifdef WITH_PYTHON_H throw erm; -#endif +#endif std::cerr << erm << "\n"; return false; } - // Get ahold of tile's pixel buffer - auto dataI = env.theImLoader.get_int_tile_buffer(), - dataL = env.theImLoader.get_seg_tile_buffer(); - - size_t baseI, baseM; - if (timeI == timeM) - { - baseM = baseI = t_index * timeFrameSize; - } - else // nVoxI > nVoxM - { - baseM = 0; - baseI = t_index * timeFrameSize; - } + // Both buffers now hold exactly this timeframe's X*Y*Z volume (the frame was + // already selected by load_volume), so no base offset is needed. + const std::vector& dataI = env.theImLoader.get_int_volume_buffer(); + const std::vector& dataL = env.theImLoader.get_seg_volume_buffer(); - // Iterate voxels - for (size_t i=0; i= w || y >= h || z >= d) @@ -139,9 +129,9 @@ namespace Nyxus if (env.singleROI) label = 1; - // Cache this pixel + // Cache this pixel LR& r = env.roiData[label]; - feed_pixel_2_cache_3D_LR (x, y, z, dataI[j], r); + feed_pixel_2_cache_3D_LR (x, y, z, dataI[i], r); } #ifdef WITH_PYTHON_H @@ -343,35 +333,23 @@ namespace Nyxus int cnt = 1; - // fetch 3D data - if (!env.theImLoader.load_tile (0/*row*/, 0/*col*/)) + // FIX (IO): assemble the whole X*Y*Z volume for this timeframe (load_volume + // loops Z), so plane-by-plane loaders (OME-Zarr, multi-page/OME-TIFF) read a + // real 3D volume here too. Mask on frame t_index (1:1) or frame 0 (1 mask : N). + const size_t maskFrame = (timeI == timeM) ? t_index : 0; + if (!env.theImLoader.load_volume (0/*channel*/, t_index, maskFrame)) { std::string erm = "Error fetching data from file pair " + intens_fpath + "(I) " + label_fpath + "(M)"; #ifdef WITH_PYTHON_H throw erm; - #endif + #endif std::cerr << erm << "\n"; return false; } - // get ahold of voxel buffers - auto dataI = env.theImLoader.get_int_tile_buffer(), - dataL = env.theImLoader.get_seg_tile_buffer(); - - // align time frame's mask and intensity volumes - size_t baseI, baseM; - if (timeI == timeM) - { - // trivial N mask : N intensity - baseM = - baseI = t_index * timeFrameSize; - } - else - { - // nontrivial 1 mask : N intensity - baseM = 0; - baseI = t_index * timeFrameSize; - } + // Both buffers hold exactly this timeframe's X*Y*Z volume (frame already selected). + const std::vector& dataI = env.theImLoader.get_int_volume_buffer(); + const std::vector& dataL = env.theImLoader.get_seg_volume_buffer(); // virtual dimensions size_t virt_h = h * aniso_y, @@ -408,7 +386,7 @@ namespace Nyxus // // skip non-mask pixels - auto lbl = dataL[baseM + i]; + auto lbl = dataL[i]; if (!lbl) continue; @@ -432,8 +410,8 @@ namespace Nyxus assert(vX < virt_w); #endif - // cache this voxel - auto inten = dataI[baseI + i]; + // cache this voxel + auto inten = dataI[i]; LR& r = env.roiData[lbl]; feed_pixel_2_cache_3D_LR (vX, vY, vZ, inten, r); } @@ -468,18 +446,20 @@ namespace Nyxus sliceSize = fullwidth * fullheight, nVox = sliceSize * fullD; - // in the 3D case tiling is a formality, so fetch the only tile in the file - if (!ilo.load_tile(0, 0)) + // FIX (IO): assemble the whole X*Y*Z volume (load_volume loops Z) so plane-by- + // plane loaders (OME-Zarr, multi-page/OME-TIFF) read the full volume, not just + // plane z=0. Whole-slide is single-channel, single-timeframe. + if (!ilo.load_volume(0/*channel*/, 0/*timeframe*/, 0/*mask*/)) { #ifdef WITH_PYTHON_H throw "Error fetching tile"; -#endif +#endif std::cerr << "Error fetching tile\n"; return false; } - // Get ahold of tile's pixel buffer - const std::vector& dataI = ilo.get_int_tile_buffer(); + // Get ahold of the assembled volume buffer + const std::vector& dataI = ilo.get_int_volume_buffer(); // iterate abstract tiles (in a tiled slide /e.g. tiled tiff/ they correspond to physical tiles, in a nontiled slide /e.g. scanline tiff or strip tiff/ they correspond to ) int cnt = 1; @@ -531,18 +511,19 @@ namespace Nyxus vw = (size_t) (double(fullW) * aniso_x), vd = (size_t) (double(fullD) * aniso_z); - // in the 3D case tiling is a formality, so fetch the only tile in the file - if (! ilo.load_tile(0, 0)) + // FIX (IO): assemble the whole X*Y*Z volume (load_volume loops Z) so plane-by- + // plane loaders read the full volume, not just plane z=0. + if (! ilo.load_volume(0/*channel*/, 0/*timeframe*/, 0/*mask*/)) { #ifdef WITH_PYTHON_H throw "Error loading volume data"; -#endif +#endif std::cerr << "Error loading volume data\n"; return false; } - // Get ahold of tile's pixel buffer - const std::vector& dataI = ilo.get_int_tile_buffer(); + // Get ahold of the assembled volume buffer + const std::vector& dataI = ilo.get_int_volume_buffer(); // iterate virtual voxels size_t vSliceSize = vh * vw, diff --git a/src/nyx/raw_dicom.h b/src/nyx/raw_dicom.h index ad4d602c7..e0e8e0543 100644 --- a/src/nyx/raw_dicom.h +++ b/src/nyx/raw_dicom.h @@ -133,6 +133,8 @@ class RawDicomLoader : public RawFormatLoader size_t indexRowGlobalTile, size_t indexColGlobalTile, size_t indexLayerGlobalTile, + [[maybe_unused]] size_t indexChannel, // DICOM grayscale: single channel + [[maybe_unused]] size_t indexTimeframe, // DICOM: frames are WSI tiles, not time [[maybe_unused]] size_t level) override { tile.resize (tileWidth_ * tileHeight_); diff --git a/src/nyx/raw_format.h b/src/nyx/raw_format.h index 7dc9de273..27c13d75c 100644 --- a/src/nyx/raw_format.h +++ b/src/nyx/raw_format.h @@ -19,7 +19,9 @@ class RawFormatLoader virtual void loadTileFromFile ( size_t indexRowGlobalTile, size_t indexColGlobalTile, - size_t indexLayerGlobalTile, + size_t indexLayerGlobalTile, // Z + size_t indexChannel, // C; 0 for single-channel data + size_t indexTimeframe, // T; 0 for non-time-series data size_t level) = 0; virtual void free_tile() = 0; // must follow loadTileFromFile() @@ -52,6 +54,10 @@ class RawFormatLoader return 1; } + [[nodiscard]] virtual size_t tileTimestamps([[maybe_unused]] size_t level) const { + return 1; + } + [[nodiscard]] virtual short bitsPerSample() const = 0; [[nodiscard]] virtual size_t numberPyramidLevels() const = 0; diff --git a/src/nyx/raw_image_loader.cpp b/src/nyx/raw_image_loader.cpp index 350d1ba6b..d85232240 100644 --- a/src/nyx/raw_image_loader.cpp +++ b/src/nyx/raw_image_loader.cpp @@ -9,6 +9,7 @@ #include "raw_nifti.h" #include "raw_omezarr.h" #include "raw_tiff.h" +#include "ome/format_detect.h" // FIX: unified content-sniffing loader dispatch RawImageLoader::RawImageLoader() {} @@ -16,9 +17,11 @@ bool RawImageLoader::open (const std::string& int_fpath, const std::string& seg_ { try { - std::string ext = Nyxus::get_big_extension(int_fpath); + // FIX: classify via detect_input_format(). Defects fixed: (1) matched only ".zarr" so + // ".ome.zarr" mis-routed to TIFF; (2) `ext==".dcm" | ext==".dicom"` used bitwise-OR (works by luck). + Nyxus::InputFormat fmt = Nyxus::detect_input_format (int_fpath); - if (ext == ".zarr") + if (fmt.kind == Nyxus::ContainerKind::OmeZarr) // FIX: was `ext==".zarr"` only (dropped .ome.zarr) { #ifdef OMEZARR_SUPPORT intFL = new RawOmezarrLoader (int_fpath); @@ -26,8 +29,8 @@ bool RawImageLoader::open (const std::string& int_fpath, const std::string& seg_ std::cout << "This version of Nyxus was not build with OmeZarr support." << std::endl; #endif } - else - if (ext == ".dcm" | ext == ".dicom") { + else + if (fmt.kind == Nyxus::ContainerKind::Dicom) { // FIX: was bitwise `ext==".dcm" | ext==".dicom"` #ifdef DICOM_SUPPORT intFL = new RawDicomLoader (int_fpath, preserve_hu); // CT/HU: scan in Hounsfield domain #else @@ -35,7 +38,7 @@ bool RawImageLoader::open (const std::string& int_fpath, const std::string& seg_ #endif } else - if (ext == ".nii" || ext == ".nii.gz") + if (fmt.kind == Nyxus::ContainerKind::Nifti) // FIX: was `ext==".nii"||".nii.gz"` { intFL = new RawNiftiLoader (int_fpath, preserve_hu); // FIX: CT/HU: scan in Hounsfield domain (matches DICOM) } @@ -82,8 +85,10 @@ bool RawImageLoader::open (const std::string& int_fpath, const std::string& seg_ // segmented slide try { - std::string ext = Nyxus::get_big_extension(seg_fpath); - if (ext == ".zarr") + // FIX: unified seg dispatch — same defects as the intensity path (".ome.zarr" dropped, + // bitwise-OR on the DICOM test) fixed here via detect_input_format(). + Nyxus::InputFormat fmt = Nyxus::detect_input_format (seg_fpath); + if (fmt.kind == Nyxus::ContainerKind::OmeZarr) // FIX: was `ext==".zarr"` only (dropped .ome.zarr) { #ifdef OMEZARR_SUPPORT segFL = new RawOmezarrLoader (seg_fpath); @@ -91,8 +96,8 @@ bool RawImageLoader::open (const std::string& int_fpath, const std::string& seg_ std::cout << "This version of Nyxus was not build with OmeZarr support." << std::endl; #endif } - else - if (ext == ".dcm" | ext == ".dicom") + else + if (fmt.kind == Nyxus::ContainerKind::Dicom) // FIX: was bitwise `ext==".dcm" | ext==".dicom"` { #ifdef DICOM_SUPPORT segFL = new RawDicomLoader (seg_fpath); @@ -101,7 +106,7 @@ bool RawImageLoader::open (const std::string& int_fpath, const std::string& seg_ #endif } else - if (ext == ".nii" || ext == ".nii.gz") + if (fmt.kind == Nyxus::ContainerKind::Nifti) // FIX: was `ext==".nii"||".nii.gz"` { segFL = new RawNiftiLoader (seg_fpath); } @@ -183,11 +188,11 @@ bool RawImageLoader::load_tile(size_t tile_idx) auto row = tile_idx / ntw; auto col = tile_idx % ntw; - intFL->loadTileFromFile (row, col, lyr, lvl); + intFL->loadTileFromFile (row, col, lyr, cur_channel, cur_timeframe, lvl); // segmentation loader is not available in wholeslide if (segFL) - segFL->loadTileFromFile (row, col, lyr, lvl); + segFL->loadTileFromFile (row, col, lyr, cur_channel, cur_timeframe, lvl); return true; } @@ -197,12 +202,12 @@ bool RawImageLoader::load_tile(size_t tile_row, size_t tile_col) if (tile_row >= nth || tile_col >= ntw) return false; - intFL->loadTileFromFile (tile_row, tile_col, lyr, lvl); - + intFL->loadTileFromFile (tile_row, tile_col, lyr, cur_channel, cur_timeframe, lvl); + // segmentation loader is not available in wholeslide if (segFL) - segFL->loadTileFromFile (tile_row, tile_col, lyr, lvl); - + segFL->loadTileFromFile (tile_row, tile_col, lyr, cur_channel, cur_timeframe, lvl); + return true; } diff --git a/src/nyx/raw_image_loader.h b/src/nyx/raw_image_loader.h index 62d506454..c871e6968 100644 --- a/src/nyx/raw_image_loader.h +++ b/src/nyx/raw_image_loader.h @@ -35,6 +35,13 @@ class RawImageLoader size_t get_inten_time(); size_t get_mask_time(); + // Select which channel (C) / timeframe (T) plane subsequent load_tile() calls + // read. Default 0/0 preserves the single-channel, single-timepoint behavior. + void set_channel (size_t c) { cur_channel = c; } + void set_timeframe (size_t t) { cur_timeframe = t; } + size_t get_channel() const { return cur_channel; } + size_t get_timeframe() const { return cur_timeframe; } + std::string get_slide_descr(); bool get_fp_phys_pixvoxels(); @@ -63,5 +70,8 @@ class RawImageLoader int lvl = 0, // Pyramid level lyr = 0; // Layer + size_t cur_channel = 0, // Currently selected channel (C) plane + cur_timeframe = 0; // Currently selected timeframe (T) plane + }; diff --git a/src/nyx/raw_nifti.h b/src/nyx/raw_nifti.h index ad85db9e2..e70026a11 100644 --- a/src/nyx/raw_nifti.h +++ b/src/nyx/raw_nifti.h @@ -101,6 +101,8 @@ class RawNiftiLoader : public RawFormatLoader size_t indexRowGlobalTile, size_t indexColGlobalTile, size_t indexLayerGlobalTile, + [[maybe_unused]] size_t indexChannel, // NIfTI: single channel + [[maybe_unused]] size_t indexTimeframe, // whole 4D volume held in memory; T sliced by consumer [[maybe_unused]] size_t level) override {} // NIFTI is not tiled @@ -223,6 +225,8 @@ class NiftiLoader : public AbstractTileLoader size_t indexRowGlobalTile, size_t indexColGlobalTile, size_t indexLayerGlobalTile, + [[maybe_unused]] size_t indexChannel, // NIfTI: single channel + [[maybe_unused]] size_t indexTimeframe, // whole 4D volume delivered at once; T sliced by consumer [[maybe_unused]] size_t level) override { tile->resize (tile_width_ * tile_height_ * tile_depth_ * numTimeFrames_); diff --git a/src/nyx/raw_omezarr.h b/src/nyx/raw_omezarr.h index 687814f90..d1f2af085 100644 --- a/src/nyx/raw_omezarr.h +++ b/src/nyx/raw_omezarr.h @@ -3,7 +3,10 @@ #ifdef OMEZARR_SUPPORT #include +#include +#include #include "nlohmann/json.hpp" +#include "ome/ome_zarr_meta.h" // parse_ome_zarr -> OmeAxes (axis-role resolution) // factory functions to create files, groups and datasets #include "z5/factory.hxx" @@ -32,20 +35,59 @@ class RawOmezarrLoader: public RawFormatLoader nlohmann::json file_attributes, ds_attributes; z5::readAttributes(*zarr_ptr_, file_attributes); - // assume only one dataset is present + // open the highest-resolution dataset (level 0) ds_name_ = file_attributes["multiscales"][0]["datasets"][0]["path"].get(); const auto ds_handle = z5::filesystem::handle::Dataset(*zarr_ptr_, ds_name_); fs::path metadata_path; auto success = z5::filesystem::metadata_detail::getMetadataPath(ds_handle, metadata_path); z5::filesystem::metadata_detail::readMetadata(metadata_path, ds_attributes); - full_depth_ = ds_attributes["shape"][2].get(); - full_height_ = ds_attributes["shape"][3].get(); - full_width_ = ds_attributes["shape"][4].get(); - tile_depth_ = ds_attributes["chunks"][2].get(); - tile_height_ = ds_attributes["chunks"][3].get(); - tile_width_ = ds_attributes["chunks"][4].get(); + std::vector level0Shape, chunkShape; + for (const auto& d : ds_attributes["shape"]) level0Shape.push_back(d.get()); + for (const auto& d : ds_attributes["chunks"]) chunkShape.push_back(d.get()); std::string dtype_str = ds_attributes["dtype"].get(); + + // Resolve axis roles from the NGFF 'axes' metadata instead of assuming a + // fixed [T,C,Z,Y,X] order. Falls back to legacy 5D TCZYX if 'axes' is absent. + Nyxus::OmeAxes axes = Nyxus::parse_ome_zarr(file_attributes, level0Shape, dtype_str); + if (axes.valid) + { + // Reject self-inconsistent metadata rather than guess: if the 'axes' + // count disagrees with the array rank, indexing the shape by axis role + // would read out of bounds. + if (axes.storageAxes.size() != level0Shape.size()) + throw std::runtime_error("OME-Zarr: 'axes' count " + std::to_string(axes.storageAxes.size()) + + " does not match array rank " + std::to_string(level0Shape.size())); + ndim_ = axes.storageAxes.size(); + ix_ = axes.storageIndexOf('X'); iy_ = axes.storageIndexOf('Y'); + iz_ = axes.storageIndexOf('Z'); ic_ = axes.storageIndexOf('C'); + it_ = axes.storageIndexOf('T'); + n_levels_ = axes.numberPyramidLevels(); + } + else + { + // No usable 'axes': map by position (X,Y last; Z,C,T before) — rank-safe. + ndim_ = level0Shape.size(); + int n = (int)ndim_; + ix_ = n - 1; iy_ = n - 2; + iz_ = (n >= 3) ? n - 3 : -1; + ic_ = (n >= 4) ? n - 4 : -1; + it_ = (n >= 5) ? n - 5 : -1; + n_levels_ = 1; + } + // X and Y must resolve to real dimensions, else the read would index OOB. + if (ix_ < 0 || iy_ < 0 || (size_t)ix_ >= level0Shape.size() || (size_t)iy_ >= level0Shape.size()) + throw std::runtime_error("OME-Zarr: cannot resolve X/Y axes from metadata"); + + full_width_ = level0Shape[ix_]; + full_height_ = level0Shape[iy_]; + full_depth_ = (iz_ >= 0) ? level0Shape[iz_] : 1; + tile_width_ = chunkShape[ix_]; + tile_height_ = chunkShape[iy_]; + tile_depth_ = (iz_ >= 0) ? chunkShape[iz_] : 1; + bits_per_sample_ = Nyxus::bits_of(Nyxus::pixel_type_from_zarr_dtype(dtype_str)); + + // dtype -> internal format code for the read-template dispatch if (dtype_str == "(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 2: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 3: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 4: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 5: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 6: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 7: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 8: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 9: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; case 10: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; default: - loadTile(pixel_row_index, pixel_col_index, pixel_layer_index); + loadTile(pixel_row_index, pixel_col_index, pixel_layer_index, indexChannel, indexTimeframe); break; } } @@ -139,7 +183,8 @@ class RawOmezarrLoader: public RawFormatLoader } template - void loadTile(size_t pixel_row_index, size_t pixel_col_index, size_t pixel_layer_index) + void loadTile(size_t pixel_row_index, size_t pixel_col_index, size_t pixel_layer_index, + size_t pixel_channel_index, size_t pixel_timeframe_index) { size_t data_height = tile_height_, data_width = tile_width_; if (pixel_row_index + data_height > full_height_) { @@ -151,11 +196,16 @@ class RawOmezarrLoader: public RawFormatLoader // Create a buffer to hold the read data std::vector buffer(data_height * data_width); - - // Create an ArrayView into the buffer (z5 3.0.1 uses ArrayView instead of xtensor) - z5::types::ShapeType shape = {1, 1, 1, data_height, data_width}; + + // Build the read window by axis ROLE (honoring the resolved 'axes' order), + // reading one Y*X plane at the requested Z/C/T. z5 3.0.1 uses ArrayView. + z5::types::ShapeType shape(ndim_, 1), offset(ndim_, 0); + shape[iy_] = data_height; offset[iy_] = pixel_row_index; + shape[ix_] = data_width; offset[ix_] = pixel_col_index; + if (iz_ >= 0) offset[iz_] = pixel_layer_index; + if (ic_ >= 0) offset[ic_] = pixel_channel_index; + if (it_ >= 0) offset[it_] = pixel_timeframe_index; auto view = z5::multiarray::makeView(buffer.data(), shape); - z5::types::ShapeType offset = {0, 0, pixel_layer_index, pixel_row_index, pixel_col_index}; // Read subarray from the cached z5 dataset z5::multiarray::readSubarray(*ds_, view, offset.begin()); @@ -197,12 +247,10 @@ class RawOmezarrLoader: public RawFormatLoader /// @return Tile depth [[nodiscard]] size_t tileDepth([[maybe_unused]] size_t level) const override { return tile_depth_; } - /// @brief Tiff bits per sample - /// @return Size of a sample in bits - [[nodiscard]] short bitsPerSample() const override { return 1; } - /// @brief Level accessor - /// @return 1 - [[nodiscard]] size_t numberPyramidLevels() const override { return 1; } + /// @brief Bits per sample (resolved from the dataset dtype) + [[nodiscard]] short bitsPerSample() const override { return bits_per_sample_; } + /// @brief Number of resolution (pyramid) levels declared in multiscales + [[nodiscard]] size_t numberPyramidLevels() const override { return n_levels_; } private: @@ -214,6 +262,12 @@ class RawOmezarrLoader: public RawFormatLoader tile_height_ = 0, ///< Tile height tile_depth_ = 0; ///< Tile depth + // Storage-dimension index of each axis role (-1 if the axis is absent). + int ix_ = 4, iy_ = 3, iz_ = 2, ic_ = 1, it_ = 0; + size_t ndim_ = 5; ///< Number of on-disk dimensions (2..5) + short bits_per_sample_ = 16; ///< Real bit depth + size_t n_levels_ = 1; ///< Pyramid level count + short data_format_ = 0; std::unique_ptr zarr_ptr_; std::string ds_name_; diff --git a/src/nyx/raw_tiff.h b/src/nyx/raw_tiff.h index cbef867f3..01e52d520 100644 --- a/src/nyx/raw_tiff.h +++ b/src/nyx/raw_tiff.h @@ -6,12 +6,12 @@ #include #undef uint64 #undef int64 -#else - #include -#endif -#include -#include -#include +#else + #include +#endif +#include +#include +#include #pragma once #include @@ -21,6 +21,7 @@ #include #include "raw_format.h" +#include "ome/ome_tiff_meta.h" // parse_ome_xml -> OmeAxes (OME-TIFF plane->IFD mapping) class RawTiffTileLoader : public RawFormatLoader { @@ -168,6 +169,8 @@ class RawTiffTileLoader : public RawFormatLoader size_t indexRowGlobalTile, size_t indexColGlobalTile, size_t indexLayerGlobalTile, + [[maybe_unused]] size_t indexChannel, // plain 2D TIFF: single channel + [[maybe_unused]] size_t indexTimeframe, // plain 2D TIFF: no time series size_t level) override { // Low level read TIFF bytes @@ -320,6 +323,23 @@ class RawTiffStripLoader : public RawFormatLoader fullDepth_ = TIFFNumberOfDirectories(tiff_); + // OME-TIFF: the IFDs are a (z,c,t) rasterization, not a plain Z-stack. + // Parse the OME-XML (IFD-0 ImageDescription) so (z,c,t) map to the right + // IFD and fullDepth reflects SizeZ, not the total page count. + { + char* desc = nullptr; + if (TIFFGetField(tiff_, TIFFTAG_IMAGEDESCRIPTION, &desc) == 1 && desc + && std::string(desc).find("(buf); + auto* fub = static_cast(buf); for (size_t r = 0; r < tileHeight_; r++) { size_t offs = r * scanline_szb; @@ -561,6 +587,9 @@ class RawTiffStripLoader : public RawFormatLoader sampleFormat_ = 0, ///< Sample format as defined by libtiff bitsPerSample_ = 0; ///< Bit Per Sample as defined by libtiff + bool is_ome_ = false; ///< true when IFD-0 carries an OME-XML block + Nyxus::OmeAxes ome_; ///< parsed OME dimensions (drives the plane->IFD map) + double minval, maxval; // low level buffer diff --git a/tests/data/ometiff/bad_corrupt.tif b/tests/data/ometiff/bad_corrupt.tif new file mode 100644 index 0000000000000000000000000000000000000000..745c85794995c3e0dcb589ba8863a340b44aefa9 GIT binary patch literal 39 scmXTP$ShU>qLkFM%)HE!)SOC%y!;Y{M1>GfH#dc}%$!s%21X`k02Ss84FCWD literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/bad_rgb.ome.tif b/tests/data/ometiff/bad_rgb.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..5f26d09af99bdcad2002bd362eb9f900b958c9ec GIT binary patch literal 1039 zcmd6lO^=g67{{U2%^r-#gPN=!$efG`FGC}t1lU9CCL~?AN!z5mw zfqo`W9zFUo{0Ke+Y*LSV6Ee(?ng9QJo_UF)56ZcsC@&O6eWIv9@=xvg40xapl_B&$ zpbdegMz*t_2f)wOf$|ODD@?rXy?c87c5OvmQkZTcC!EU>t7!7OuMI5%?chMY*X+4zP)ETb@;V}hQ&Ahq|H!ETy+WH=yTzj;`EOKfb>I7-4RWuDy zQ*p0pSmHVE7?1Ec)m`kSI>ss1$0M8Q8)x%z!ppzS**_Q1Xj)76= zM>*M2O$KhsgZ=(Ya@zk|pqR4*&FWUtq0~%SPPsgzn<}9YeSJH*TOw_FEWz3^E?lH7 z*cZAEP*1l3f~}WhlK_o6x`Pw$XxUQ41+6kl?r8!AR}F>qlO5rlW^MkSJxi9bAbFY5 zdQPjhEEzQPXKcF@b0T(T=$pMf^p|k^r$i7fx`Ks*cb*YWCq=`B%qz9s(g50RfF5p4 GsQd+d%-!Vx literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim2_yx.ome.tif b/tests/data/ometiff/dim2_yx.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..c5b706af7305b11800a3004b0f5895d9ea876939 GIT binary patch literal 957 zcmb7?*>2N76o!AN3wvpSw$QSeVJii1LA6pVPLUESMQUlJCQ{lfTxXJu6ptg@At@5C z#U+A_$kYH1;=V{{NggGj29_aUH-4pe#ZmN@q&J%fuyRA&ajPFBF{Wmgjgy zDdBqw_(%(Dh4(jU*5@=Eb2&E){z959Whu-5gLu2(pTs+JUW@YIrr0kU>66xxG1uS9-%eY3p-M~%UqFi^V)RP8M|zlBxA=grqih&j=2nkABC|WjxY5v8W^YkkOy9DkijUMmaUmK%bm_F z7XP`YA2LVTG%cfhe3UyiA>+i8J||Z!oH$E&6#8D0?eEIP)hj!UFyQ(u?lHB~X*OIo ziKH``h@N9tD*e4Z&b5kF>1&qNx3qn~Y-tzv#omRzU-o+YWU5EpOTx%yb9bti&9bfA zy2WbMX5bCDnkLT3q4WOi1-zJ=q-cp5AH|s@$5hi00hj3=okTrO*}G@;^EOl4mt(R{ zgiI_t=Tq^GcU@M=>C9zCtWFo3$Iky!JEdB+KJ=vIqpVuNj+qub^@8z;$1NUZo5!QK_H z^8x$>zVxM^!ae33PG2|!12?lXzh`fEZ=b#YI-PM00)jyaKoQbTip~~clVa}nhX|W> z*5WJ%4pvNriLYHORL|9t!VEE1L(OoVT@oLm1a{Z|AsngmPvKZYZj#g=Cca-{wI-;r zm6a6u+%|`rn_^c}?P^+nmAv}v(RwtR-|O*syru30U%4Sp!GxIr0Dr#%vtSVJ_)pFpv2xU?GdhWHDJRVJS|sS;lf!u##1*W({ju$9gufkxgu73pwPH zM?PEGhKmBWQ%DhRJQP!cmmTb+lwIs*4?gy?k23u1=Ku#eL^+44ppq)8siBr59How9 z9OnclImKz}Im1~RXyhE{xxhs(ahWSLag}Ra=LXHRaFbiy<_>qc$9*2qN*fP(#ABZD zlxIBW1uuEUYu@mdcf1#EbkNBMKJtmreBmqK=+geGu7$qn?vK`7z0{i3+@rN9VWZY! h;3D6owxsY1m5(w1t*Mh^-WCNp5N!V>>NQ1BRqAjRQ&8qS)8Az?O_9*GZw2 zt?c{0Q}(UwQ1-$neh0sTj|^9`I^%u}6Et&2ua4gLEccvy^Z6|t53GjB01=7&uSkdo zCGLy#2lh1*_lMZ$4ff#Gkv{hI!R`TA8*2ZOoOL~%^*uf}gm_+ZHbzzj{trvMDa1!4 z-rS=#r2mtp?N7PpWgI_#@A9llrDD}Zthprr^ga>#=x2ac404>T?F3HbB=ZuW5x`?;6%koJje6Az>B=Z%e=y?yvFOi!JE9r+Z^N_-sL^s=L0_EBR=L6 zKIJnG@i|}cC13G1-|#Kp@jZw6fgkyapZSGf`Hds|&L4i)!e0NExjH&a+hwztGfRKV z896#j|47_BON0H#&W=AvE0g8H70mL`|CXgZ5G%Hq{z>!quXm+K78`YCk7&7;V`o$& zrmKpuOHSFcs~L5=I2KK+BiZ!i#7;#LZ8xLVJg=G3v`(iJbDF|#Sf!TZmYn8N%xP7% zsZvceOjnZvEv_4hsGd-}%G9E39i#4S4HR)*H?*0F@vbsz*{)~WB_UO=m2v}j<4(!+ zg8Y^pTv@h4a~dMH=$2JwdOAOvQQIv$)oxqmR3e!adU8mQCXHkzY8VwGYL?^4=t5#) z+d^VEZkC6oDAy9E=d?0v&n>6K^>`wdh#6`&oo|>`q4t!m9Hm|jiGrnQisO&8^TK5BZV gk{^?0No}ES+9Kz)ZO;$qbGwB|5EL!naz3*B3$xiO=>Px# literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim4_czyx.ome.tif b/tests/data/ometiff/dim4_czyx.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..21f5b4fc2d9daf71fd5b2fe8e0f197448bb6a072 GIT binary patch literal 3976 zcmcIncXU%#9R9vv2Yc@w9(!aKLK>R3(t=bARN87m!KUeJ8*P%Bq_oAsP(Zmna zs8HmIF`TZjFeTdFQQ?#rj>M(M;r0}yrvMAv0-a*qeJZ9~Tuk>k%^opau3~yKEvmms z;XX0ERpH@rT&vPo(%7M{@-*d7-Q%yYx2mejTdSMun$$n>KSGd#RHPvtE!5arp*7kl zt#)d}9ncY-&>3CO72VJsJMZw7yZy517N^FXc&aS7=ob~hT#~2kr;*1Fv0{g zEU?0c42;28*f9NB0Gf;*Zn29rS7G_~K&PF-hsDKBR&{2hI)WC~#a4u@$gC7CZ;XDKpLKyX!g9bFB z33G8iF2IGj2=g!>3$PFu;}R^wVl2U>xC~3N442~yT#2i&99LrnuEDjq4%g!b+=!J} zg`03QR$~p;VjXV5t+)-h;||=3yKpz|!M(T->v2CGz=L=Q8?X@%;}JZHO?V8C;|V;8 zr?44YuoX{Z8=k?lcn;5FyPCTf@FHHq%h-uscm=QGHN1{D@Fw2E+t`ggcn9y|J-m+( zuooZVBYcc~*pCDF1fSwFe2y>hCBDKze2s7LExyC|_yLD-7(e1C{EQ>`1xN8Ke#7th z1ApQejz<Ew83(Zy`$FqbEB0`r*9 ziJZifIhh4Kg{QKRQ#h5=Sj5vfoy9C+DNpAaEaME$v;oj+JGqOm z@KwIX*ZBtDakGYTgd4QkrQ+~$J`31k^S3JnC`3=A2 zcl@3|@DLC4NB+d0d4#|4D1YT|{GEUBPaflOwfQL`DUvE_k}fTzrL>aP(ni`!J83T+ zq@#3_&eBD?N;l~)J*21flHSrs`bt0PF9XCN14WZTGFXPlP#Gq}WrU2BQ8HSLViL1h z#40w)kTEh=>@rRqk||CZFInP}Y{`*aQB^Be>;6w&{kxXts$zE{we*Opk$>0HV+zOD z(sWhS<7;OGC&uNv1iDTW|vvL}J zwT3x*FysyRU7FQo(G0rZ6R7n1t6f@Y$t0sg%gN4~Hf5qg1^PoSttK3U6n3s&k%y==zJyb{e& z(p0CY^dEry+z zNQcF4UtO&pD$;{d6V1_u-s+lgkvm*t&dxH&!v9Odng9*k32Bt5@6ddA*lj47q|&R# fP*m&o>v@5Ce^}YKX%W-S8C4*feRWq0H5-2c33)w0 literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim4_tzyx.ome.tif b/tests/data/ometiff/dim4_tzyx.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..44a165380c7289910c786b6465ff5b54917da4f2 GIT binary patch literal 2791 zcmcImXLJ)s7#xo&sRWV$2_!_hG(u1_SO~=u#l~Q;vB5Ifrui(L>;vf}R9xr~dhfmW z-g{3%NN=Qf(tGbcD_MC_@{c@Q@7=rE)y%iEyEnI&mDL890cZpy)kBg%(N3Dl-D2uH%cN#6BsJQP2$>%g3~mC(=4H8^EfUMoEAy-WAzUU+%k?&3*0(E zYZmpd5`E{zbun4^6Zga`ta>~i)vFi+)#9JTUlNF9)I)tVKtnON%W(y+6t%7vGrk6m z(FE7xI$V#YXolvv0XL!rTH+?$j8?b>I@}5gx8Zi&fje;*TH|iC!98dTJq$3y1T#|5 z4(*W&3p&7xje&48TAP!e9(R31pPQ1veCUP=<1-7>Z%=!Uqk01TY*ygb+pq6{tiNsxbm1F$$wG z24gV}<1qpEVIn4BGNxcEreQi}U?yf^Hs)Y1=3zb-U?CP^F_vH{mSH(oU?o;zHP&D) z)?qz1U?VnRGqzwWwqZMVU?+BAH}+sJ_F+E`;2;j+Fpl6Tj^Q{?;3Q7rG|u2Gy09zL znZa(%WOw#p7JIT6vzf!*%w-AucljRQ=Lh_dAMs;;!cX}bKj#b6;|*3u5{c5c1ZZ1w2PW{+84>Pb!2ms(0wOD!E!WOqj)N)IY>*dMe@ z312_D6jO@DU@@2_M_QIoE>m>T!ZBnM>)l)SDeLc3(GK}B{c-j^;mwx+MxF14>gPP>#M21_rO zHBIrxMvG&cDLP=j>5RzTyAuv8RK+Enp1q|WyoP!mz61| eCA4{7SyR&e5iJ}w6Xn)SM1a_&#a%of4*Uh-TChX_ literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim5.ome.tif b/tests/data/ometiff/dim5.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..07dc14592af1100448c1d2dab93f1e3dcd597087 GIT binary patch literal 7120 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 literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim5_cztyx.ome.tif b/tests/data/ometiff/dim5_cztyx.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..3abb1be40e3781977c8b425c69e5339dc55a324d GIT binary patch literal 7120 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 literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim5_tzcyx.ome.tif b/tests/data/ometiff/dim5_tzcyx.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..2bce4e948148e01d4b42f9c91250b97f9c3f9f33 GIT binary patch literal 7120 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 literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim5_zctyx.ome.tif b/tests/data/ometiff/dim5_zctyx.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..636f669f18eb248aa2199420741c0a1e7c12c36c GIT binary patch literal 7120 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 literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim5_ztcyx.ome.tif b/tests/data/ometiff/dim5_ztcyx.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..d87038fecebebaf000213067e60767efb7cc5837 GIT binary patch literal 7120 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 literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/gen_ome_tiff.py b/tests/data/ometiff/gen_ome_tiff.py new file mode 100644 index 000000000..6ed768c66 --- /dev/null +++ b/tests/data/ometiff/gen_ome_tiff.py @@ -0,0 +1,98 @@ +"""Generate OME-TIFF fixtures whose every voxel encodes its own (x,y,z,c,t) +coordinate, so a reader that maps (z,c,t) to the wrong IFD (page) reads a +provably wrong value. + + value(x,y,z,c,t) = 1 + ((((t*C + c)*Z + z)*Y + y)*X + x) # C=3,Z=4,Y=6,X=8 + +Stores (same encoding throughout; absent axes pinned to index 0): + + dim5.ome.tif axes TCZYX -> DimensionOrder XYZCT (default: Z fastest) + dim5_ctzyx.ome.tif axes CTZYX -> DimensionOrder XYZTC (non-default: T before C) + -> proves the plane->IFD map honors DimensionOrder + dim3_zyx.ome.tif axes ZYX 3D + dim2_yx.ome.tif axes YX 2D + dim3_plain.tif plain multi-page TIFF (NO OME-XML) -> legacy page=Z fallback + +tifffile writes OME-TIFF as multi-page (strip) with one IFD per (z,c,t) plane in +DimensionOrder raster order, so the loader's ifdForPlane() must reproduce that. + +Run with the nyxus_build env python (tifffile): + C:\\Users\\dvladi\\miniforge3\\envs\\nyxus_build\\python.exe gen_ome_tiff.py +""" +import os +import numpy as np +import tifffile + +T, C, Z, Y, X = 2, 3, 4, 6, 8 +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def encoded_tczyx(): + t, c, z, y, x = np.meshgrid( + np.arange(T), np.arange(C), np.arange(Z), np.arange(Y), np.arange(X), indexing="ij") + return (1 + ((((t * C + c) * Z + z) * Y + y) * X + x)).astype("uint16") + + +def _data_for(order): + base = encoded_tczyx() # [t,c,z,y,x] + sel = tuple(slice(None) if a in order else 0 for a in "TCZYX") + sub = base[sel] + remaining = [a for a in "TCZYX" if a in order] + perm = tuple(remaining.index(a) for a in order) + return np.transpose(sub, perm).copy() + + +def write_ome(name, order): + path = os.path.join(HERE, name) + data = _data_for(order) + tifffile.imwrite(path, data, photometric="minisblack", metadata={"axes": order}, ome=True) + with tifffile.TiffFile(path) as tf: + import re + dimord = re.search(r'DimensionOrder="([A-Z]+)"', tf.ome_metadata).group(1) + print("wrote %-20s axes=%-6s DimensionOrder=%s IFDs=%d tiled=%s" + % (name, order, dimord, len(tf.pages), tf.pages[0].is_tiled)) + + +def write_plain(name, order): + """Plain multi-page TIFF, no OME-XML (tests the non-OME fallback).""" + path = os.path.join(HERE, name) + data = _data_for(order) # e.g. ZYX -> a plain z-stack + tifffile.imwrite(path, data, photometric="minisblack") + with tifffile.TiffFile(path) as tf: + print("wrote %-20s axes=%-6s (plain, no OME) IFDs=%d" % (name, order, len(tf.pages))) + + +def write_bad_rgb(name): + """RGB OME-TIFF -- nyxus is grayscale-only, so the loader must reject it.""" + path = os.path.join(HERE, name) + data = np.zeros((Y, X, 3), "uint8") + tifffile.imwrite(path, data, photometric="rgb", metadata={"axes": "YXS"}, ome=True) + print("wrote %-20s (RGB -> not grayscale)" % name) + + +def write_bad_corrupt(name): + """Not a TIFF at all -- TIFFOpen must fail cleanly.""" + path = os.path.join(HERE, name) + with open(path, "wb") as f: + f.write(b"this is definitely not a TIFF file\n\x00\x01\x02\x03") + print("wrote %-20s (corrupt -> open fails)" % name) + + +def main(): + # All 6 orderings of {T,C,Z} before Y,X -> the 6 legal OME DimensionOrder values. + for o in ["TCZYX", "TZCYX", "CTZYX", "CZTYX", "ZTCYX", "ZCTYX"]: + write_ome("dim5.ome.tif" if o == "TCZYX" else "dim5_%s.ome.tif" % o.lower(), o) + # 4D (rank-4 coverage): one time-only, one channel-only. + write_ome("dim4_tzyx.ome.tif", "TZYX") + write_ome("dim4_czyx.ome.tif", "CZYX") + # lower rank + non-OME fallback + write_ome("dim3_zyx.ome.tif", "ZYX") + write_ome("dim2_yx.ome.tif", "YX") + write_plain("dim3_plain.tif", "ZYX") + # --- illegal / adversarial: must be rejected cleanly, not crash --- + write_bad_rgb("bad_rgb.ome.tif") + write_bad_corrupt("bad_corrupt.tif") + + +if __name__ == "__main__": + main() diff --git a/tests/data/omezarr/README.md b/tests/data/omezarr/README.md index c3bcac8de..fe0ae39e2 100644 --- a/tests/data/omezarr/README.md +++ b/tests/data/omezarr/README.md @@ -17,10 +17,30 @@ and `chunks[2..4]` as the tile sizes. |------------------|-----------:|--------|-----------|-----------|------------------------| | `test.ome.zarr` | 512x512 | uint16 | 1024x1024 | 1x1 | `(row + col) % 65536` | | `multi.ome.zarr` | 1500x1200 | uint16 | 1024x1024 | 2x2* | `(row*7 + col*3) % 65536` | +| `dim5.ome.zarr` | 6x8 | uint16 | 2x3x1x6x8 | 1x1 | 5D coordinate encoding (below) | \* `multi.ome.zarr` has partial edge tiles (1500 = 1024 + 476, 1200 = 1024 + 176), so it also exercises the loaders' partial-tile clipping path. +### `dim5.ome.zarr` — 5D channel/timeframe addressability + +A genuinely 5D store, shape `(T=2, C=3, Z=4, Y=6, X=8)`, chunked `(T,C,1,Y,X)` — +one chunk per z-slice (4 chunk files) — and written **uncompressed** (so it reads +with header-only z5). Z gets its own chunk because the loader maps the layer index +as `layer*tileDepth` (`tileDepth = chunks[2]`); C and T are addressed directly by +the read offset, so they don't need separate chunks. Every voxel encodes its own +coordinate: + +``` +value(x,y,z,c,t) = 1 + ((((t*C + c)*Z + z)*Y + y)*X + x) # C=3, Z=4, Y=6, X=8 +``` + +`test_omezarr_5d_channel_time_addressing` / `test_raw_omezarr_5d_channel_time_addressing` +read plane `(z,c,t)` via `loadTileFromFile(..., layer=z, channel=c, timeframe=t, ...)` +and assert every value matches the encoding — so a loader that ignored C/T (offset +pinned to `{0,0,...}`) returns the wrong plane and fails. Generated by `gen_dim5.py` +(zarr 3.x). Regenerate: `python gen_dim5.py`. + Deterministic checksums asserted by the tests: - `test.ome.zarr` sum of all pixels = `133955584` diff --git a/tests/data/omezarr/bad_axes_count.ome.zarr/.zattrs b/tests/data/omezarr/bad_axes_count.ome.zarr/.zattrs new file mode 100644 index 000000000..0a8629e58 --- /dev/null +++ b/tests/data/omezarr/bad_axes_count.ome.zarr/.zattrs @@ -0,0 +1,46 @@ +{ + "multiscales": [ + { + "version": "0.4", + "axes": [ + { + "name": "t", + "type": "time" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/bad_axes_count.ome.zarr/.zgroup b/tests/data/omezarr/bad_axes_count.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/bad_axes_count.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/bad_axes_count.ome.zarr/0/.zarray b/tests/data/omezarr/bad_axes_count.ome.zarr/0/.zarray new file mode 100644 index 000000000..9226eae5a --- /dev/null +++ b/tests/data/omezarr/bad_axes_count.ome.zarr/0/.zarray @@ -0,0 +1,19 @@ +{ + "shape": [ + 4, + 6, + 8 + ], + "chunks": [ + 1, + 6, + 8 + ], + "dtype": "006+#P-%#U7KwI||G%!gKu_PmP-tXqA~H2Iw-8%Oq*m58wlX_=2S+El W!ddB}QoFjjdw6006+#P-%#U7KwI||G%!gKu_PmP-tXqA~H2Iw-8%Oq*m58wlX_=2S+El W!ddB}QoFjjdw6006-Il7>n{G_**xi~Ro;-35C328Kc-V-u07nYo47QX;jowy~Aj**iEo Y$ra8@7nR!8&E3P(%iG6SqxIAI15%z1nE(I) literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim3_zyx.ome.zarr/0/2.0.0 b/tests/data/omezarr/dim3_zyx.ome.zarr/0/2.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..fa41e215f5a6b8ffad768a34fb5432b2e61d6597 GIT binary patch literal 96 zcmV~$2LeI>006-Is)kBKG_**xi#)lzKu_PmP-tXqA~H2Iw-8%Oq*m58wlX_=2S+El W!ddB}QoFjjdw6>V7P Y( TyFK>WXTJmfaL^%#9dXoq{!vN! literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim4_czyx.ome.zarr/0/0.1.0.0 b/tests/data/omezarr/dim4_czyx.ome.zarr/0/0.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..eb67a2e44edbace477d7a4c3371dbadf74a27c6f GIT binary patch literal 288 zcmV~$2O38j=#P6d@zQB4gWsHKjN)YHHxKJ$e}nrNnlR@!K%gHF2W=9m*s zIpdrQF1g|#*W7T+9rrwd`pCxy7-*2eJ~6~lpBiSk5k?wiv@t$2);QyR?h6x4G|6OB zO!cK{_W0Frez(^?`~Bg7gAO_Dh@*}S0 S@{il@xa*$#9(d@HfBg@a<3t(& literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim4_czyx.ome.zarr/0/0.2.0.0 b/tests/data/omezarr/dim4_czyx.ome.zarr/0/0.2.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..05ccef7af9f94a72b42ca3f3aa190282777def80 GIT binary patch literal 288 zcmV~$140A<6h+Z@e{RwivTfVkLbh!;xwe>XyK@@Vq-M3KRc&flhdR}zZuO{Fed^bM z1~sH%jc8P38rOs-HKl3IXjXHY*Mb(cq-CvWRcmr;T^riembSH{UF~UKfdmmu2%&@# zP6Uxe5lsxS#1T&di6oIs3aO-#P6nA|kxdS{X?_+;Yb~4?Ob3GcUaI#ycN;^2Ikl{PKqkGjlOlb2E4IFi-O` PZ*!QB`I?{kTY&uoPM<$I literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim4_czyx.ome.zarr/0/0.3.0.0 b/tests/data/omezarr/dim4_czyx.ome.zarr/0/0.3.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..c2d16b33836237ae18b5579f30c4fad1a1990f78 GIT binary patch literal 288 zcmV~$g+T%U00hAQ-@wMeL@}{DSHLdpZUqC(6->nLz;4AH&AVl+U=?dv#|GTkga=#L z#twF|hkYF25Jx!12~Kf_b6nsOUR>cCH@L+e?(u*}JmDEHc*Psu@c|z`@r7^v;1_?e zvXWJ-W({ju$9gufkxgu-jV)|t8{65zPIj@IJ?v#4`#Hct4snXs- z7?Vsf%?xo8%reJ33oNq4GApdI#yT4$Ns%VQCR=Q?!!CR5bHE`-9-H;VoTr|7Zr%$E x7A<+{m1Qedz4pdiYudrMNvseDQVEwr@A*lA3^#VV2~k(8DW$VV}u!J zf=Q;BCc+F+V#Jwcj(HYXWQhdJtgy-&>uj*e7TfHQWS2emIpB~KX^uQLdrMNvseDQVEwC%HF3A3^#VV2~k(8DW$VV}u!J zf=Q;BCc+F+V#Jwcj(HYXWQhdJtgy-&>uj*e7TfHQWS2emIpB~KX^uQL(06@|EUGM)RZD|Qj?V&}lkQ7BFA*G~2Th8etNIwG%GQ=<=j1ppuFyl-x z$rRH>m?27xIJ3+#&jO1qkzkn>R#{`64K~?gn;nwuvd2CL9Fii<5yxgcG3%*k=FEHU zg$0XVdgZkxZ!CLj#i}*$tb6Z+4Ih27>9Z|geD%$D+jji$(=WgM@z+1Q{!5?-zj#WL literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5.ome.zarr/.zattrs b/tests/data/omezarr/dim5.ome.zarr/.zattrs new file mode 100644 index 000000000..8f363a6b0 --- /dev/null +++ b/tests/data/omezarr/dim5.ome.zarr/.zattrs @@ -0,0 +1,46 @@ +{ + "multiscales": [ + { + "version": "0.4", + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ], + "axes": [ + { + "name": "t", + "type": "time" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5.ome.zarr/.zgroup b/tests/data/omezarr/dim5.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim5.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5.ome.zarr/0/.zarray b/tests/data/omezarr/dim5.ome.zarr/0/.zarray new file mode 100644 index 000000000..7f0e4af6a --- /dev/null +++ b/tests/data/omezarr/dim5.ome.zarr/0/.zarray @@ -0,0 +1,23 @@ +{ + "shape": [ + 2, + 3, + 4, + 6, + 8 + ], + "chunks": [ + 2, + 3, + 1, + 6, + 8 + ], + "dtype": "{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_t& zMhQw$hH_M(5>=>14Qf$`dNiOBO=w07TG57fbf6Pm=td8E(T9EvU=TwX#t23+hH*?_ z5>uGQ3}!Khc`RTNmw1_1c$L?9oi})sw|JX(c$fEhpAYzukNB8R_>|9>&*yx>0>0!c zzUCXgWg*}3JwNaxKk+la@GHOZJAd#efAKf}@Gt*yi7mBdw%k_ON?T>CZH?txo~^ZY zw%#__M%!eYZHsNSZMNNZ*iPGJyKRr{wSBhV4%k6EWQXmD9kpY2+)mg@J7uTsjGeV} zcHS=7MWalTgB;}~XSv8#ZgQ80Jmn>C`N&s(@>hTY6{KK=C{$qzSA-%JrD(+{R&k0~ zf)bUaWThxoX-ZdyGL@xl<)}!-Dp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKPf|rK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5.ome.zarr/0/0.0.1.0.0 b/tests/data/omezarr/dim5.ome.zarr/0/0.0.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..678ed89e2b57253198aa620a5b4f198239f69bdc GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?wBaw>(Sc5Mp&LEuMIZVxfI$r5ABF*vM$s9-5e#GygE^9;IGQ00Wf;R5 z!7+?v6r&l#v5aLL<2jBAOk@(1nZmO?$Md|vi@e0kyuz!z#_PPno4m!_yu-V^$NPN1 zhkV4xe8Q)E#^-#&mwd(7e8abV$M^iekNm{X{KBvN#_#;WpZvw&{KL66&*s|#TWE`H zu`RKsw#=5>3R`KbY_+YiwYJXI+XmZcn{2afv8}evw%ZQdX}fHc2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(37+B^p5p~x;uT)w4c_7%-s1y4;uAjO3%=qT zzT*de;un775B}mGgdnLjuHZ_p;%ctpTCU@IZs104;%08)R&L{V?%+-?aFM&Xn|rvI z`?#M6c#wy9m`8Y&$9SA4c#@}hnrC>H=XjnMc#)U5#AROQ6<*~vUgr(oW~iWh>q%*j_ZU@>Xc6FjLzzu&Z~ntnv*%3i@BPcxtoW1nwNQ-kNKLP`CEVmT95@> uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFAnOcPU@^K>Z)$)t{&>CUh1tr>Z^Y0uK^mUK^m+f8meI$t`QokQ5vl=8mn;{uL+u{ zNt&!FnyP7dmnyY!5uM4mci?A3=uoTO%94oLAtFRhtuommE9viR`o3I&M zuoc^|9XqfSyRaL3uowHV9|v#{hj182a1_UI94BxRr*Il)a2Drq9v5&Cmv9+Za23~Z z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=#pYR!9@D<@E89eEMOsvSj-ZZvW(@dU?r9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxRJWSa literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_ctzyx.ome.zarr/.zattrs b/tests/data/omezarr/dim5_ctzyx.ome.zarr/.zattrs new file mode 100644 index 000000000..7a2798426 --- /dev/null +++ b/tests/data/omezarr/dim5_ctzyx.ome.zarr/.zattrs @@ -0,0 +1,46 @@ +{ + "multiscales": [ + { + "version": "0.4", + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ], + "axes": [ + { + "name": "c", + "type": "channel" + }, + { + "name": "t", + "type": "time" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_ctzyx.ome.zarr/.zgroup b/tests/data/omezarr/dim5_ctzyx.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim5_ctzyx.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_ctzyx.ome.zarr/0/.zarray b/tests/data/omezarr/dim5_ctzyx.ome.zarr/0/.zarray new file mode 100644 index 000000000..e9172f8f4 --- /dev/null +++ b/tests/data/omezarr/dim5_ctzyx.ome.zarr/0/.zarray @@ -0,0 +1,23 @@ +{ + "shape": [ + 3, + 2, + 4, + 6, + 8 + ], + "chunks": [ + 3, + 2, + 1, + 6, + 8 + ], + "dtype": "{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_=t zsV%eRw!&80DqC%9EZ6dEt*x{5w!t>qCfjUVY^!av?Y6^q+AiB|du*@mv;B6!4%#6* zY)9;<9kb(h!cN*LJ8fs|tevy-cEK)M5sFcQQk0<_6{tiNs!@Yl)S(^?Xhaj5(SlaA zp&cFQL>IczgI@HZ9|IV~5QZ^=QH)_66PUylrZIzA%wZl2SOjH~9ONh`Im<<^a+A9} zjQrl&LIbD@T`j znOAs~*La;bc$2qyn|FAZ_jsQV_>hnIm{0hW&zR5We8B>~B-|;;^@FPF* zGr#aFzwtYN@F#!qH~;W2|FKBLDp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKLPoPK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_ctzyx.ome.zarr/0/0.0.1.0.0 b/tests/data/omezarr/dim5_ctzyx.ome.zarr/0/0.0.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..fab096529cb6629e2f6aa5d0022264a56f9f9e1e GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?v|+Bzv-!5b7TO|PY)fpZEwkme!dBWUTWxD>t*x{5w!t>qCfjUVY^!av z?Y6^q+AiB|du*@mv;B6!4%#6*Y)9;<9kb(h!cN*LJ8fs|FWS+8PIRFgJ?KRr`Z0h( z4B;P!0g^`18Nd+?WDtWnlA}19Aq-_0!x_ObjARs}8N;!RWgO!@iy=9F7NR^AMha`@iCw9DWCB< zU+^Vg@ipJ@E#L7yKky?z@iV{hE5GqOfAA-N@i+hQuiDk2PIakUJ?d4T`Zb_I4e6hT z1*ec2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(@Fs8ZHt+B*@9{nz;0Px;!v(HzgF8In2`_lV2fpxwKLQYlAOs@> zp$J1bA`povL?Z^Vh(kOQkccECBL%5QLpm~$i7aFz2f4^YJ_@u@i?movv{cKqTr0Fv ztF&5cv{vi1UK_Mgo3vS5v{l=*T|2Z>yR=(-v{(DIUk7whhjds+bX3Q5Tqkr=r*v9p zbXMndULW8g9^o;b;3=NrIbPr;Ug0&~;4R+aJwD(gKH)RI;48l2JAU9Ne&IL%;4l6` z2$D+U3a;cTuI3u9 uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFBfnTmv9+Za23~Z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=# zpYR!9@D<@E89ebWlfiQfGBhS9Mc&^-xdsQg8K9U-eUe4bVUh(qIkI zPz}>?jnGJq(rAs*SdG(oP0&P5(qv81R87-#&CpEE(rnGqT+P#bEnp#wSj-ZZvW(@d zU?r;hy6H! zgE)l4ID(@%hT}MalQ@ObID@k|hx2eSM{_b~b1_$QGk5baPxCTw^D$rZGk*)PKnt>9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxJ4l++ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_cztyx.ome.zarr/.zattrs b/tests/data/omezarr/dim5_cztyx.ome.zarr/.zattrs new file mode 100644 index 000000000..9bedfbd37 --- /dev/null +++ b/tests/data/omezarr/dim5_cztyx.ome.zarr/.zattrs @@ -0,0 +1,46 @@ +{ + "multiscales": [ + { + "version": "0.4", + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ], + "axes": [ + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "t", + "type": "time" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_cztyx.ome.zarr/.zgroup b/tests/data/omezarr/dim5_cztyx.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim5_cztyx.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_cztyx.ome.zarr/0/.zarray b/tests/data/omezarr/dim5_cztyx.ome.zarr/0/.zarray new file mode 100644 index 000000000..e1f0f5a5d --- /dev/null +++ b/tests/data/omezarr/dim5_cztyx.ome.zarr/0/.zarray @@ -0,0 +1,23 @@ +{ + "shape": [ + 3, + 4, + 2, + 6, + 8 + ], + "chunks": [ + 3, + 1, + 2, + 6, + 8 + ], + "dtype": "{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_=t zsV%eRw!&80DqC%9EZ6dEt*x{5w!t>qCfjUVY^!av?Y6^q+AiB|du*@mv;B6!4%#6* zY)9;<9kb(h!cN*LJ8fs|tevy-cEK)M5sFcQQk0<_6{tiNs!@Yl)S(^?Xhaj5(SlaA zp&cFQL>IczgI@HZ9|IV~5QZ^=QH)_66PUylrZIzA%wZl2SOjH~9ONh`Im<<^a+A9} zjQrl&LIbD@T`j znOAs~*La;bc$2qyn|FAZ_jsQV_>hnIm{0hW&zR5We8B>~B-|;;^@FPF* zGr#aFzwtYN@F#!qH~;W2|FKBLDp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKLPoPK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_cztyx.ome.zarr/0/0.1.0.0.0 b/tests/data/omezarr/dim5_cztyx.ome.zarr/0/0.1.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..fab096529cb6629e2f6aa5d0022264a56f9f9e1e GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?v|+Bzv-!5b7TO|PY)fpZEwkme!dBWUTWxD>t*x{5w!t>qCfjUVY^!av z?Y6^q+AiB|du*@mv;B6!4%#6*Y)9;<9kb(h!cN*LJ8fs|FWS+8PIRFgJ?KRr`Z0h( z4B;P!0g^`18Nd+?WDtWnlA}19Aq-_0!x_ObjARs}8N;!RWgO!@iy=9F7NR^AMha`@iCw9DWCB< zU+^Vg@ipJ@E#L7yKky?z@iV{hE5GqOfAA-N@i+hQuiDk2PIakUJ?d4T`Zb_I4e6hT z1*ec2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(@Fs8ZHt+B*@9{nz;0Px;!v(HzgF8In2`_lV2fpxwKLQYlAOs@> zp$J1bA`povL?Z^Vh(kOQkccECBL%5QLpm~$i7aFz2f4^YJ_@u@i?movv{cKqTr0Fv ztF&5cv{vi1UK_Mgo3vS5v{l=*T|2Z>yR=(-v{(DIUk7whhjds+bX3Q5Tqkr=r*v9p zbXMndULW8g9^o;b;3=NrIbPr;Ug0&~;4R+aJwD(gKH)RI;48l2JAU9Ne&IL%;4l6` z2$D+U3a;cTuI3u9 uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFBfnTmv9+Za23~Z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=# zpYR!9@D<@E89ebWlfiQfGBhS9Mc&^-xdsQg8K9U-eUe4bVUh(qIkI zPz}>?jnGJq(rAs*SdG(oP0&P5(qv81R87-#&CpEE(rnGqT+P#bEnp#wSj-ZZvW(@d zU?r;hy6H! zgE)l4ID(@%hT}MalQ@ObID@k|hx2eSM{_b~b1_$QGk5baPxCTw^D$rZGk*)PKnt>9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxJ4l++ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_noaxes.ome.zarr/.zattrs b/tests/data/omezarr/dim5_noaxes.ome.zarr/.zattrs new file mode 100644 index 000000000..c1ebb88a3 --- /dev/null +++ b/tests/data/omezarr/dim5_noaxes.ome.zarr/.zattrs @@ -0,0 +1,24 @@ +{ + "multiscales": [ + { + "version": "0.4", + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_noaxes.ome.zarr/.zgroup b/tests/data/omezarr/dim5_noaxes.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim5_noaxes.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_noaxes.ome.zarr/0/.zarray b/tests/data/omezarr/dim5_noaxes.ome.zarr/0/.zarray new file mode 100644 index 000000000..7f0e4af6a --- /dev/null +++ b/tests/data/omezarr/dim5_noaxes.ome.zarr/0/.zarray @@ -0,0 +1,23 @@ +{ + "shape": [ + 2, + 3, + 4, + 6, + 8 + ], + "chunks": [ + 2, + 3, + 1, + 6, + 8 + ], + "dtype": "{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_t& zMhQw$hH_M(5>=>14Qf$`dNiOBO=w07TG57fbf6Pm=td8E(T9EvU=TwX#t23+hH*?_ z5>uGQ3}!Khc`RTNmw1_1c$L?9oi})sw|JX(c$fEhpAYzukNB8R_>|9>&*yx>0>0!c zzUCXgWg*}3JwNaxKk+la@GHOZJAd#efAKf}@Gt*yi7mBdw%k_ON?T>CZH?txo~^ZY zw%#__M%!eYZHsNSZMNNZ*iPGJyKRr{wSBhV4%k6EWQXmD9kpY2+)mg@J7uTsjGeV} zcHS=7MWalTgB;}~XSv8#ZgQ80Jmn>C`N&s(@>hTY6{KK=C{$qzSA-%JrD(+{R&k0~ zf)bUaWThxoX-ZdyGL@xl<)}!-Dp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKPf|rK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_noaxes.ome.zarr/0/0.0.1.0.0 b/tests/data/omezarr/dim5_noaxes.ome.zarr/0/0.0.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..678ed89e2b57253198aa620a5b4f198239f69bdc GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?wBaw>(Sc5Mp&LEuMIZVxfI$r5ABF*vM$s9-5e#GygE^9;IGQ00Wf;R5 z!7+?v6r&l#v5aLL<2jBAOk@(1nZmO?$Md|vi@e0kyuz!z#_PPno4m!_yu-V^$NPN1 zhkV4xe8Q)E#^-#&mwd(7e8abV$M^iekNm{X{KBvN#_#;WpZvw&{KL66&*s|#TWE`H zu`RKsw#=5>3R`KbY_+YiwYJXI+XmZcn{2afv8}evw%ZQdX}fHc2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(37+B^p5p~x;uT)w4c_7%-s1y4;uAjO3%=qT zzT*de;un775B}mGgdnLjuHZ_p;%ctpTCU@IZs104;%08)R&L{V?%+-?aFM&Xn|rvI z`?#M6c#wy9m`8Y&$9SA4c#@}hnrC>H=XjnMc#)U5#AROQ6<*~vUgr(oW~iWh>q%*j_ZU@>Xc6FjLzzu&Z~ntnv*%3i@BPcxtoW1nwNQ-kNKLP`CEVmT95@> uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFAnOcPU@^K>Z)$)t{&>CUh1tr>Z^Y0uK^mUK^m+f8meI$t`QokQ5vl=8mn;{uL+u{ zNt&!FnyP7dmnyY!5uM4mci?A3=uoTO%94oLAtFRhtuommE9viR`o3I&M zuoc^|9XqfSyRaL3uowHV9|v#{hj182a1_UI94BxRr*Il)a2Drq9v5&Cmv9+Za23~Z z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=#pYR!9@D<@E89eEMOsvSj-ZZvW(@dU?r9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxRJWSa literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_tzcyx.ome.zarr/.zattrs b/tests/data/omezarr/dim5_tzcyx.ome.zarr/.zattrs new file mode 100644 index 000000000..9fd33a7fb --- /dev/null +++ b/tests/data/omezarr/dim5_tzcyx.ome.zarr/.zattrs @@ -0,0 +1,46 @@ +{ + "multiscales": [ + { + "version": "0.4", + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ], + "axes": [ + { + "name": "t", + "type": "time" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_tzcyx.ome.zarr/.zgroup b/tests/data/omezarr/dim5_tzcyx.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim5_tzcyx.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_tzcyx.ome.zarr/0/.zarray b/tests/data/omezarr/dim5_tzcyx.ome.zarr/0/.zarray new file mode 100644 index 000000000..c92f859ef --- /dev/null +++ b/tests/data/omezarr/dim5_tzcyx.ome.zarr/0/.zarray @@ -0,0 +1,23 @@ +{ + "shape": [ + 2, + 4, + 3, + 6, + 8 + ], + "chunks": [ + 2, + 1, + 3, + 6, + 8 + ], + "dtype": "{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_t& zMhQw$hH_M(5>=>14Qf$`dNiOBO=w07TG57fbf6Pm=td8E(T9EvU=TwX#t23+hH*?_ z5>uGQ3}!Khc`RTNmw1_1c$L?9oi})sw|JX(c$fEhpAYzukNB8R_>|9>&*yx>0>0!c zzUCXgWg*}3JwNaxKk+la@GHOZJAd#efAKf}@Gt*yi7mBdw%k_ON?T>CZH?txo~^ZY zw%#__M%!eYZHsNSZMNNZ*iPGJyKRr{wSBhV4%k6EWQXmD9kpY2+)mg@J7uTsjGeV} zcHS=7MWalTgB;}~XSv8#ZgQ80Jmn>C`N&s(@>hTY6{KK=C{$qzSA-%JrD(+{R&k0~ zf)bUaWThxoX-ZdyGL@xl<)}!-Dp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKPf|rK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_tzcyx.ome.zarr/0/0.1.0.0.0 b/tests/data/omezarr/dim5_tzcyx.ome.zarr/0/0.1.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..678ed89e2b57253198aa620a5b4f198239f69bdc GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?wBaw>(Sc5Mp&LEuMIZVxfI$r5ABF*vM$s9-5e#GygE^9;IGQ00Wf;R5 z!7+?v6r&l#v5aLL<2jBAOk@(1nZmO?$Md|vi@e0kyuz!z#_PPno4m!_yu-V^$NPN1 zhkV4xe8Q)E#^-#&mwd(7e8abV$M^iekNm{X{KBvN#_#;WpZvw&{KL66&*s|#TWE`H zu`RKsw#=5>3R`KbY_+YiwYJXI+XmZcn{2afv8}evw%ZQdX}fHc2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(37+B^p5p~x;uT)w4c_7%-s1y4;uAjO3%=qT zzT*de;un775B}mGgdnLjuHZ_p;%ctpTCU@IZs104;%08)R&L{V?%+-?aFM&Xn|rvI z`?#M6c#wy9m`8Y&$9SA4c#@}hnrC>H=XjnMc#)U5#AROQ6<*~vUgr(oW~iWh>q%*j_ZU@>Xc6FjLzzu&Z~ntnv*%3i@BPcxtoW1nwNQ-kNKLP`CEVmT95@> uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFAnOcPU@^K>Z)$)t{&>CUh1tr>Z^Y0uK^mUK^m+f8meI$t`QokQ5vl=8mn;{uL+u{ zNt&!FnyP7dmnyY!5uM4mci?A3=uoTO%94oLAtFRhtuommE9viR`o3I&M zuoc^|9XqfSyRaL3uowHV9|v#{hj182a1_UI94BxRr*Il)a2Drq9v5&Cmv9+Za23~Z z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=#pYR!9@D<@E89eEMOsvSj-ZZvW(@dU?r9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxRJWSa literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_zctyx.ome.zarr/.zattrs b/tests/data/omezarr/dim5_zctyx.ome.zarr/.zattrs new file mode 100644 index 000000000..80d186378 --- /dev/null +++ b/tests/data/omezarr/dim5_zctyx.ome.zarr/.zattrs @@ -0,0 +1,46 @@ +{ + "multiscales": [ + { + "version": "0.4", + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ], + "axes": [ + { + "name": "z", + "type": "space" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "t", + "type": "time" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_zctyx.ome.zarr/.zgroup b/tests/data/omezarr/dim5_zctyx.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim5_zctyx.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_zctyx.ome.zarr/0/.zarray b/tests/data/omezarr/dim5_zctyx.ome.zarr/0/.zarray new file mode 100644 index 000000000..a9fa0bc1e --- /dev/null +++ b/tests/data/omezarr/dim5_zctyx.ome.zarr/0/.zarray @@ -0,0 +1,23 @@ +{ + "shape": [ + 4, + 3, + 2, + 6, + 8 + ], + "chunks": [ + 1, + 3, + 2, + 6, + 8 + ], + "dtype": "{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_=t zsV%eRw!&80DqC%9EZ6dEt*x{5w!t>qCfjUVY^!av?Y6^q+AiB|du*@mv;B6!4%#6* zY)9;<9kb(h!cN*LJ8fs|tevy-cEK)M5sFcQQk0<_6{tiNs!@Yl)S(^?Xhaj5(SlaA zp&cFQL>IczgI@HZ9|IV~5QZ^=QH)_66PUylrZIzA%wZl2SOjH~9ONh`Im<<^a+A9} zjQrl&LIbD@T`j znOAs~*La;bc$2qyn|FAZ_jsQV_>hnIm{0hW&zR5We8B>~B-|;;^@FPF* zGr#aFzwtYN@F#!qH~;W2|FKBLDp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKLPoPK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_zctyx.ome.zarr/0/1.0.0.0.0 b/tests/data/omezarr/dim5_zctyx.ome.zarr/0/1.0.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..fab096529cb6629e2f6aa5d0022264a56f9f9e1e GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?v|+Bzv-!5b7TO|PY)fpZEwkme!dBWUTWxD>t*x{5w!t>qCfjUVY^!av z?Y6^q+AiB|du*@mv;B6!4%#6*Y)9;<9kb(h!cN*LJ8fs|FWS+8PIRFgJ?KRr`Z0h( z4B;P!0g^`18Nd+?WDtWnlA}19Aq-_0!x_ObjARs}8N;!RWgO!@iy=9F7NR^AMha`@iCw9DWCB< zU+^Vg@ipJ@E#L7yKky?z@iV{hE5GqOfAA-N@i+hQuiDk2PIakUJ?d4T`Zb_I4e6hT z1*ec2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(@Fs8ZHt+B*@9{nz;0Px;!v(HzgF8In2`_lV2fpxwKLQYlAOs@> zp$J1bA`povL?Z^Vh(kOQkccECBL%5QLpm~$i7aFz2f4^YJ_@u@i?movv{cKqTr0Fv ztF&5cv{vi1UK_Mgo3vS5v{l=*T|2Z>yR=(-v{(DIUk7whhjds+bX3Q5Tqkr=r*v9p zbXMndULW8g9^o;b;3=NrIbPr;Ug0&~;4R+aJwD(gKH)RI;48l2JAU9Ne&IL%;4l6` z2$D+U3a;cTuI3u9 uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFBfnTmv9+Za23~Z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=# zpYR!9@D<@E89ebWlfiQfGBhS9Mc&^-xdsQg8K9U-eUe4bVUh(qIkI zPz}>?jnGJq(rAs*SdG(oP0&P5(qv81R87-#&CpEE(rnGqT+P#bEnp#wSj-ZZvW(@d zU?r;hy6H! zgE)l4ID(@%hT}MalQ@ObID@k|hx2eSM{_b~b1_$QGk5baPxCTw^D$rZGk*)PKnt>9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxJ4l++ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_ztcyx.ome.zarr/.zattrs b/tests/data/omezarr/dim5_ztcyx.ome.zarr/.zattrs new file mode 100644 index 000000000..9609d020e --- /dev/null +++ b/tests/data/omezarr/dim5_ztcyx.ome.zarr/.zattrs @@ -0,0 +1,46 @@ +{ + "multiscales": [ + { + "version": "0.4", + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ], + "axes": [ + { + "name": "z", + "type": "space" + }, + { + "name": "t", + "type": "time" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_ztcyx.ome.zarr/.zgroup b/tests/data/omezarr/dim5_ztcyx.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim5_ztcyx.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_ztcyx.ome.zarr/0/.zarray b/tests/data/omezarr/dim5_ztcyx.ome.zarr/0/.zarray new file mode 100644 index 000000000..0b7f4fcac --- /dev/null +++ b/tests/data/omezarr/dim5_ztcyx.ome.zarr/0/.zarray @@ -0,0 +1,23 @@ +{ + "shape": [ + 4, + 2, + 3, + 6, + 8 + ], + "chunks": [ + 1, + 2, + 3, + 6, + 8 + ], + "dtype": "{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_t& zMhQw$hH_M(5>=>14Qf$`dNiOBO=w07TG57fbf6Pm=td8E(T9EvU=TwX#t23+hH*?_ z5>uGQ3}!Khc`RTNmw1_1c$L?9oi})sw|JX(c$fEhpAYzukNB8R_>|9>&*yx>0>0!c zzUCXgWg*}3JwNaxKk+la@GHOZJAd#efAKf}@Gt*yi7mBdw%k_ON?T>CZH?txo~^ZY zw%#__M%!eYZHsNSZMNNZ*iPGJyKRr{wSBhV4%k6EWQXmD9kpY2+)mg@J7uTsjGeV} zcHS=7MWalTgB;}~XSv8#ZgQ80Jmn>C`N&s(@>hTY6{KK=C{$qzSA-%JrD(+{R&k0~ zf)bUaWThxoX-ZdyGL@xl<)}!-Dp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKPf|rK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_ztcyx.ome.zarr/0/1.0.0.0.0 b/tests/data/omezarr/dim5_ztcyx.ome.zarr/0/1.0.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..678ed89e2b57253198aa620a5b4f198239f69bdc GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?wBaw>(Sc5Mp&LEuMIZVxfI$r5ABF*vM$s9-5e#GygE^9;IGQ00Wf;R5 z!7+?v6r&l#v5aLL<2jBAOk@(1nZmO?$Md|vi@e0kyuz!z#_PPno4m!_yu-V^$NPN1 zhkV4xe8Q)E#^-#&mwd(7e8abV$M^iekNm{X{KBvN#_#;WpZvw&{KL66&*s|#TWE`H zu`RKsw#=5>3R`KbY_+YiwYJXI+XmZcn{2afv8}evw%ZQdX}fHc2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(37+B^p5p~x;uT)w4c_7%-s1y4;uAjO3%=qT zzT*de;un775B}mGgdnLjuHZ_p;%ctpTCU@IZs104;%08)R&L{V?%+-?aFM&Xn|rvI z`?#M6c#wy9m`8Y&$9SA4c#@}hnrC>H=XjnMc#)U5#AROQ6<*~vUgr(oW~iWh>q%*j_ZU@>Xc6FjLzzu&Z~ntnv*%3i@BPcxtoW1nwNQ-kNKLP`CEVmT95@> uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFAnOcPU@^K>Z)$)t{&>CUh1tr>Z^Y0uK^mUK^m+f8meI$t`QokQ5vl=8mn;{uL+u{ zNt&!FnyP7dmnyY!5uM4mci?A3=uoTO%94oLAtFRhtuommE9viR`o3I&M zuoc^|9XqfSyRaL3uowHV9|v#{hj182a1_UI94BxRr*Il)a2Drq9v5&Cmv9+Za23~Z z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=#pYR!9@D<@E89eEMOsvSj-ZZvW(@dU?r9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxRJWSa literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/gen_dim5.py b/tests/data/omezarr/gen_dim5.py new file mode 100644 index 000000000..34b9c377f --- /dev/null +++ b/tests/data/omezarr/gen_dim5.py @@ -0,0 +1,115 @@ +"""Generate OME-Zarr v2 fixtures whose every voxel encodes its own (x,y,z,c,t) +coordinate, so a reader that mis-orders or fails to address the C/T/Z axes reads +a provably wrong value. + + value(x,y,z,c,t) = 1 + ((((t*C + c)*Z + z)*Y + y)*X + x) # C=3,Z=4,Y=6,X=8 + +Stores (same encoding throughout; absent axes are pinned to index 0): + + dim5.ome.zarr axes t,c,z,y,x default TCZYX + dim5_ctzyx.ome.zarr axes c,t,z,y,x non-default (C/T swapped) -> proves 'axes' is honored + dim3_zyx.ome.zarr axes z,y,x 3D -> proves rank-3 axis mapping + dim2_yx.ome.zarr axes y,x 2D -> proves rank-2 axis mapping + dim5_noaxes.ome.zarr (no 'axes' key) 5D TCZYX -> proves the legacy fallback + +All chunked one z-slice per chunk and written UNCOMPRESSED (compressor=None) so +they read with header-only z5 (no blosc/zlib link). + +Run with the nyxus_build env python (zarr 3.x): + C:\\Users\\dvladi\\miniforge3\\envs\\nyxus_build\\python.exe gen_dim5.py +""" +import os +import shutil +import json +import numpy as np +import zarr + +T, C, Z, Y, X = 2, 3, 4, 6, 8 +HERE = os.path.dirname(os.path.abspath(__file__)) + +_AX = { + "t": {"name": "t", "type": "time"}, + "c": {"name": "c", "type": "channel"}, + "z": {"name": "z", "type": "space"}, + "y": {"name": "y", "type": "space"}, + "x": {"name": "x", "type": "space"}, +} + + +def encoded_tczyx(): + """Logical volume indexed [t,c,z,y,x].""" + t, c, z, y, x = np.meshgrid( + np.arange(T), np.arange(C), np.arange(Z), np.arange(Y), np.arange(X), indexing="ij") + return (1 + ((((t * C + c) * Z + z) * Y + y) * X + x)).astype("uint16") + + +def _data_for(order): + """Array laid out in `order` (a subset/permutation of 'tczyx'); axes absent + from `order` are pinned to index 0.""" + base = encoded_tczyx() # [t,c,z,y,x] + sel = tuple(slice(None) if a in order else 0 for a in "tczyx") + sub = base[sel] # absent axes dropped + remaining = [a for a in "tczyx" if a in order] # kept axes, tczyx order + perm = tuple(remaining.index(a) for a in order) # -> requested order + return np.transpose(sub, perm).copy() + + +def write_store(name, order, include_axes=True): + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + data = _data_for(order) + chunks = tuple(1 if a == "z" else s for a, s in zip(order, data.shape)) + + g = zarr.open_group(path, mode="w", zarr_format=2) + a = g.create_array("0", shape=data.shape, dtype=data.dtype, chunks=chunks, compressors=None) + a[:] = data + + ms = {"version": "0.4", + "datasets": [{"path": "0", + "coordinateTransformations": [{"type": "scale", "scale": [1.0] * len(order)}]}]} + if include_axes: + ms["axes"] = [_AX[a] for a in order] + g.attrs.put({"multiscales": [ms]}) + + meta = json.load(open(os.path.join(path, "0", ".zarray"))) + print("wrote %-22s axes=%-6s shape=%s chunks=%s%s" + % (name, order if include_axes else "(none)", meta["shape"], meta["chunks"], + "" if include_axes else " [no axes -> legacy fallback]")) + + +def write_bad(name, shape, axes): + """Write a deliberately malformed store (its 'axes' disagrees with the array) + so the loader must reject it cleanly, not crash.""" + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + g = zarr.open_group(path, mode="w", zarr_format=2) + chunks = tuple(1 if i == len(shape) - 3 else s for i, s in enumerate(shape)) + a = g.create_array("0", shape=shape, dtype="uint16", chunks=chunks, compressors=None) + a[:] = 0 + g.attrs.put({"multiscales": [{"version": "0.4", "axes": axes, + "datasets": [{"path": "0", + "coordinateTransformations": [{"type": "scale", "scale": [1.0] * len(axes)}]}]}]}) + print("wrote %-24s shape=%s axes=%d (MALFORMED)" % (name, list(shape), len(axes))) + + +def main(): + # All 6 orderings of {t,c,z} before y,x -- the complete legal OME axis-order set. + for o in ["tczyx", "tzcyx", "ctzyx", "cztyx", "ztcyx", "zctyx"]: + write_store("dim5.ome.zarr" if o == "tczyx" else "dim5_%s.ome.zarr" % o, o) + # 4D (rank-4 coverage): one time-only, one channel-only. + write_store("dim4_tzyx.ome.zarr", "tzyx") + write_store("dim4_czyx.ome.zarr", "czyx") + # lower rank + fallback + write_store("dim3_zyx.ome.zarr", "zyx") + write_store("dim2_yx.ome.zarr", "yx") + write_store("dim5_noaxes.ome.zarr", "tczyx", include_axes=False) + # --- illegal / adversarial: must be rejected cleanly, not crash --- + # 3D array but 'axes' declares 5 entries -> indexing the shape by axis role OOBs + write_bad("bad_axes_count.ome.zarr", (Z, Y, X), [_AX[k] for k in ("t", "c", "z", "y", "x")]) + # axes present but none labeled x/y -> X/Y unresolvable + write_bad("bad_no_xy.ome.zarr", (Z, Y, X), + [{"name": "z", "type": "space"}, {"name": "a", "type": "space"}, {"name": "b", "type": "space"}]) + + +if __name__ == "__main__": + main() diff --git a/tests/test_3d_nifti_mechanics.h b/tests/test_3d_nifti_mechanics.h index be018f633..aaafb62ba 100644 --- a/tests/test_3d_nifti_mechanics.h +++ b/tests/test_3d_nifti_mechanics.h @@ -1,5 +1,6 @@ #include #include "../src/nyx/raw_nifti.h" +#include "../src/nyx/image_loader.h" #include "../src/nyx/helpers/fsystem.h" void test_3d_nifti_loader () @@ -56,7 +57,7 @@ void test_3d_nifti_data_access_consistency() ASSERT_NO_THROW(h = ldr1.fullHeight(0)); auto t = std::make_shared> (d*w*h); - ASSERT_NO_THROW (ldr1.loadTileFromFile (t, 0, 0, 0, 0)); + ASSERT_NO_THROW (ldr1.loadTileFromFile (t, 0, 0, 0, 0/*channel*/, 0/*timeframe*/, 0)); // stats std::vector& databuf = *t; @@ -65,3 +66,40 @@ void test_3d_nifti_data_access_consistency() tot += x; ASSERT_TRUE(tot == 544286216); } + +// Safety net for wiring the volumetric consumer onto load_volume(): for a NIfTI +// (whole-4D loader), load_volume(0,t) must equal the t-th timeframe slab of the +// current whole-volume read (load_tile(0,0) + get_int_tile_buffer). If this holds, +// swapping the consumer's read source cannot regress NIfTI. +void test_facade_nifti_load_volume_equivalence() +{ + fs::path p(__FILE__); + fs::path f("/data/nifti/signal/signal1.nii"); + fs::path ds = (p.parent_path().string() + f.make_preferred().string()); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps sp; + sp.fname_int = ds.string(); + sp.fname_seg = ""; + FpImageOptions fp; + ImageLoader il; + ASSERT_TRUE(il.open(sp, fp)) << ds.string(); + + const size_t timeFrameSize = il.get_full_width() * il.get_full_height() * il.get_full_depth(); + const size_t nt = il.get_inten_time(); + ASSERT_GE(nt, 1u); + + ASSERT_TRUE(il.load_tile((size_t)0, (size_t)0)); // the current NIfTI read: whole x*y*z*t blob + const std::vector whole = il.get_int_tile_buffer(); // copy (load_volume reuses ptrI) + ASSERT_GE(whole.size(), timeFrameSize * nt); + + for (size_t t = 0; t < nt; ++t) + { + ASSERT_TRUE(il.load_volume(0, t)); + const std::vector& vol = il.get_int_volume_buffer(); + ASSERT_EQ(vol.size(), timeFrameSize); + for (size_t i = 0; i < timeFrameSize; ++i) + ASSERT_EQ(vol[i], whole[t * timeFrameSize + i]) << "t=" << t << " i=" << i; + } + il.close(); +} diff --git a/tests/test_all.cc b/tests/test_all.cc index e7ced9fd9..cf0a339bd 100644 --- a/tests/test_all.cc +++ b/tests/test_all.cc @@ -4,6 +4,8 @@ #include "../src/nyx/environment.h" #include "../src/nyx/globals.h" #include "test_contour.h" +#include "test_ome_meta.h" // native-OME metadata parsers / OmeAxes descriptor +#include "test_ometiff_mechanics.h" // OME-TIFF native (z,c,t)->IFD read (core; no USE_Z5) #include "test_firstorder_regression.h" #include "test_intensity_histogram_regression.h" #include "test_intensity_histogram_ibsi.h" @@ -2509,6 +2511,10 @@ TEST(TEST_NYXUS, TEST_3D_NIFTY_DACC_CONSISTENCY) { ASSERT_NO_THROW (test_3d_nifti_data_access_consistency()); } +TEST(TEST_NYXUS, TEST_FACADE_NIFTI_LOAD_VOLUME_EQUIVALENCE) { + ASSERT_NO_THROW (test_facade_nifti_load_volume_equivalence()); +} + //***** OME-Zarr i/o ***** @@ -2538,10 +2544,147 @@ TEST(TEST_NYXUS, TEST_RAW_OMEZARR_MULTITILE) { ASSERT_NO_THROW (test_raw_omezarr_multitile()); } +TEST(TEST_NYXUS, TEST_OMEZARR_5D_CHANNEL_TIME_ADDRESSING) { + ASSERT_NO_THROW (test_omezarr_addressing("dim5.ome.zarr", 2, 3, 4)); +} + +TEST(TEST_NYXUS, TEST_RAW_OMEZARR_5D_CHANNEL_TIME_ADDRESSING) { + ASSERT_NO_THROW (test_raw_omezarr_addressing("dim5.ome.zarr", 2, 3, 4)); +} + +// All 6 legal orderings of {t,c,z}: passes only if the loader honors the NGFF +// 'axes' metadata instead of assuming a fixed [T,C,Z,Y,X] order. +TEST(TEST_NYXUS, TEST_OMEZARR_ALL_5D_PERMUTATIONS) { + ASSERT_NO_THROW (test_omezarr_all_5d_permutations()); +} + +// 4D (rank-4): time-only and channel-only. +TEST(TEST_NYXUS, TEST_OMEZARR_4D_TZYX) { + ASSERT_NO_THROW (test_omezarr_addressing("dim4_tzyx.ome.zarr", 2, 1, 4)); + ASSERT_NO_THROW (test_raw_omezarr_addressing("dim4_tzyx.ome.zarr", 2, 1, 4)); +} +TEST(TEST_NYXUS, TEST_OMEZARR_4D_CZYX) { + ASSERT_NO_THROW (test_omezarr_addressing("dim4_czyx.ome.zarr", 1, 3, 4)); + ASSERT_NO_THROW (test_raw_omezarr_addressing("dim4_czyx.ome.zarr", 1, 3, 4)); +} + +// End-to-end through the wired volumetric consumer (scan_trivial_wholevolume). +TEST(TEST_NYXUS, TEST_OMEZARR_WHOLEVOLUME_CONSUMER) { + ASSERT_NO_THROW (test_omezarr_wholevolume_consumer("dim3_zyx.ome.zarr", 4)); +} + +// Facade whole-volume assembly (load_volume loops Z into one X*Y*Z buffer). +TEST(TEST_NYXUS, TEST_OMEZARR_FACADE_VOLUME_3D) { + ASSERT_NO_THROW (test_omezarr_facade_volume("dim3_zyx.ome.zarr", 1, 1, 4)); +} +TEST(TEST_NYXUS, TEST_OMEZARR_FACADE_VOLUME_5D) { + ASSERT_NO_THROW (test_omezarr_facade_volume("dim5.ome.zarr", 2, 3, 4)); +} + +// Lower-rank stores: a fixed shape[2..4] loader would crash; the axis-role loader +// reads 3D (ZYX) and 2D (YX) correctly. +TEST(TEST_NYXUS, TEST_OMEZARR_3D_ZYX) { + ASSERT_NO_THROW (test_omezarr_addressing("dim3_zyx.ome.zarr", 1, 1, 4)); +} + +TEST(TEST_NYXUS, TEST_RAW_OMEZARR_3D_ZYX) { + ASSERT_NO_THROW (test_raw_omezarr_addressing("dim3_zyx.ome.zarr", 1, 1, 4)); +} + +TEST(TEST_NYXUS, TEST_OMEZARR_2D_YX) { + ASSERT_NO_THROW (test_omezarr_addressing("dim2_yx.ome.zarr", 1, 1, 1)); +} + +// No 'axes' metadata -> the loader falls back to legacy 5D TCZYX and still reads. +TEST(TEST_NYXUS, TEST_OMEZARR_NOAXES_FALLBACK) { + ASSERT_NO_THROW (test_omezarr_addressing("dim5_noaxes.ome.zarr", 2, 3, 4)); +} + +TEST(TEST_NYXUS, TEST_RAW_OMEZARR_NOAXES_FALLBACK) { + ASSERT_NO_THROW (test_raw_omezarr_addressing("dim5_noaxes.ome.zarr", 2, 3, 4)); +} + +// Negative: out-of-range Z/C/T plane index must throw. +TEST(TEST_NYXUS, TEST_OMEZARR_OUT_OF_RANGE_THROWS) { + ASSERT_NO_THROW (test_omezarr_out_of_range_throws()); +} + +// Illegal / adversarial: malformed metadata must be rejected cleanly, not crash. +TEST(TEST_NYXUS, TEST_OMEZARR_MALFORMED_THROWS) { + ASSERT_NO_THROW (test_omezarr_malformed_throws()); +} + #endif // OMEZARR_SUPPORT -int main(int argc, char **argv) +//***** OME-TIFF native (z,c,t) -> IFD read (core; runs in every build) ***** + +TEST(TEST_NYXUS, TEST_OMETIFF_5D_CHANNEL_TIME_ADDRESSING) { + ASSERT_NO_THROW (test_ometiff_addressing("dim5.ome.tif", 2, 3, 4)); +} +TEST(TEST_NYXUS, TEST_RAW_OMETIFF_5D_CHANNEL_TIME_ADDRESSING) { + ASSERT_NO_THROW (test_raw_ometiff_addressing("dim5.ome.tif", 2, 3, 4)); +} + +// All 6 legal DimensionOrder values: passes only if ifdForPlane honors DimensionOrder. +TEST(TEST_NYXUS, TEST_OMETIFF_ALL_5D_PERMUTATIONS) { + ASSERT_NO_THROW (test_ometiff_all_5d_permutations()); +} + +// 4D (rank-4): time-only and channel-only. +TEST(TEST_NYXUS, TEST_OMETIFF_4D_TZYX) { + ASSERT_NO_THROW (test_ometiff_addressing("dim4_tzyx.ome.tif", 2, 1, 4)); + ASSERT_NO_THROW (test_raw_ometiff_addressing("dim4_tzyx.ome.tif", 2, 1, 4)); +} +TEST(TEST_NYXUS, TEST_OMETIFF_4D_CZYX) { + ASSERT_NO_THROW (test_ometiff_addressing("dim4_czyx.ome.tif", 1, 3, 4)); + ASSERT_NO_THROW (test_raw_ometiff_addressing("dim4_czyx.ome.tif", 1, 3, 4)); +} + +// End-to-end through the wired volumetric consumer (scan_trivial_wholevolume). +TEST(TEST_NYXUS, TEST_OMETIFF_WHOLEVOLUME_CONSUMER) { + ASSERT_NO_THROW (test_ometiff_wholevolume_consumer("dim3_zyx.ome.tif", 4)); +} + +// Facade whole-volume assembly (load_volume loops Z into one X*Y*Z buffer). +TEST(TEST_NYXUS, TEST_OMETIFF_FACADE_VOLUME_3D) { + ASSERT_NO_THROW (test_ometiff_facade_volume("dim3_zyx.ome.tif", 1, 1, 4)); +} +TEST(TEST_NYXUS, TEST_OMETIFF_FACADE_VOLUME_5D) { + ASSERT_NO_THROW (test_ometiff_facade_volume("dim5.ome.tif", 2, 3, 4)); +} + +// Lower-rank OME-TIFF. +TEST(TEST_NYXUS, TEST_OMETIFF_3D_ZYX) { + ASSERT_NO_THROW (test_ometiff_addressing("dim3_zyx.ome.tif", 1, 1, 4)); +} +TEST(TEST_NYXUS, TEST_RAW_OMETIFF_3D_ZYX) { + ASSERT_NO_THROW (test_raw_ometiff_addressing("dim3_zyx.ome.tif", 1, 1, 4)); +} +TEST(TEST_NYXUS, TEST_OMETIFF_2D_YX) { + ASSERT_NO_THROW (test_ometiff_addressing("dim2_yx.ome.tif", 1, 1, 1)); +} + +// Plain multi-page TIFF (no OME-XML): the legacy page=Z fallback must still work. +TEST(TEST_NYXUS, TEST_OMETIFF_PLAIN_MULTIPAGE_FALLBACK) { + ASSERT_NO_THROW (test_ometiff_addressing("dim3_plain.tif", 1, 1, 4)); +} +TEST(TEST_NYXUS, TEST_RAW_OMETIFF_PLAIN_MULTIPAGE_FALLBACK) { + ASSERT_NO_THROW (test_raw_ometiff_addressing("dim3_plain.tif", 1, 1, 4)); +} + +// Negative: out-of-range Z/C/T plane index must throw. +TEST(TEST_NYXUS, TEST_OMETIFF_OUT_OF_RANGE_THROWS) { + ASSERT_NO_THROW (test_ometiff_out_of_range_throws()); +} + +// Illegal / adversarial: RGB / corrupt / missing files must be rejected cleanly. +TEST(TEST_NYXUS, TEST_OMETIFF_MALFORMED_THROWS) { + ASSERT_NO_THROW (test_ometiff_malformed_throws()); +} + + +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); int ret = RUN_ALL_TESTS(); diff --git a/tests/test_hu_mechanics.h b/tests/test_hu_mechanics.h index 33876a038..afcf76f77 100644 --- a/tests/test_hu_mechanics.h +++ b/tests/test_hu_mechanics.h @@ -75,7 +75,7 @@ static std::vector hu_load_tile0(const char* fixture, bool preserve_hu size_t th = ldr.tileHeight(0), tw = ldr.tileWidth(0); auto tile = std::make_shared>(th * tw, 0u); - EXPECT_NO_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 0)); + EXPECT_NO_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 0/*channel*/, 0/*timeframe*/, 0)); return *tile; } @@ -124,7 +124,7 @@ static std::vector hu_load_dicom_tile0(const char* fixture, bool prese NyxusGrayscaleDicomLoader ldr(1, ds.string(), fpmin, preserve_hu); size_t th = ldr.tileHeight(0), tw = ldr.tileWidth(0); auto tile = std::make_shared>(th * tw, 0u); - EXPECT_NO_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 0)); + EXPECT_NO_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 0/*channel*/, 0/*timeframe*/, 0)); return *tile; } diff --git a/tests/test_morphology_common.h b/tests/test_morphology_common.h index a72212037..a8b2bb473 100644 --- a/tests/test_morphology_common.h +++ b/tests/test_morphology_common.h @@ -238,7 +238,7 @@ static void calculate_fractal_blob512_feature_values(std::vector>(tw * th * td); - loader.loadTileFromFile(tile, 0, 0, 0, 0); // 512x512 <= 1024 strip tile: one read + loader.loadTileFromFile(tile, 0, 0, 0, 0/*channel*/, 0/*timeframe*/, 0); // 512x512 <= 1024 strip tile: one read std::vector px; for (size_t y = 0; y < H; y++) diff --git a/tests/test_ome_meta.h b/tests/test_ome_meta.h new file mode 100644 index 000000000..62bf687bc --- /dev/null +++ b/tests/test_ome_meta.h @@ -0,0 +1,422 @@ +#pragma once + +// Unit tests for the native-OME metadata parsers / OmeAxes descriptor. +// The OME-XML parser and OmeAxes helpers are exercised here with embedded +// fixtures (no external files). The OME-Zarr parser is guarded by +// OMEZARR_SUPPORT (compiled only in USE_Z5 builds); it is additionally +// validated end-to-end against the reference-data matrix by +// io_oracle/ome_meta_selftest.cpp. + +#include +#include "../src/nyx/ome/ome_axes.h" +#include "../src/nyx/ome/ome_tiff_meta.h" + +// 5D, non-default DimensionOrder XYZTC (C and T swapped) — the case that catches +// a reader assuming TCZYX. Taken verbatim from io_oracle/fixtures/xml/5d_ctzyx.xml. +static const char* kOmeXml_5d_ctzyx = + "" + "" + "" + "" + ""; + +static const char* kOmeXml_2d = + "" + ""; + +TEST(OmeTiffMeta, ParsesNonDefault5D) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(kOmeXml_5d_ctzyx); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 8u); + EXPECT_EQ(a.sizeY, 6u); + EXPECT_EQ(a.sizeZ, 4u); + EXPECT_EQ(a.sizeC, 3u); + EXPECT_EQ(a.sizeT, 2u); + // XYZTC means C slowest, T next -> on-disk (slowest-first) is C,T,Z,Y,X. + EXPECT_EQ(a.storageOrder, "CTZYX"); + EXPECT_EQ(a.omeDimensionOrder, "XYZTC"); + EXPECT_EQ(a.dtype, Nyxus::PixelType::UInt16); + EXPECT_EQ(a.bitsPerSample, 16u); + EXPECT_DOUBLE_EQ(a.physX, 0.5); + EXPECT_DOUBLE_EQ(a.physZ, 2.0); + EXPECT_DOUBLE_EQ(a.timeIncrement, 1.0); + EXPECT_EQ(a.unitZ, "micrometer"); + EXPECT_EQ(a.unitT, "second"); + EXPECT_TRUE(a.isVolumetric()); + EXPECT_TRUE(a.isMultiChannel()); + EXPECT_TRUE(a.isTimeSeries()); + ASSERT_EQ(a.channelNames.size(), 2u); + EXPECT_EQ(a.channelNames[0], "DAPI"); + EXPECT_EQ(a.channelNames[1], "GFP"); + // axis-role lookup independent of on-disk order + EXPECT_EQ(a.storageIndexOf('C'), 0); // C is first (slowest) in CTZYX + EXPECT_EQ(a.storageIndexOf('X'), 4); +} + +TEST(OmeTiffMeta, Parses2DSingletonAxesCollapse) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(kOmeXml_2d); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeZ, 1u); + EXPECT_EQ(a.sizeC, 1u); + EXPECT_EQ(a.sizeT, 1u); + EXPECT_EQ(a.storageOrder, "YX"); // singleton Z/C/T dropped; planes stay YX + EXPECT_FALSE(a.isVolumetric()); + EXPECT_FALSE(a.isMultiChannel()); + EXPECT_FALSE(a.isTimeSeries()); +} + +TEST(OmeTiffMeta, InvalidWhenNoPixels) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(""); + EXPECT_FALSE(a.valid); +} + +// --------------------------------------------------------------------------- +// Negative / robustness: bad or missing metadata for every field we parse. +// The parser must never crash and must degrade to safe defaults. +// --------------------------------------------------------------------------- + +TEST(OmeTiffMetaBad, EmptyAndGarbageInput) +{ + EXPECT_FALSE(Nyxus::parse_ome_xml("").valid); + EXPECT_FALSE(Nyxus::parse_ome_xml("not xml at all").valid); + EXPECT_FALSE(Nyxus::parse_ome_xml("").valid); +} + +TEST(OmeTiffMetaBad, UnterminatedPixelsTagIsInvalid) +{ + // no '>' closing the Pixels start-tag + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(""); + EXPECT_FALSE(a.valid); +} + +TEST(OmeTiffMetaBad, AllSizesMissingDefaultToOne) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(""); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 1u); EXPECT_EQ(a.sizeY, 1u); EXPECT_EQ(a.sizeZ, 1u); + EXPECT_EQ(a.sizeC, 1u); EXPECT_EQ(a.sizeT, 1u); + EXPECT_EQ(a.storageOrder, "YX"); + EXPECT_EQ(a.omeDimensionOrder, "XYZCT"); // missing -> default + EXPECT_EQ(a.dtype, Nyxus::PixelType::UInt16); // missing Type -> default + EXPECT_DOUBLE_EQ(a.physX, 1.0); // missing PhysicalSize -> 1.0 + EXPECT_DOUBLE_EQ(a.physZ, 1.0); + EXPECT_DOUBLE_EQ(a.timeIncrement, 1.0); + EXPECT_TRUE(a.unitXY.empty()); + EXPECT_TRUE(a.unitZ.empty()); + EXPECT_TRUE(a.unitT.empty()); +} + +TEST(OmeTiffMetaBad, NonNumericSizeDefaultsToOne) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml( + ""); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 1u); // "abc" -> 1, not 0 + EXPECT_EQ(a.sizeY, 6u); + EXPECT_EQ(a.sizeZ, 1u); // "" -> 1 +} + +TEST(OmeTiffMetaBad, ZeroSizeClampedToOne) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml( + ""); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 1u); // 0 would divide-by-zero the tile grid + EXPECT_EQ(a.sizeY, 1u); + EXPECT_EQ(a.sizeZ, 4u); +} + +TEST(OmeTiffMetaBad, NegativeSizeRejectedToDefault) +{ + // strtoull wraps a leading '-' into a huge extent; parser must reject it + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(""); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 1u); // negative -> default 1, not a giant number + EXPECT_EQ(a.sizeY, 6u); +} + +TEST(OmeTiffMetaBad, MissingDimensionOrderDefaults) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(""); + EXPECT_EQ(a.omeDimensionOrder, "XYZCT"); + EXPECT_EQ(a.storageOrder, "ZYX"); +} + +TEST(OmeTiffMetaBad, MalformedDimensionOrderFallsBack) +{ + for (const char* bad : { "FOOBAR", "XYZC", "XYZCC", "XXYZCT", "xyzct", "" }) + { + std::string xml = std::string(""; + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(xml); + ASSERT_TRUE(a.valid) << bad; + EXPECT_EQ(a.omeDimensionOrder, "XYZCT") << "bad input: " << bad; + } +} + +TEST(OmeTiffMetaBad, SingleQuotedAttributes) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml( + ""); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 8u); + EXPECT_EQ(a.sizeZ, 4u); + EXPECT_EQ(a.dtype, Nyxus::PixelType::UInt8); + EXPECT_DOUBLE_EQ(a.physX, 0.25); +} + +TEST(OmeTiffMetaBad, UnknownAndMissingTypeFallBackToUint16) +{ + EXPECT_EQ(Nyxus::parse_ome_xml("").dtype, + Nyxus::PixelType::UInt16); + EXPECT_EQ(Nyxus::parse_ome_xml("").dtype, + Nyxus::PixelType::UInt16); +} + +TEST(OmeTiffMetaBad, AllPixelTypesResolve) +{ + auto ty = [](const char* t) { + std::string xml = std::string(""; + return Nyxus::parse_ome_xml(xml).dtype; + }; + EXPECT_EQ(ty("uint8"), Nyxus::PixelType::UInt8); + EXPECT_EQ(ty("int16"), Nyxus::PixelType::Int16); + EXPECT_EQ(ty("int32"), Nyxus::PixelType::Int32); + EXPECT_EQ(ty("float"), Nyxus::PixelType::Float32); + EXPECT_EQ(ty("double"), Nyxus::PixelType::Float64); + EXPECT_EQ(Nyxus::parse_ome_xml("").bitsPerSample, 64u); +} + +TEST(OmeTiffMetaBad, NonNumericPhysicalSizesDefaultToOne) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml( + ""); + ASSERT_TRUE(a.valid); + EXPECT_DOUBLE_EQ(a.physX, 1.0); // failed parse -> default, never 0 + EXPECT_DOUBLE_EQ(a.physZ, 1.0); + EXPECT_DOUBLE_EQ(a.timeIncrement, 1.0); +} + +TEST(OmeTiffMetaBad, NanInfPhysicalSizesRejected) +{ + // strtod parses "NaN"/"inf" prefixes; such values must not leak into calibration + for (const char* bad : { "NaN", "NaNsense", "inf", "-inf", "infinity" }) + { + std::string xml = std::string(""; + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(xml); + ASSERT_TRUE(a.valid) << bad; + EXPECT_DOUBLE_EQ(a.physZ, 1.0) << "PhysicalSizeZ from " << bad; + EXPECT_DOUBLE_EQ(a.timeIncrement, 1.0) << "TimeIncrement from " << bad; + } +} + +TEST(OmeTiffMetaBad, SizeXNotConfusedWithPhysicalSizeX) +{ + // attribute-name boundary: SizeX must not read from PhysicalSizeX + Nyxus::OmeAxes a = Nyxus::parse_ome_xml( + ""); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 8u); + EXPECT_DOUBLE_EQ(a.physX, 9.9); + EXPECT_EQ(a.unitXY, "nm"); +} + +TEST(OmeTiffMetaBad, WhitespaceAndNewlinesInTag) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml( + ""); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 8u); + EXPECT_EQ(a.sizeZ, 4u); + EXPECT_EQ(a.omeDimensionOrder, "XYZCT"); +} + +TEST(OmeTiffMetaBad, ChannelsWithoutNameYieldNoNames) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml( + "" + ""); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeC, 2u); + EXPECT_TRUE(a.channelNames.empty()); // no Name attr -> nothing collected +} + +TEST(OmeTiffMetaBad, FirstPixelsWinsAcrossMultipleImages) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml( + "" + ""); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 8u); + EXPECT_EQ(a.sizeY, 6u); +} + +TEST(OmeAxes, PixelTypeMappings) +{ + using namespace Nyxus; + EXPECT_EQ(pixel_type_from_ome_type("uint16"), PixelType::UInt16); + EXPECT_EQ(pixel_type_from_ome_type("float"), PixelType::Float32); + EXPECT_EQ(pixel_type_from_ome_type("double"), PixelType::Float64); + // zarr v2 numpy strings and v3 names both resolve + EXPECT_EQ(pixel_type_from_zarr_dtype(" shape = {3, 2, 4, 6, 8}; // C,T,Z,Y,X + Nyxus::OmeAxes a = Nyxus::parse_ome_zarr(attrs, shape, " shape, const char* dt = " all default + EXPECT_DOUBLE_EQ(a.physY, 1.0); +} + +TEST(OmeZarrMetaBad, NonNumericScaleElementDefaultsThatAxis) +{ + Nyxus::OmeAxes a = zarr_parse( + R"({"multiscales":[{"axes":[{"name":"y","type":"space"},{"name":"x","type":"space"}], + "datasets":[{"path":"0","coordinateTransformations":[{"type":"scale","scale":["oops",0.5]}]}]}]})", + {6, 8}); + ASSERT_TRUE(a.valid); + EXPECT_DOUBLE_EQ(a.physY, 1.0); // "oops" -> 1.0 + EXPECT_DOUBLE_EQ(a.physX, 0.5); +} + +TEST(OmeZarrMetaBad, MalformedAxisEntrySkipped) +{ + // a non-object axis entry must be skipped, not crash + Nyxus::OmeAxes a = zarr_parse( + R"({"multiscales":[{"axes":["notanobject",{"name":"y","type":"space"},{"name":"x","type":"space"}], + "datasets":[{"path":"0","coordinateTransformations":[{"type":"scale","scale":[1,0.5,0.5]}]}]}]})", + {1, 6, 8}); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeX, 8u); + EXPECT_EQ(a.sizeY, 6u); +} + +TEST(OmeZarrMetaBad, UnknownDtypeFallsBackUint16) +{ + Nyxus::OmeAxes a = zarr_parse( + R"({"multiscales":[{"axes":[{"name":"y","type":"space"},{"name":"x","type":"space"}], + "datasets":[{"path":"0","coordinateTransformations":[{"type":"scale","scale":[1,1]}]}]}]})", + {6, 8}, "weird64"); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.dtype, Nyxus::PixelType::UInt16); +} + +TEST(OmeZarrMetaBad, V3OmeKeyPathParses) +{ + // 0.5 nests the model under "ome" + Nyxus::OmeAxes a = zarr_parse( + R"({"ome":{"version":"0.5","multiscales":[{ + "axes":[{"name":"z","type":"space"},{"name":"y","type":"space"},{"name":"x","type":"space"}], + "datasets":[{"path":"0","coordinateTransformations":[{"type":"scale","scale":[2.0,0.5,0.5]}]}]}]}})", + {4, 6, 8}, "uint16"); + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeZ, 4u); + EXPECT_EQ(a.storageOrder, "ZYX"); + EXPECT_DOUBLE_EQ(a.physZ, 2.0); +} + +TEST(OmeZarrMetaBad, ShorterShapeThanAxesDefaultsMissingSizes) +{ + // level0Shape shorter than axes list -> missing dims default to size 1, no OOB + Nyxus::OmeAxes a = zarr_parse( + R"({"multiscales":[{"axes":[{"name":"c","type":"channel"},{"name":"y","type":"space"},{"name":"x","type":"space"}], + "datasets":[{"path":"0","coordinateTransformations":[{"type":"scale","scale":[1,0.5,0.5]}]}]}]})", + {3}); // only C provided + ASSERT_TRUE(a.valid); + EXPECT_EQ(a.sizeC, 3u); + EXPECT_EQ(a.sizeY, 1u); + EXPECT_EQ(a.sizeX, 1u); +} +#endif diff --git a/tests/test_ometiff_mechanics.h b/tests/test_ometiff_mechanics.h new file mode 100644 index 000000000..88da399a6 --- /dev/null +++ b/tests/test_ometiff_mechanics.h @@ -0,0 +1,195 @@ +#pragma once + +// OME-TIFF native read: the multi-page loaders map (z,c,t) to the correct IFD via +// the OME-XML DimensionOrder, instead of assuming every page is a Z-slice. +// +// Fixtures (tests/data/ometiff, see gen_ome_tiff.py) encode every voxel as +// value(x,y,z,c,t) = 1 + ((((t*C + c)*Z + z)*Y + y)*X + x), C=3,Z=4,Y=6,X=8 +// so reading plane (z,c,t) must return that plane's values; a loader that mapped +// (z,c,t) to the wrong page returns provably wrong data. Unlike OME-Zarr this needs +// no USE_Z5 -- TIFF is a core dependency, so these run in every build. + +#include +#include +#include +#include "../src/nyx/grayscale_tiff.h" +#include "../src/nyx/raw_tiff.h" +#include "../src/nyx/image_loader.h" +#include "../src/nyx/globals.h" // scan_trivial_wholevolume, LR +#include "../src/nyx/helpers/fsystem.h" + +static inline fs::path ometiff_data_path(const char* name) +{ + fs::path p(__FILE__); + fs::path rel(std::string("/data/ometiff/") + name); + return fs::path(p.parent_path().string() + rel.make_preferred().string()); +} + +static inline uint32_t ometiff_enc(int x, int y, int z, int c, int t) +{ + const int C = 3, Z = 4, Y = 6, X = 8; + return static_cast(1 + ((((t * C + c) * Z + z) * Y + y) * X + x)); +} + +// AbstractTileLoader stack (NyxusGrayscaleTiffStripLoader). (T,C,Z) are the store's +// extents (X=8,Y=6 fixed). One body covers TCZYX, non-default CTZYX, 3D, 2D, and +// the plain non-OME multi-page store -- all must return the same encoded values. +void test_ometiff_addressing(const char* store, int T, int C, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = ometiff_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto ldr = NyxusGrayscaleTiffStripLoader(1, ds.string()); + ASSERT_EQ(ldr.fullWidth(0), (size_t)X); + ASSERT_EQ(ldr.fullHeight(0), (size_t)Y); + ASSERT_EQ(ldr.fullDepth(0), (size_t)Z); // 5D OME-TIFF must report SizeZ, not the total page count + const size_t tw = ldr.tileWidth(0); + auto tile = std::make_shared>(ldr.tileHeight(0) * tw, 0u); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + for (int z = 0; z < Z; ++z) + { + std::fill(tile->begin(), tile->end(), 0u); + ASSERT_NO_THROW(ldr.loadTileFromFile(tile, 0, 0, z, c, t, 0)); + const std::vector& buf = *tile; + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(buf[y * tw + x], ometiff_enc(x, y, z, c, t)) + << store << " plane (z" << z << " c" << c << " t" << t << ") at (" << x << "," << y << ")"; + } +} + +// RawFormatLoader stack (RawTiffStripLoader). +void test_raw_ometiff_addressing(const char* store, int T, int C, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = ometiff_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto ldr = RawTiffStripLoader(1, ds.string()); + ASSERT_EQ(ldr.fullDepth(0), (size_t)Z); + const size_t w = ldr.fullWidth(0); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + for (int z = 0; z < Z; ++z) + { + ASSERT_NO_THROW(ldr.loadTileFromFile(0, 0, z, c, t, 0)); + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(ldr.get_uint32_pixel(y * w + x), ometiff_enc(x, y, z, c, t)) + << store << " plane (z" << z << " c" << c << " t" << t << ") at (" << x << "," << y << ")"; + } + ldr.free_tile(); +} + +// End-to-end through the WIRED volumetric consumer: scan_trivial_wholevolume must +// feed the whole X*Y*Z volume (all Z planes) into the ROI's voxel cloud, with the +// correct encoded intensity. Before the wiring it read only plane z=0, so this +// asserts the consumer fix, not just the facade. +void test_ometiff_wholevolume_consumer(const char* store, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = ometiff_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader ilo; + ASSERT_TRUE(ilo.open(p, fp)) << ds.string(); + + LR vroi; + ASSERT_TRUE(Nyxus::scan_trivial_wholevolume(vroi, ds.string(), ilo)); + + // every voxel of the X*Y*Z volume must be captured (not just plane z=0 -> 48) + ASSERT_EQ(vroi.raw_pixels_3D.size(), (size_t)X * Y * Z); + for (const Pixel3& px : vroi.raw_pixels_3D) + ASSERT_EQ((uint32_t)px.inten, ometiff_enc((int)px.x, (int)px.y, (int)px.z, 0, 0)) + << "voxel (" << px.x << "," << px.y << "," << px.z << ")"; + ilo.close(); +} + +// Whole-volume assembly through the ImageLoader facade: load_volume() stacks all +// Z-planes (per (channel,timeframe)) into one X*Y*Z buffer -- the foundation that +// lets the volumetric pipeline consume a multi-page OME-TIFF. +void test_ometiff_facade_volume(const char* store, int T, int C, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = ometiff_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ds.string(); + ASSERT_EQ(il.get_full_width(), (size_t)X); + ASSERT_EQ(il.get_full_height(), (size_t)Y); + ASSERT_EQ(il.get_full_depth(), (size_t)Z); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + { + ASSERT_TRUE(il.load_volume(c, t)); + const std::vector& vol = il.get_int_volume_buffer(); + ASSERT_EQ(vol.size(), (size_t)X * Y * Z); + for (int z = 0; z < Z; ++z) + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(vol[(size_t)z * X * Y + (size_t)y * X + x], ometiff_enc(x, y, z, c, t)) + << store << " vol (x" << x << " y" << y << " z" << z << " c" << c << " t" << t << ")"; + } + il.close(); +} + +// Every one of the 6 legal OME DimensionOrder values (all orderings of {T,C,Z} +// before Y,X) must read correctly -- proves ifdForPlane honors DimensionOrder. +void test_ometiff_all_5d_permutations() +{ + for (const char* s : { "dim5.ome.tif", "dim5_tzcyx.ome.tif", "dim5_ctzyx.ome.tif", + "dim5_cztyx.ome.tif", "dim5_ztcyx.ome.tif", "dim5_zctyx.ome.tif" }) + { + test_ometiff_addressing(s, 2, 3, 4); + test_raw_ometiff_addressing(s, 2, 3, 4); + if (::testing::Test::HasFatalFailure()) return; + } +} + +// Negative: an out-of-range Z/C/T plane maps to a non-existent IFD and must throw. +// dim5.ome.tif has T=2, C=3, Z=4 (24 IFDs). +void test_ometiff_out_of_range_throws() +{ + fs::path ds = ometiff_data_path("dim5.ome.tif"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto ldr = NyxusGrayscaleTiffStripLoader(1, ds.string()); + auto tile = std::make_shared>(ldr.tileHeight(0) * ldr.tileWidth(0), 0u); + EXPECT_ANY_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 99, 0, 0)); // channel out of range + EXPECT_ANY_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 0, 99, 0)); // timeframe out of range + EXPECT_ANY_THROW(ldr.loadTileFromFile(tile, 0, 0, 99, 0, 0, 0)); // z out of range + + auto raw = RawTiffStripLoader(1, ds.string()); + EXPECT_ANY_THROW(raw.loadTileFromFile(0, 0, 0, 99, 0, 0)); + EXPECT_ANY_THROW(raw.loadTileFromFile(0, 0, 0, 0, 99, 0)); + EXPECT_ANY_THROW(raw.loadTileFromFile(0, 0, 99, 0, 0, 0)); +} + +// Illegal / adversarial: a non-grayscale (RGB) OME-TIFF and a corrupt/non-TIFF file +// must be rejected cleanly (throw), not crash. +void test_ometiff_malformed_throws() +{ + for (const char* s : { "bad_rgb.ome.tif", "bad_corrupt.tif" }) + { + fs::path ds = ometiff_data_path(s); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + EXPECT_ANY_THROW(NyxusGrayscaleTiffStripLoader(1, ds.string())) << s; + EXPECT_ANY_THROW(RawTiffStripLoader(1, ds.string())) << s; + } + // a path that does not exist + EXPECT_ANY_THROW(RawTiffStripLoader(1, ometiff_data_path("does_not_exist.tif").string())); +} diff --git a/tests/test_omezarr_mechanics.h b/tests/test_omezarr_mechanics.h index cdd2eb127..b20f1ee69 100644 --- a/tests/test_omezarr_mechanics.h +++ b/tests/test_omezarr_mechanics.h @@ -8,6 +8,8 @@ #include #include "../src/nyx/omezarr.h" #include "../src/nyx/raw_omezarr.h" +#include "../src/nyx/image_loader.h" +#include "../src/nyx/globals.h" // scan_trivial_wholevolume, LR #include "../src/nyx/helpers/fsystem.h" // The OME-Zarr test datasets under tests/data/omezarr are generated with bfio @@ -60,7 +62,7 @@ void test_omezarr_tileloader_content() size_t tw = ldr.tileWidth(0); auto tile = std::make_shared>(th * tw, 0u); - ASSERT_NO_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 0)); + ASSERT_NO_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 0/*channel*/, 0/*timeframe*/, 0)); const std::vector& buf = *tile; @@ -106,7 +108,7 @@ void test_omezarr_tileloader_multitile() for (size_t tc = 0; tc < nCols; ++tc) { std::fill(tile->begin(), tile->end(), 0u); - ASSERT_NO_THROW(ldr.loadTileFromFile(tile, tr, tc, 0, 0)); + ASSERT_NO_THROW(ldr.loadTileFromFile(tile, tr, tc, 0, 0/*channel*/, 0/*timeframe*/, 0)); const std::vector& buf = *tile; const size_t row0 = tr * th; @@ -157,7 +159,7 @@ void test_raw_omezarr_content() auto ldr = RawOmezarrLoader(ds.string()); const size_t tw = ldr.tileWidth(0); - ASSERT_NO_THROW(ldr.loadTileFromFile(0, 0, 0, 0)); + ASSERT_NO_THROW(ldr.loadTileFromFile(0, 0, 0, 0/*channel*/, 0/*timeframe*/, 0)); // get_uint32_pixel indexes into the internal tile buffer (stride tileWidth) ASSERT_EQ(ldr.get_uint32_pixel(0 * tw + 0), 0u); @@ -195,7 +197,7 @@ void test_raw_omezarr_multitile() { for (size_t tc = 0; tc < nCols; ++tc) { - ASSERT_NO_THROW(ldr.loadTileFromFile(tr, tc, 0, 0)); + ASSERT_NO_THROW(ldr.loadTileFromFile(tr, tc, 0, 0/*channel*/, 0/*timeframe*/, 0)); const size_t row0 = tr * th; const size_t col0 = tc * tw; const size_t validH = std::min(th, H - row0); @@ -216,4 +218,191 @@ void test_raw_omezarr_multitile() ASSERT_EQ(total, 12681000000ull); } +// --------------------------------------------------------------------------- +// 5D (T,C,Z,Y,X) channel/timeframe addressability. +// +// dim5.ome.zarr (see gen_dim5.py) encodes every voxel as +// value(x,y,z,c,t) = 1 + ((((t*C + c)*Z + z)*Y + y)*X + x), C=3,Z=4,Y=6,X=8 +// chunked one z/c/t-plane per chunk. Reading plane (z,c,t) must return exactly +// that plane's values; a loader that ignored C/T (offset pinned to {0,0,...}, +// the pre-fix behavior) would return the c=0/t=0 plane for every (c,t). +// --------------------------------------------------------------------------- + +static inline uint32_t dim5_enc(int x, int y, int z, int c, int t) +{ + const int C = 3, Z = 4, Y = 6, X = 8; + return static_cast(1 + ((((t * C + c) * Z + z) * Y + y) * X + x)); +} + +// AbstractTileLoader stack. (T,C,Z) are the store's extents (X=8,Y=6 fixed). The +// encoded value is axis-order- AND rank-invariant, so one body covers the default +// TCZYX, the non-default CTZYX, the lower-rank 3D/2D stores, and the no-axes +// (legacy fallback) store — all of which must return the same encoded values. +void test_omezarr_addressing(const char* store, int T, int C, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = omezarr_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto ldr = NyxusOmeZarrLoader(1, ds.string()); + ASSERT_EQ(ldr.fullWidth(0), (size_t)X); + ASSERT_EQ(ldr.fullHeight(0), (size_t)Y); + ASSERT_EQ(ldr.fullDepth(0), (size_t)Z); + const size_t tw = ldr.tileWidth(0); + auto tile = std::make_shared>(ldr.tileHeight(0) * tw, 0u); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + for (int z = 0; z < Z; ++z) + { + std::fill(tile->begin(), tile->end(), 0u); + ASSERT_NO_THROW(ldr.loadTileFromFile(tile, 0, 0, z, c, t, 0)); + const std::vector& buf = *tile; + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(buf[y * tw + x], dim5_enc(x, y, z, c, t)) + << store << " plane (z" << z << " c" << c << " t" << t << ") at (" << x << "," << y << ")"; + } +} + +// RawFormatLoader stack (same coverage as above). +void test_raw_omezarr_addressing(const char* store, int T, int C, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = omezarr_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto ldr = RawOmezarrLoader(ds.string()); + ASSERT_EQ(ldr.fullDepth(0), (size_t)Z); + const size_t tw = ldr.tileWidth(0); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + for (int z = 0; z < Z; ++z) + { + ASSERT_NO_THROW(ldr.loadTileFromFile(0, 0, z, c, t, 0)); + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(ldr.get_uint32_pixel(y * tw + x), dim5_enc(x, y, z, c, t)) + << store << " plane (z" << z << " c" << c << " t" << t << ") at (" << x << "," << y << ")"; + } + + if (C > 1 && T > 1) // distinct (c,t) planes must differ -> the offset really uses C/T + { + ldr.loadTileFromFile(0, 0, 0, 0, 0, 0); uint32_t p000 = ldr.get_uint32_pixel(0); + ldr.loadTileFromFile(0, 0, 0, 1, 0, 0); uint32_t p010 = ldr.get_uint32_pixel(0); + ldr.loadTileFromFile(0, 0, 0, 0, 1, 0); uint32_t p001 = ldr.get_uint32_pixel(0); + ASSERT_NE(p000, p010) << "channel index ignored"; + ASSERT_NE(p000, p001) << "timeframe index ignored"; + } +} + +// End-to-end through the WIRED volumetric consumer: scan_trivial_wholevolume must +// feed the whole X*Y*Z volume (all Z planes) into the ROI's voxel cloud with the +// correct encoded intensity -- before the wiring it read only plane z=0. +void test_omezarr_wholevolume_consumer(const char* store, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = omezarr_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader ilo; + ASSERT_TRUE(ilo.open(p, fp)) << ds.string(); + + LR vroi; + ASSERT_TRUE(Nyxus::scan_trivial_wholevolume(vroi, ds.string(), ilo)); + + ASSERT_EQ(vroi.raw_pixels_3D.size(), (size_t)X * Y * Z); // all voxels, not just plane 0 (48) + for (const Pixel3& px : vroi.raw_pixels_3D) + ASSERT_EQ((uint32_t)px.inten, dim5_enc((int)px.x, (int)px.y, (int)px.z, 0, 0)) + << "voxel (" << px.x << "," << px.y << "," << px.z << ")"; + ilo.close(); +} + +// Whole-volume assembly through the ImageLoader facade: load_volume() must stack +// all Z-planes (per (channel,timeframe)) into one X*Y*Z buffer. This is the +// foundation that lets the volumetric pipeline consume plane-by-plane OME-Zarr. +void test_omezarr_facade_volume(const char* store, int T, int C, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = omezarr_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; // whole-slide: intensity only + FpImageOptions fp; + ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ds.string(); + ASSERT_EQ(il.get_full_width(), (size_t)X); + ASSERT_EQ(il.get_full_height(), (size_t)Y); + ASSERT_EQ(il.get_full_depth(), (size_t)Z); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + { + ASSERT_TRUE(il.load_volume(c, t)); + const std::vector& vol = il.get_int_volume_buffer(); + ASSERT_EQ(vol.size(), (size_t)X * Y * Z); + for (int z = 0; z < Z; ++z) + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(vol[(size_t)z * X * Y + (size_t)y * X + x], dim5_enc(x, y, z, c, t)) + << store << " vol (x" << x << " y" << y << " z" << z << " c" << c << " t" << t << ")"; + } + il.close(); +} + +// Every one of the 6 legal orderings of {t,c,z} before y,x must read correctly +// (proves the axis-role resolution, not just the default TCZYX). +void test_omezarr_all_5d_permutations() +{ + for (const char* s : { "dim5.ome.zarr", "dim5_tzcyx.ome.zarr", "dim5_ctzyx.ome.zarr", + "dim5_cztyx.ome.zarr", "dim5_ztcyx.ome.zarr", "dim5_zctyx.ome.zarr" }) + { + test_omezarr_addressing(s, 2, 3, 4); + test_raw_omezarr_addressing(s, 2, 3, 4); + if (::testing::Test::HasFatalFailure()) return; + } +} + +// Negative: requesting a Z/C/T plane beyond the array extent must throw, not read +// out-of-bounds / wrong data. dim5.ome.zarr has T=2, C=3, Z=4. +void test_omezarr_out_of_range_throws() +{ + fs::path ds = omezarr_data_path("dim5.ome.zarr"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto ldr = NyxusOmeZarrLoader(1, ds.string()); + auto tile = std::make_shared>(ldr.tileHeight(0) * ldr.tileWidth(0), 0u); + EXPECT_ANY_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 99, 0, 0)); // channel out of range + EXPECT_ANY_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 0, 99, 0)); // timeframe out of range + EXPECT_ANY_THROW(ldr.loadTileFromFile(tile, 0, 0, 99, 0, 0, 0)); // z out of range + + auto raw = RawOmezarrLoader(ds.string()); + EXPECT_ANY_THROW(raw.loadTileFromFile(0, 0, 0, 99, 0, 0)); + EXPECT_ANY_THROW(raw.loadTileFromFile(0, 0, 0, 0, 99, 0)); + EXPECT_ANY_THROW(raw.loadTileFromFile(0, 0, 99, 0, 0, 0)); +} + +// Illegal / adversarial: self-inconsistent metadata must be rejected cleanly +// (throw), not crash. bad_axes_count declares 5 axes for a 3D array (indexing the +// shape by axis role would read OOB); bad_no_xy has axes but none labeled x/y. +void test_omezarr_malformed_throws() +{ + for (const char* s : { "bad_axes_count.ome.zarr", "bad_no_xy.ome.zarr" }) + { + fs::path ds = omezarr_data_path(s); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + EXPECT_ANY_THROW(NyxusOmeZarrLoader(1, ds.string())) << s; + EXPECT_ANY_THROW(RawOmezarrLoader(ds.string())) << s; + } + // a store path that does not exist at all + EXPECT_ANY_THROW(RawOmezarrLoader(omezarr_data_path("does_not_exist.ome.zarr").string())); +} + #endif // OMEZARR_SUPPORT diff --git a/tests/test_tiff_loader_mechanics.h b/tests/test_tiff_loader_mechanics.h index 9ff33d5c1..6f1a6f9c3 100644 --- a/tests/test_tiff_loader_mechanics.h +++ b/tests/test_tiff_loader_mechanics.h @@ -83,7 +83,7 @@ inline void test_uint32_strip_tiff_loader() auto tile = std::make_shared>(tw * th * td); // Single tile at (row,col,layer,level) = (0,0,0,0) covers this small image. - loader.loadTileFromFile(tile, 0, 0, 0, 0); + loader.loadTileFromFile(tile, 0, 0, 0, 0/*channel*/, 0/*timeframe*/, 0); for (uint32_t y = 0; y < h; ++y) for (uint32_t x = 0; x < w; ++x) From 21e9598d860224cfc7c43d0e2326367810f8f425 Mon Sep 17 00:00:00 2001 From: Demian Vladi Date: Fri, 17 Jul 2026 08:24:46 -0700 Subject: [PATCH 3/4] feat(io): per-channel/timeframe addressing, physical-spacing calibration, and Zarr v3/tiled OME-TIFF hardening Step 2 of the IO project: builds multi-dimensional addressing and physical calibration on top of the Step 1 native-read foundation, and hardens it with the fixes and edge-case coverage that came from exercising it against real fixtures. Adds per-channel/timeframe volumetric featurization with a c_index output column, opt-in physical-voxel-spacing calibration with phys_x/y/z/unit output columns, tiled multi-plane OME-TIFF (z,c,t)->IFD support, and native OME-Zarr 0.5 (Zarr v3) reads including zstd. The remaining commits fix correctness issues this surfaced: prescan out-of-bounds reads, CSV output overwriting itself across channels/ timeframes, partial edge tiles, Zarr v3 sharding/blosc/pyramidal reads, non-canonical OME-XML TiffData plane->IFD mapping, a multi-timeframe segmented OME-TIFF crash, and an end-to-end anisotropy-rescaling check. Absorbed commits (oldest to newest): 4d2afda feat(io): per-channel/timeframe volumetric featurization + c_index output column dd01c6e feat(io): opt-in physical-voxel-spacing calibration + phys_x/y/z/unit output columns 5839643 test(io): cover resolve_slide_anisotropy and out-of-range volume reads 9a24427 fix(io): reuse a single-channel mask across all intensity channels 7a19496 fix(io): pass use_physical_spacing to initialize_environment from the 3D Python classes c06a2d7 feat(io): tiled multi-plane OME-TIFF - (z,c,t)->IFD in the tile loaders 9157d34 feat(io): read OME-Zarr 0.5 (Zarr v3), incl. zstd - via z5, no tensorstore 4df18ba fix(3d): whole-volume featurization threw "illegal call" and produced no output 2c72a85 fix(3d): prescan read out-of-bounds garbage and covered only (c0,t0) 6f3ce30 test(python): drop index/calibration columns by name in the 3D IBSI check 6267d16 fix(io): assemble volumes from the whole tile grid, and stream the prescan 4e8766a fix(io): stop CSV output from overwriting itself once per channel/timeframe 66b5d31 test(io): cover partial edge tiles in the volumetric assembly b68bbb5 test(io): verify Zarr v3 sharding (sharding_indexed codec) reads end-to-end 3831675 fix(io): honor OME-XML plane->IFD mapping (non-canonical layouts) fd81d63 test(io): cover Zarr v3 blosc and pyramidal (SubIFD) OME-TIFF reads 302a4b7 fix(io): clamp mask timeframe -- multi-timeframe segmented OME-TIFF crashed 374bbd5 test(io): positive + negative edge coverage across the OME read surfaces 2c916fe test(io): assert anisotropy actually rescales ROI geometry end-to-end --- .gitignore | 5 + CMakeLists.txt | 14 +- src/nyx/abs_tile_loader.h | 8 + src/nyx/cli_option_constants.h | 1 + src/nyx/constants.h | 1 + src/nyx/environment.cpp | 9 + src/nyx/environment.h | 21 + src/nyx/features/3d_intensity.cpp | 4 +- src/nyx/features/3d_intensity.h | 7 +- src/nyx/globals.h | 47 +- src/nyx/grayscale_tiff.h | 75 ++- src/nyx/image_loader.cpp | 45 +- src/nyx/ome/ome_axes.h | 31 +- src/nyx/ome/ome_tiff_meta.cpp | 65 ++ src/nyx/ome/ome_tiff_meta.h | 7 +- src/nyx/omezarr.h | 84 ++- src/nyx/output_2_apache.cpp | 4 +- src/nyx/output_2_buffer.cpp | 39 +- src/nyx/output_2_csv.cpp | 128 ++-- src/nyx/output_writers.cpp | 129 +++- src/nyx/output_writers.h | 15 +- src/nyx/phase1.cpp | 4 +- src/nyx/phase2_3d.cpp | 90 ++- src/nyx/python/new_bindings_py.cpp | 4 +- src/nyx/python/nyxus/nyxus.py | 16 +- src/nyx/raw_format.h | 7 + src/nyx/raw_image_loader.cpp | 25 + src/nyx/raw_image_loader.h | 83 +++ src/nyx/raw_omezarr.h | 76 ++- src/nyx/raw_tiff.h | 83 ++- src/nyx/reduce_trivial_rois.cpp | 2 +- src/nyx/slideprops.cpp | 143 +++-- src/nyx/slideprops.h | 11 + src/nyx/workflow_2d_segmented.cpp | 9 +- src/nyx/workflow_2d_whole.cpp | 10 +- src/nyx/workflow_3d_segmented.cpp | 27 +- src/nyx/workflow_3d_whole.cpp | 87 +-- src/nyx/workflow_pythonapi.cpp | 10 +- tests/data/ometiff/dim3_emptymask.ome.tif | Bin 0 -> 1743 bytes tests/data/ometiff/dim3_mask.ome.tif | Bin 0 -> 1743 bytes tests/data/ometiff/dim4_mask_c2.ome.tif | Bin 0 -> 2859 bytes tests/data/ometiff/dim5_badifd.ome.tif | Bin 0 -> 1634 bytes tests/data/ometiff/dim5_calibrated.ome.tif | Bin 0 -> 7273 bytes tests/data/ometiff/dim5_multitile.ome.tif | Bin 0 -> 13939 bytes tests/data/ometiff/dim5_oddtile.ome.tif | Bin 0 -> 7347 bytes tests/data/ometiff/dim5_pyramid.ome.tif | Bin 0 -> 32223 bytes tests/data/ometiff/dim5_reordered.ome.tif | Bin 0 -> 2302 bytes tests/data/ometiff/dim5_tiled.ome.tif | Bin 0 -> 17702 bytes tests/data/ometiff/gen_ome_tiff.py | 182 ++++++ tests/data/omezarr/dim3_mask.ome.zarr/.zattrs | 36 ++ tests/data/omezarr/dim3_mask.ome.zarr/.zgroup | 3 + .../data/omezarr/dim3_mask.ome.zarr/0/.zarray | 19 + .../data/omezarr/dim3_mask.ome.zarr/0/.zattrs | 1 + tests/data/omezarr/dim3_mask.ome.zarr/0/0.0.0 | Bin 0 -> 192 bytes tests/data/omezarr/dim3_mask.ome.zarr/0/1.0.0 | Bin 0 -> 192 bytes tests/data/omezarr/dim3_mask.ome.zarr/0/2.0.0 | Bin 0 -> 192 bytes tests/data/omezarr/dim3_mask.ome.zarr/0/3.0.0 | Bin 0 -> 192 bytes .../omezarr/dim5_calibrated.ome.zarr/.zattrs | 49 ++ .../omezarr/dim5_calibrated.ome.zarr/.zgroup | 3 + .../dim5_calibrated.ome.zarr/0/.zarray | 23 + .../dim5_calibrated.ome.zarr/0/.zattrs | 1 + .../dim5_calibrated.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 576 bytes .../dim5_calibrated.ome.zarr/0/0.0.1.0.0 | Bin 0 -> 576 bytes .../dim5_calibrated.ome.zarr/0/0.0.2.0.0 | Bin 0 -> 576 bytes .../dim5_calibrated.ome.zarr/0/0.0.3.0.0 | Bin 0 -> 576 bytes .../omezarr/dim5_multichunk.ome.zarr/.zattrs | 46 ++ .../omezarr/dim5_multichunk.ome.zarr/.zgroup | 3 + .../dim5_multichunk.ome.zarr/0/.zarray | 23 + .../dim5_multichunk.ome.zarr/0/.zattrs | 1 + .../dim5_multichunk.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.0.0.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.0.1.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.0.1.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.1.0.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.1.0.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.1.1.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.1.1.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.2.0.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.2.0.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.2.1.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.2.1.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.3.0.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.3.0.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.3.1.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.0.3.1.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.1.0.0.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.1.0.0.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.1.0.1.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.1.0.1.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.1.1.0.0 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.1.1.0.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.1.1.1.0 | 2 + .../dim5_multichunk.ome.zarr/0/0.1.1.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.1.2.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.1.2.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.1.2.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.1.2.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.1.3.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.1.3.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.1.3.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.1.3.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.0.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.0.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.0.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.0.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.1.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.1.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.1.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.1.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.2.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.2.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.2.1.0 | 2 + .../dim5_multichunk.ome.zarr/0/0.2.2.1.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/0.2.3.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.3.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.3.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/0.2.3.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.0.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.0.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.0.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.0.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.1.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.1.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.1.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.1.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.2.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.2.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.2.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.2.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.3.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.3.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.3.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.0.3.1.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/1.1.0.0.0 | 2 + .../dim5_multichunk.ome.zarr/0/1.1.0.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.0.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.0.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.1.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.1.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.1.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.1.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.2.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.2.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.2.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.2.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.3.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.3.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.3.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.1.3.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.0.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.0.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.0.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.0.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.1.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.1.0.1 | Bin 0 -> 24 bytes .../dim5_multichunk.ome.zarr/0/1.2.1.1.0 | 2 + .../dim5_multichunk.ome.zarr/0/1.2.1.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.2.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.2.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.2.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.2.1.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.3.0.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.3.0.1 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.3.1.0 | 1 + .../dim5_multichunk.ome.zarr/0/1.2.3.1.1 | 1 + .../omezarr/dim5_oddchunk.ome.zarr/.zattrs | 46 ++ .../omezarr/dim5_oddchunk.ome.zarr/.zgroup | 3 + .../omezarr/dim5_oddchunk.ome.zarr/0/.zarray | 23 + .../omezarr/dim5_oddchunk.ome.zarr/0/.zattrs | 1 + .../dim5_oddchunk.ome.zarr/0/0.0.0.0.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.0.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.0.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.0.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.1.0.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.1.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.1.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.1.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.2.0.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.2.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.2.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.2.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.3.0.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.3.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.3.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.0.3.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.0.0.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.0.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.0.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.0.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.1.0.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.1.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.1.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.1.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.2.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/0.1.2.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.2.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.2.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.3.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/0.1.3.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.3.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.1.3.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.0.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/0.2.0.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.0.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.0.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.1.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/0.2.1.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.1.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.1.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.2.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/0.2.2.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.2.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.2.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.3.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/0.2.3.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.3.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/0.2.3.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.0.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.0.0.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.0.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.0.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.1.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.0.1.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.1.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.1.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.2.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.0.2.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.2.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.2.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.3.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.0.3.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.3.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.0.3.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.0.0.0 | 2 + .../dim5_oddchunk.ome.zarr/0/1.1.0.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.0.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.0.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.1.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.1.1.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.1.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.1.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.2.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.1.2.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.2.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.2.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.3.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.1.3.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.3.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.1.3.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.0.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.2.0.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.0.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.0.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.1.0.0 | 2 + .../dim5_oddchunk.ome.zarr/0/1.2.1.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.1.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.1.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.2.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.2.2.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.2.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.2.1.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.3.0.0 | 1 + .../dim5_oddchunk.ome.zarr/0/1.2.3.0.1 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.3.1.0 | Bin 0 -> 40 bytes .../dim5_oddchunk.ome.zarr/0/1.2.3.1.1 | Bin 0 -> 40 bytes .../omezarr/dim5_v3.ome.zarr/0/c/0/0/0/0/0 | Bin 0 -> 576 bytes .../omezarr/dim5_v3.ome.zarr/0/c/0/0/1/0/0 | Bin 0 -> 576 bytes .../omezarr/dim5_v3.ome.zarr/0/c/0/0/2/0/0 | Bin 0 -> 576 bytes .../omezarr/dim5_v3.ome.zarr/0/c/0/0/3/0/0 | Bin 0 -> 576 bytes .../data/omezarr/dim5_v3.ome.zarr/0/zarr.json | 41 ++ tests/data/omezarr/dim5_v3.ome.zarr/zarr.json | 52 ++ .../dim5_v3_badcodec.ome.zarr/0/c/0/0/0/0/0 | Bin 0 -> 576 bytes .../dim5_v3_badcodec.ome.zarr/0/c/0/0/1/0/0 | Bin 0 -> 576 bytes .../dim5_v3_badcodec.ome.zarr/0/c/0/0/2/0/0 | Bin 0 -> 576 bytes .../dim5_v3_badcodec.ome.zarr/0/c/0/0/3/0/0 | Bin 0 -> 576 bytes .../dim5_v3_badcodec.ome.zarr/0/zarr.json | 1 + .../dim5_v3_badcodec.ome.zarr/zarr.json | 52 ++ .../dim5_v3_blosc.ome.zarr/0/c/0/0/0/0/0 | Bin 0 -> 257 bytes .../dim5_v3_blosc.ome.zarr/0/c/0/0/1/0/0 | Bin 0 -> 262 bytes .../dim5_v3_blosc.ome.zarr/0/c/0/0/2/0/0 | Bin 0 -> 262 bytes .../dim5_v3_blosc.ome.zarr/0/c/0/0/3/0/0 | Bin 0 -> 262 bytes .../dim5_v3_blosc.ome.zarr/0/zarr.json | 51 ++ .../omezarr/dim5_v3_blosc.ome.zarr/zarr.json | 52 ++ .../dim5_v3_sharded.ome.zarr/0/c/0/0/0/0/0 | Bin 0 -> 644 bytes .../dim5_v3_sharded.ome.zarr/0/c/0/1/0/0/0 | Bin 0 -> 644 bytes .../dim5_v3_sharded.ome.zarr/0/c/0/2/0/0/0 | Bin 0 -> 644 bytes .../dim5_v3_sharded.ome.zarr/0/c/1/0/0/0/0 | Bin 0 -> 644 bytes .../dim5_v3_sharded.ome.zarr/0/c/1/1/0/0/0 | Bin 0 -> 644 bytes .../dim5_v3_sharded.ome.zarr/0/c/1/2/0/0/0 | Bin 0 -> 644 bytes .../dim5_v3_sharded.ome.zarr/0/zarr.json | 67 ++ .../dim5_v3_sharded.ome.zarr/zarr.json | 52 ++ .../dim5_v3_zstd.ome.zarr/0/c/0/0/0/0/0 | Bin 0 -> 73 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/0/1/0/0 | Bin 0 -> 84 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/0/2/0/0 | Bin 0 -> 88 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/0/3/0/0 | Bin 0 -> 93 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/1/0/0/0 | Bin 0 -> 93 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/1/1/0/0 | Bin 0 -> 97 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/1/2/0/0 | Bin 0 -> 83 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/1/3/0/0 | Bin 0 -> 87 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/2/0/0/0 | Bin 0 -> 92 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/2/1/0/0 | Bin 0 -> 93 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/2/2/0/0 | Bin 0 -> 98 bytes .../dim5_v3_zstd.ome.zarr/0/c/0/2/3/0/0 | Bin 0 -> 80 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/0/0/0/0 | Bin 0 -> 85 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/0/1/0/0 | Bin 0 -> 89 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/0/2/0/0 | Bin 0 -> 92 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/0/3/0/0 | Bin 0 -> 94 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/1/0/0/0 | Bin 0 -> 73 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/1/1/0/0 | Bin 0 -> 84 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/1/2/0/0 | Bin 0 -> 89 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/1/3/0/0 | Bin 0 -> 92 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/2/0/0/0 | Bin 0 -> 93 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/2/1/0/0 | Bin 0 -> 97 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/2/2/0/0 | Bin 0 -> 82 bytes .../dim5_v3_zstd.ome.zarr/0/c/1/2/3/0/0 | Bin 0 -> 87 bytes .../omezarr/dim5_v3_zstd.ome.zarr/0/zarr.json | 48 ++ .../omezarr/dim5_v3_zstd.ome.zarr/zarr.json | 52 ++ tests/data/omezarr/gen_dim5.py | 150 +++++ tests/python/test_nyxus.py | 11 +- tests/test_3d_coverage_common.h | 4 +- tests/test_3d_firstorder_pyradiomics.h | 4 +- tests/test_3d_glcm.h | 4 +- tests/test_3d_glcm_pyradiomics.h | 8 +- tests/test_3d_gldm.h | 8 +- tests/test_3d_gldm_pyradiomics.h | 4 +- tests/test_3d_gldzm_ibsi.h | 8 +- tests/test_3d_glrlm.h | 10 +- tests/test_3d_glrlm_pyradiomics.h | 8 +- tests/test_3d_glszm.h | 8 +- tests/test_3d_glszm_pyradiomics.h | 4 +- tests/test_3d_inten.h | 4 +- tests/test_3d_morphology_common.h | 8 +- tests/test_3d_ngldm_regression.h | 8 +- tests/test_3d_ngtdm.h | 8 +- tests/test_3d_ngtdm_pyradiomics.h | 4 +- tests/test_all.cc | 591 ++++++++++++++++++ tests/test_arrow_mechanics.h | 56 +- tests/test_data.h | 18 +- tests/test_ome_meta.h | 72 +++ tests/test_ometiff_mechanics.h | 202 +++++- tests/test_omezarr_mechanics.h | 102 ++- 341 files changed, 3364 insertions(+), 392 deletions(-) create mode 100644 tests/data/ometiff/dim3_emptymask.ome.tif create mode 100644 tests/data/ometiff/dim3_mask.ome.tif create mode 100644 tests/data/ometiff/dim4_mask_c2.ome.tif create mode 100644 tests/data/ometiff/dim5_badifd.ome.tif create mode 100644 tests/data/ometiff/dim5_calibrated.ome.tif create mode 100644 tests/data/ometiff/dim5_multitile.ome.tif create mode 100644 tests/data/ometiff/dim5_oddtile.ome.tif create mode 100644 tests/data/ometiff/dim5_pyramid.ome.tif create mode 100644 tests/data/ometiff/dim5_reordered.ome.tif create mode 100644 tests/data/ometiff/dim5_tiled.ome.tif create mode 100644 tests/data/omezarr/dim3_mask.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim3_mask.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim3_mask.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim3_mask.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim3_mask.ome.zarr/0/0.0.0 create mode 100644 tests/data/omezarr/dim3_mask.ome.zarr/0/1.0.0 create mode 100644 tests/data/omezarr/dim3_mask.ome.zarr/0/2.0.0 create mode 100644 tests/data/omezarr/dim3_mask.ome.zarr/0/3.0.0 create mode 100644 tests/data/omezarr/dim5_calibrated.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5_calibrated.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5_calibrated.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5_calibrated.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5_calibrated.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_calibrated.ome.zarr/0/0.0.1.0.0 create mode 100644 tests/data/omezarr/dim5_calibrated.ome.zarr/0/0.0.2.0.0 create mode 100644 tests/data/omezarr/dim5_calibrated.ome.zarr/0/0.0.3.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.0.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.0.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.0.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.3.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.3.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.3.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.3.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.1.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.0.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.0.1 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.1.0 create mode 100644 tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/.zattrs create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/.zgroup create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/.zarray create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/.zattrs create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.0.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.0.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.0.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.1.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.1.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.1.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.1.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.2.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.2.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.2.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.2.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.3.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.3.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.3.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.3.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.0.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.0.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.0.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.0.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.0.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.0.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.0.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.0.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.1.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.0.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.0.1 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.1.0 create mode 100644 tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.1.1 create mode 100644 tests/data/omezarr/dim5_v3.ome.zarr/0/c/0/0/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3.ome.zarr/0/c/0/0/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3.ome.zarr/0/c/0/0/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3.ome.zarr/0/c/0/0/3/0/0 create mode 100644 tests/data/omezarr/dim5_v3.ome.zarr/0/zarr.json create mode 100644 tests/data/omezarr/dim5_v3.ome.zarr/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/c/0/0/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/c/0/0/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/c/0/0/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/c/0/0/3/0/0 create mode 100644 tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_badcodec.ome.zarr/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/c/0/0/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/c/0/0/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/c/0/0/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/c/0/0/3/0/0 create mode 100644 tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_blosc.ome.zarr/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/0/0/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/0/1/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/0/2/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/1/0/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/1/1/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/1/2/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_sharded.ome.zarr/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/0/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/0/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/0/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/0/3/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/1/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/1/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/1/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/1/3/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/2/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/2/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/2/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/2/3/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/0/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/0/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/0/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/0/3/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/3/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/2/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/2/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/2/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/2/3/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_zstd.ome.zarr/zarr.json diff --git a/.gitignore b/.gitignore index 6ab2335fa..2492cef45 100644 --- a/.gitignore +++ b/.gitignore @@ -566,3 +566,8 @@ cython_debug/ # setuptools-scm generated version file src/nyx/python/nyxus/_version.py .planning/ + +# Feature tables written by the test suites into the tree +NyxusFeatures.arrow +NyxusFeatures.parquet +NyxusFeatures.csv diff --git a/CMakeLists.txt b/CMakeLists.txt index ae0a70163..05cdd1182 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -279,7 +279,19 @@ if(USE_Z5) include_directories (${ZLIB_INCLUDE_DIR}) add_definitions(-DHAVE_ZLIB) # telling NIFTI code of zlib availability, otherwise it'll support only uncompressed NIFTI endif() - + + # zstd: the DEFAULT codec of Zarr v3 (OME-Zarr 0.5). z5's ZstdCompressor is gated by + # WITH_ZSTD; without it, zstd-compressed v3 chunks fail to decode. Optional: if zstd + # isn't found we still read uncompressed / blosc / zlib v3 stores. + find_library(ZSTD_LIBRARY NAMES zstd libzstd) + if(ZSTD_LIBRARY) + add_definitions(-DWITH_ZSTD) + list(APPEND Nyxus_LIBRARIES ${ZSTD_LIBRARY}) + message(STATUS "zstd found (${ZSTD_LIBRARY}) -> Zarr v3 zstd codec enabled") + else() + message(STATUS "zstd not found -> Zarr v3 zstd-compressed chunks will be unsupported") + endif() + if(nlohmann_json_FOUND) include_directories(${nlohmann_json_INCLUDE_DIR}) endif() diff --git a/src/nyx/abs_tile_loader.h b/src/nyx/abs_tile_loader.h index 60d745819..aa6544b22 100644 --- a/src/nyx/abs_tile_loader.h +++ b/src/nyx/abs_tile_loader.h @@ -74,6 +74,14 @@ class AbstractTileLoader return 1; } + /// \brief Physical voxel spacing along X/Y/Z (default 1.0 == uncalibrated). + /// OME loaders return the parsed PhysicalSize*; everything else stays 1.0. + [[nodiscard]] virtual double physicalSizeX() const { return 1.0; } + [[nodiscard]] virtual double physicalSizeY() const { return 1.0; } + [[nodiscard]] virtual double physicalSizeZ() const { return 1.0; } + /// \brief Physical-size unit string (e.g. "micrometer"); empty if unknown. + [[nodiscard]] virtual std::string physicalSizeUnit() const { return std::string(); } + /// \brief Getter to tile Width /// @param level tile's level considered /// \return Tile Width diff --git a/src/nyx/cli_option_constants.h b/src/nyx/cli_option_constants.h index 266e9875e..da2bc546a 100644 --- a/src/nyx/cli_option_constants.h +++ b/src/nyx/cli_option_constants.h @@ -69,6 +69,7 @@ #define clo_AGGREGATE "--aggr" // -> raw_aggregate #define clo_ANNOTATE "--annot" // -> raw_annotate #define clo_ANNOT_SEP "--annotsep" // -> raw_anno_separator +#define clo_USE_PHYSICAL_SPACING "--use-physical-spacing" // -> rawUsePhysicalSpacing (true/false); opt-in OME PhysicalSize* voxel spacing // Valid values of 'OUTPUTTYPE' #define clo_OT_SEPCSV "separatecsv" diff --git a/src/nyx/constants.h b/src/nyx/constants.h index f833602f2..91fc0e3c1 100644 --- a/src/nyx/constants.h +++ b/src/nyx/constants.h @@ -1,4 +1,5 @@ #pragma once constexpr double DEFAULT_T_INDEX = 0.0; +constexpr double DEFAULT_C_INDEX = 0.0; // FIX (IO): default channel index for single-channel / non-OME input constexpr int DEFAULT_NUM_HISTO_BINS = 24; diff --git a/src/nyx/environment.cpp b/src/nyx/environment.cpp index f6f419daf..40c4631b5 100644 --- a/src/nyx/environment.cpp +++ b/src/nyx/environment.cpp @@ -473,6 +473,7 @@ bool Environment::parse_cmdline(int argc, char** argv) || find_string_argument(i, clo_FPIMAGE_MIN, fpimageOptions.raw_min_intensity) || find_string_argument(i, clo_FPIMAGE_MAX, fpimageOptions.raw_max_intensity) || find_string_argument(i, clo_PRESERVE_HU, fpimageOptions.raw_preserve_hu) // CT/HU mode + || find_string_argument(i, clo_USE_PHYSICAL_SPACING, rawUsePhysicalSpacing) // FIX (IO): opt-in physical voxel spacing || find_string_argument(i, clo_RESULTFNAME, nyxus_result_fname) || find_string_argument(i, clo_CLI_DIM, raw_dim) @@ -799,6 +800,14 @@ bool Environment::parse_cmdline(int argc, char** argv) } } + //==== Parse opt-in physical-spacing flag (FIX (IO); default false) + if (!rawUsePhysicalSpacing.empty()) + { + std::string v = rawUsePhysicalSpacing; + std::transform(v.begin(), v.end(), v.begin(), ::tolower); + use_physical_spacing_ = (v == "true" || v == "t" || v == "1" || v == "on" || v == "yes"); + } + //==== Parse exclusive-inclusive timing #ifdef CHECKTIMING if (!rawExclusiveTiming.empty()) diff --git a/src/nyx/environment.h b/src/nyx/environment.h index a07e3352a..d65a76259 100644 --- a/src/nyx/environment.h +++ b/src/nyx/environment.h @@ -2,6 +2,7 @@ #include #include +#include // FIX: csv_paths_written registry (see below) #include #include "arrow_output_stream.h" @@ -80,6 +81,18 @@ class Environment: public BasicEnvironment std::string rawOutpType; //= ""; // Valid values: "separatecsv", "singlecsv", "arrow", "parquet" bool separateCsv; //= true; + // FIX: CSV output paths already written during this run. A slide now yields one row per + // (channel, timeframe), and the CSV sinks are called once per plane -- but in separatecsv + // mode they opened the same path with "w" every time, so each plane truncated the previous + // and only the LAST one survived. The first write to a path truncates and renders the + // header; later writes append. Held per-Environment rather than in a function-static so a + // long-lived Python session starts each run clean instead of appending to a stale file. + std::unordered_set csv_paths_written; + + // True the first time `path` is written in this run (and records it). + bool csv_claim_first_write (const std::string& path) { return csv_paths_written.insert(path).second; } + void reset_csv_output_state() { csv_paths_written.clear(); } + Nyxus::SaveOption saveOption; // x- and y- resolution in pixels per centimeter @@ -87,6 +100,14 @@ class Environment: public BasicEnvironment float xyRes; //= 0.0, float pixelSizeUm; //= 0.0; + // FIX (IO): opt-in physical-voxel-spacing calibration. When set, the 3D pipeline uses + // each slide's OME PhysicalSize* (ratio-normalized) as voxel spacing (anisotropy). + // Off by default -> today's cube-voxel behavior is unchanged. Spacing is still emitted + // as phys_x/y/z + phys_unit output columns regardless of this flag. + std::string rawUsePhysicalSpacing; //= ""; + bool use_physical_spacing_ = false; + bool use_physical_spacing() const { return use_physical_spacing_; } + int get_pixel_distance(); void set_pixel_distance(int pixelDistance); size_t get_ram_limit(); diff --git a/src/nyx/features/3d_intensity.cpp b/src/nyx/features/3d_intensity.cpp index 2777609fb..21d8b4d2b 100644 --- a/src/nyx/features/3d_intensity.cpp +++ b/src/nyx/features/3d_intensity.cpp @@ -367,10 +367,10 @@ void D3_VoxelIntensityFeatures::reduce (size_t start, size_t end, std::vector>& feature_vals); static void reduce (size_t start, size_t end, std::vector* ptrLabels, std::unordered_map * ptrLabelData, const Fsettings & s, const Dataset & ds); - static void extract (LR& r, const Fsettings& s); + // FIX: extract() must carry the Dataset, because calculate() needs it for + // COVERED_IMAGE_INTENSITY_RANGE (slide extrema via ds.dataset_props[r.slide_idx]). + // The old 2-arg extract called the 2-arg calculate, which is a stub that throws + // "illegal call" -- that broke ALL 3D whole-volume featurization (the segmented path + // goes through reduce(), which passes the Dataset, so it never hit this). + static void extract (LR& r, const Fsettings& s, const Dataset& ds); void cleanup_instance(); // list dependencies of this class diff --git a/src/nyx/globals.h b/src/nyx/globals.h index b72938db2..929384eea 100644 --- a/src/nyx/globals.h +++ b/src/nyx/globals.h @@ -38,7 +38,12 @@ namespace Nyxus const char colname_intensity_image[] = "intensity_image", colname_mask_image[] = "mask_image", colname_roi_label[] = "ROI_label", - colname_t_index[] = "t_index"; + colname_t_index[] = "t_index", + colname_c_index[] = "c_index", // FIX (IO): channel index column, mirrors t_index + colname_phys_unit[] = "phys_unit", // FIX (IO): physical-size unit (leading string column) + colname_phys_x[] = "phys_x", // FIX (IO): physical voxel spacing columns (numeric) + colname_phys_y[] = "phys_y", + colname_phys_z[] = "phys_z"; // segmented 2D workflow int processDataset_2D_segmented( @@ -78,18 +83,25 @@ namespace Nyxus bool gatherRoisMetrics(int slide_idx, const std::string& intens_fpath, const std::string& label_fpath, Environment & env, ImageLoader & L); bool gather_wholeslide_metrics(const std::string& intens_fpath, ImageLoader& L, LR& roi); bool gatherRoisMetrics_25D (Environment& env, size_t sidx, const std::string& intens_fpath, const std::string& mask_fpath, const std::vector& z_indices); - bool gatherRoisMetrics_3D (Environment& env, size_t sidx, const std::string& intens_fpath, const std::string& mask_fpath, size_t t_index); + bool gatherRoisMetrics_3D (Environment& env, size_t sidx, const std::string& intens_fpath, const std::string& mask_fpath, size_t t_index, size_t channel); bool processTrivialRois (Environment& env, const std::vector& trivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t memory_limit); bool processTrivialRois_25D (Environment & env, const std::vector& trivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t memory_limit, const std::vector& z_indices); - bool processTrivialRois_3D (Environment & env, size_t sidx, size_t t_index, const std::vector& trivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t memory_limit); + bool processTrivialRois_3D (Environment & env, size_t sidx, size_t t_index, size_t channel, const std::vector& trivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t memory_limit); bool processNontrivialRois (Environment& env, const std::vector& nontrivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath); bool scan_trivial_wholeslide (LR& vroi, const std::string& intens_fpath, ImageLoader& ldr); // reads pixels of whole slide 'intens_fpath' into virtual ROI 'vroi' bool scan_trivial_wholeslide_anisotropic (LR& vroi, const std::string& intens_fpath, ImageLoader& ldr, double aniso_x, double aniso_y); - bool scan_trivial_wholevolume (LR& vroi, const std::string& intens_fpath, ImageLoader& ldr); - bool scan_trivial_wholevolume_anisotropic (LR& vroi, const std::string& intens_fpath, ImageLoader& ldr, double aniso_x, double aniso_y, double aniso_z); + // FIX (IO): resolve the effective 3D voxel spacing for slide `sidx` and whether the + // anisotropic (resampling) scan path should run. Explicit --aniso* wins; else, when + // --use-physical-spacing is on, the slide's OME PhysicalSize* ratio-normalized (min=1). + // Returns false (and ax=ay=az=1) when the grid is effectively isotropic. + bool resolve_slide_anisotropy (const Environment& env, size_t sidx, double& ax, double& ay, double& az); - bool scanTrivialRois_3D (Environment& env, const std::vector& batch_labels, const std::string& intens_fpath, const std::string& label_fpath, size_t t_index); + // FIX (IO): channel/timeframe select which C/T plane the whole-volume read assembles (was pinned to c=0,t=0) + bool scan_trivial_wholevolume (LR& vroi, const std::string& intens_fpath, ImageLoader& ldr, size_t channel, size_t timeframe); + bool scan_trivial_wholevolume_anisotropic (LR& vroi, const std::string& intens_fpath, ImageLoader& ldr, double aniso_x, double aniso_y, double aniso_z, size_t channel, size_t timeframe); + + bool scanTrivialRois_3D (Environment& env, const std::vector& batch_labels, const std::string& intens_fpath, const std::string& label_fpath, size_t t_index, size_t channel); void dump_roi_metrics (const int dim, const std::string& output_dir, const size_t ram_limit, const std::string& seg_fpath, const Uniqueids& uniqueLabels, const Roidata& roiData); void dump_roi_pixels (const int dim, const std::string& output_dir, const std::vector& batch_labels, const std::string& seg_fpath, const Uniqueids& uniqueLabels, const Roidata& roiData); void dump_2d_image_with_halfcontour( @@ -132,30 +144,37 @@ namespace Nyxus // 2 scenarios of saving a result of feature calculation of a label-intensity file pair: saving to a CSV-file and saving to a matrix to be later consumed by a Python endpoint std::string get_feature_output_fname (Environment& env, const std::string& intFpath, const std::string& segFpath); extern const std::vector mandatory_output_columns; - bool save_features_2_csv (Environment & env, const std::string & intFpath, const std::string & segFpath, const std::string & outputDir, size_t t_index, bool need_aggregation); - bool save_features_2_csv_wholeslide (Environment & env, const LR & r, const std::string & ifpath, const std::string & mfpath, const std::string & outdir, size_t t_index); - bool save_features_2_buffer (ResultsCache &results_cache, Environment &env, size_t t_index); + // FIX (IO): thread c_index alongside t_index so the output carries the channel column + bool save_features_2_csv (Environment & env, const std::string & intFpath, const std::string & segFpath, const std::string & outputDir, size_t t_index, size_t c_index, bool need_aggregation); + bool save_features_2_csv_wholeslide (Environment & env, const LR & r, const std::string & ifpath, const std::string & mfpath, const std::string & outdir, size_t t_index, size_t c_index); + bool save_features_2_buffer (ResultsCache &results_cache, Environment &env, size_t t_index, size_t c_index); bool save_features_2_buffer_wholeslide( ResultsCache& rescache, - Environment& env, + Environment& env, const LR& r, const std::string& ifpath, - const std::string& mfpath); - std::tuple> save_features_2_apache_wholeslide (Environment & env, const LR & wsi_roi, const std::string & wsi_path); + const std::string& mfpath, + size_t t_index, + size_t c_index); + std::tuple> save_features_2_apache_wholeslide (Environment & env, const LR & wsi_roi, const std::string & wsi_path, size_t t_index, size_t c_index); std::vector get_feature_values ( Environment & env, const FeatureSet & user_selected_features, const Uniqueids & uniqueLabels, const Roidata & roiData, - const Dataset & dataset); + const Dataset & dataset, + size_t t_index, + size_t c_index); std::vector get_feature_values_roi ( Environment & env, const FeatureSet & fset, const LR& r, const std::string & ifpath, - const std::string & mfpath); + const std::string & mfpath, + size_t t_index, + size_t c_index); std::vector get_header (Environment & env); std::string get_arrow_filename(const std::string& output_path, const std::string& default_filename, const SaveOption& arrow_file_type); diff --git a/src/nyx/grayscale_tiff.h b/src/nyx/grayscale_tiff.h index d6b253714..6c6b0caf0 100644 --- a/src/nyx/grayscale_tiff.h +++ b/src/nyx/grayscale_tiff.h @@ -71,18 +71,38 @@ class NyxusGrayscaleTiffTileLoader : public AbstractTileLoader TIFFGetField(tiff_, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel); TIFFGetField(tiff_, TIFFTAG_BITSPERSAMPLE, &(this->bitsPerSample_)); TIFFGetField(tiff_, TIFFTAG_SAMPLEFORMAT, &(this->sampleFormat_)); + // FIX (IO): SAMPLEFORMAT is optional; TIFF's default is 1 (unsigned int). tifffile + // omits it for uint images, leaving sampleFormat_=0 -> "format not supported". Mirror + // the strip loader's defaulting so tiled uint OME-TIFFs read. + if (sampleFormat_ < 1 || sampleFormat_ > 3) + sampleFormat_ = 1; // Test if the file is greyscale - if (samplesPerPixel != 1) + if (samplesPerPixel != 1) { std::stringstream message; message << "Tile Loader ERROR: The file is not greyscale: SamplesPerPixel = " << samplesPerPixel << "."; throw (std::runtime_error(message.str())); } + + // FIX (IO): a TILED multi-plane OME-TIFF carries its (z,c,t) layout in the IFD-0 + // OME-XML just like the strip variant. Parse it so loadTileFromFile addresses the + // right IFD per (z,c,t); a plain single-plane tiled TIFF stays 2D (fullDepth 1). + fullDepth_ = TIFFNumberOfDirectories(tiff_); + char* desc = nullptr; + if (TIFFGetField(tiff_, TIFFTAG_IMAGEDESCRIPTION, &desc) && desc != nullptr) + { + ome_ = Nyxus::parse_ome_xml(desc); + if (ome_.valid) + { + is_ome_ = true; + fullDepth_ = ome_.sizeZ; // depth is SizeZ, NOT the total IFD count (= Z*C*T) + } + } } - else - { - throw (std::runtime_error("Tile Loader ERROR: The file can not be opened.")); + else + { + throw (std::runtime_error("Tile Loader ERROR: The file can not be opened.")); } } @@ -105,13 +125,28 @@ class NyxusGrayscaleTiffTileLoader : public AbstractTileLoader void loadTileFromFile(std::shared_ptr> tile, size_t indexRowGlobalTile, size_t indexColGlobalTile, - size_t indexLayerGlobalTile, - [[maybe_unused]] size_t indexChannel, // plain 2D TIFF: single channel - [[maybe_unused]] size_t indexTimeframe, // plain 2D TIFF: no time series + size_t indexLayerGlobalTile, // Z plane + size_t indexChannel, // C plane (OME); 0 for plain 2D TIFF + size_t indexTimeframe, // T plane (OME); 0 for plain 2D TIFF size_t level) override { std::string err; + // FIX (IO): select the (z,c,t) plane's IFD before reading the tile. A tiled OME-TIFF + // stores one plane per IFD in DimensionOrder raster order (ifdForPlane); a plain tiled + // TIFF maps layer->IFD directly. Range-guard OME so an out-of-range plane throws instead + // of reading the wrong IFD or zeros. + if (is_ome_ && (indexLayerGlobalTile >= ome_.sizeZ + || indexChannel >= ome_.sizeC || indexTimeframe >= ome_.sizeT)) + { + throw std::runtime_error("NyxusGrayscaleTiffTileLoader: (z,c,t)=(" + std::to_string(indexLayerGlobalTile) + + "," + std::to_string(indexChannel) + "," + std::to_string(indexTimeframe) + ") out of range"); + } + size_t ifd = is_ome_ ? ome_.ifdForPlane(indexLayerGlobalTile, indexChannel, indexTimeframe) + : indexLayerGlobalTile; + if (TIFFSetDirectory(tiff_, (uint16_t)ifd) != 1) + throw std::runtime_error("NyxusGrayscaleTiffTileLoader: TIFFSetDirectory(ifd=" + std::to_string(ifd) + ") failed"); + // Get ahold of the logical (feature extraction facing) tile buffer from its smart pointer std::vector& tileDataVec = *tile; @@ -227,6 +262,16 @@ class NyxusGrayscaleTiffTileLoader : public AbstractTileLoader /// @return 1 [[nodiscard]] size_t numberPyramidLevels() const override { return 1; } + // FIX (IO): a tiled multi-plane OME-TIFF advertises its Z depth + C/T extents + physical + // spacing from the parsed OME-XML, just like the strip variant (1 / plain values otherwise). + [[nodiscard]] size_t fullDepth([[maybe_unused]] size_t level) const override { return fullDepth_; } + [[nodiscard]] size_t numberChannels() const override { return is_ome_ ? ome_.sizeC : 1; } + [[nodiscard]] size_t fullTimestamps([[maybe_unused]] size_t level) const override { return is_ome_ ? ome_.sizeT : 1; } + [[nodiscard]] double physicalSizeX() const override { return is_ome_ ? ome_.physX : 1.0; } + [[nodiscard]] double physicalSizeY() const override { return is_ome_ ? ome_.physY : 1.0; } + [[nodiscard]] double physicalSizeZ() const override { return is_ome_ ? ome_.physZ : 1.0; } + [[nodiscard]] std::string physicalSizeUnit() const override { return is_ome_ ? ome_.unitXY : std::string(); } + private: #if 0 // A faster implementation is available. Keeping this for records. @@ -362,9 +407,13 @@ class NyxusGrayscaleTiffTileLoader : public AbstractTileLoader size_t fullHeight_ = 0, ///< Full height in pixel fullWidth_ = 0, ///< Full width in pixel + fullDepth_ = 1, ///< Full depth (Z) in planes; >1 for multi-plane OME-TIFF tileHeight_ = 0, ///< Tile height tileWidth_ = 0; ///< Tile width + bool is_ome_ = false; ///< true when IFD-0 carries an OME-XML block + Nyxus::OmeAxes ome_; ///< parsed OME dimensions (drives the (z,c,t)->IFD map) + short sampleFormat_ = 0, ///< Sample format as defined by libtiff bitsPerSample_ = 0; ///< Bit Per Sample as defined by libtiff @@ -617,6 +666,18 @@ class NyxusGrayscaleTiffStripLoader : public AbstractTileLoader /// @return Full Depth [[nodiscard]] size_t fullDepth([[maybe_unused]] size_t level) const override { return fullDepth_; } + // FIX: advertise C/T extents from the parsed OME-XML so the volumetric pipeline + // iterates channels/timeframes; non-OME TIFF keeps the single-plane default of 1. + /// @brief Channel (C) extent from OME-XML (1 for plain TIFF) + [[nodiscard]] size_t numberChannels() const override { return is_ome_ ? ome_.sizeC : 1; } + /// @brief Time (T) extent from OME-XML (1 for plain TIFF) + [[nodiscard]] size_t fullTimestamps([[maybe_unused]] size_t level) const override { return is_ome_ ? ome_.sizeT : 1; } + // FIX (IO): physical voxel spacing from OME-XML PhysicalSize* (1.0 for plain TIFF) + [[nodiscard]] double physicalSizeX() const override { return is_ome_ ? ome_.physX : 1.0; } + [[nodiscard]] double physicalSizeY() const override { return is_ome_ ? ome_.physY : 1.0; } + [[nodiscard]] double physicalSizeZ() const override { return is_ome_ ? ome_.physZ : 1.0; } + [[nodiscard]] std::string physicalSizeUnit() const override { return is_ome_ ? ome_.unitXY : std::string(); } + /// @brief Tiff tile width /// @param level Tiff level [not used] /// @return Tile width diff --git a/src/nyx/image_loader.cpp b/src/nyx/image_loader.cpp index e9d8236f0..940e812aa 100644 --- a/src/nyx/image_loader.cpp +++ b/src/nyx/image_loader.cpp @@ -281,17 +281,36 @@ void ImageLoader::assemble_volume (AbstractTileLoader* fl, const size_t frameStride = ltd * th * tw; const size_t frameBase = (ltt > 1) ? timeframe * frameStride : 0; + // FIX: walk the whole tile GRID of each plane, not just tile (0,0). A plane commonly + // spans several tiles/chunks (OME-Zarr chunks are typically 512x512, tiled OME-TIFF + // 256x256), and the old code read tile (0,0) once and then copied fh*fw out of that + // single tile's buffer -- wrong data past the first tile plus an out-of-bounds read. + // Edge tiles are partial, so each tile contributes only its valid [validH x validW]. + const size_t lnth = fl->numberTileHeight (lvl); + const size_t lntw = fl->numberTileWidth (lvl); + for (size_t lz = 0; lz < lntd; ++lz) { - fl->loadTileFromFile (ptr, 0, 0, lz, channel, timeframe, lvl); - for (size_t pz = 0; pz < ltd && (lz * ltd + pz) < fd; ++pz) + for (size_t tr = 0; tr < lnth; ++tr) + for (size_t tc = 0; tc < lntw; ++tc) { - const size_t gz = lz * ltd + pz; - for (size_t row = 0; row < fh; ++row) + fl->loadTileFromFile (ptr, tr, tc, lz, channel, timeframe, lvl); + + const size_t row0 = tr * th, col0 = tc * tw; + if (row0 >= fh || col0 >= fw) + continue; + const size_t validH = (std::min) (th, fh - row0), + validW = (std::min) (tw, fw - col0); + + for (size_t pz = 0; pz < ltd && (lz * ltd + pz) < fd; ++pz) { - const size_t src = frameBase + (pz * th + row) * tw; - const size_t d = gz * sliceSize + row * fw; - std::copy (ptr->begin() + src, ptr->begin() + src + fw, dst.begin() + d); + const size_t gz = lz * ltd + pz; + for (size_t row = 0; row < validH; ++row) + { + const size_t src = frameBase + (pz * th + row) * tw; + const size_t d = gz * sliceSize + (row0 + row) * fw + col0; + std::copy (ptr->begin() + src, ptr->begin() + src + validW, dst.begin() + d); + } } } } @@ -312,7 +331,17 @@ bool ImageLoader::load_volume (size_t channel, size_t timeframe, size_t mask_tim { if (vol_seg_.size() != volSize) vol_seg_.resize (volSize); - assemble_volume (segFL, ptrL, vol_seg_, channel, mask_timeframe); // mask may be on a different frame + // FIX (IO): the mask is usually channel-agnostic (a single-channel segmentation that + // applies to every intensity channel), so index into the mask's OWN channels only when + // it actually has that many; otherwise fall back to channel 0. Without this, featurizing + // intensity channel c>0 against a 1-channel mask read the mask out of range and dropped + // the ROI. Mirrors the mask_timeframe separation for the 1-mask : N-intensity case. + size_t mask_channel = (channel < segFL->numberChannels()) ? channel : 0; + // FIX: clamp the mask TIMEFRAME too (callers already pass 0 for a single-timeframe mask, + // but make load_volume self-defending like it is for the channel -- an unclamped t>0 + // would read the TIFF mask past its last IFD and throw uncaught). Symmetric with above. + size_t mask_tf = (mask_timeframe < segFL->fullTimestamps (lvl)) ? mask_timeframe : 0; + assemble_volume (segFL, ptrL, vol_seg_, mask_channel, mask_tf); } return true; } diff --git a/src/nyx/ome/ome_axes.h b/src/nyx/ome/ome_axes.h index e064ddea8..ca0d09346 100644 --- a/src/nyx/ome/ome_axes.h +++ b/src/nyx/ome/ome_axes.h @@ -120,6 +120,18 @@ namespace Nyxus std::vector levels; // >=1; levels[0] full-res std::vector channelNames; // optional (OME 'Channel'/omero) + // Explicit plane-ordinal -> IFD map from the OME elements, indexed by the + // canonical DimensionOrder ordinal (see canonicalPlaneOrdinal). Empty when the OME-XML + // carries no TiffData (or none that maps same-file planes), in which case the plane + // ordinal IS the IFD (the OME default: planes stored contiguously from IFD 0 in + // DimensionOrder). A writer that offsets the first IFD or reorders planes populates this. + std::vector planeToIfd; + + // True when a names planes in another file (a child): companion / + // multi-file OME-TIFF, which this reader does not resolve. Those planes are left at the + // canonical fallback; the flag lets callers detect the unsupported layout. + bool multiFileTiff = false; + bool valid = false; // set true by a successful parse // Position of a label in storageAxes; -1 if absent. @@ -130,10 +142,10 @@ namespace Nyxus return -1; } - // Map a (z,c,t) plane to its IFD / plane ordinal under the OME - // DimensionOrder (default rasterization: the axes after XY, fastest to - // slowest). E.g. XYZCT -> ifd = z + c*sizeZ + t*sizeZ*sizeC. - std::size_t ifdForPlane(std::size_t z, std::size_t c, std::size_t t) const + // The plane's ordinal under the OME DimensionOrder (default rasterization: the axes + // after XY, fastest to slowest). E.g. XYZCT -> z + c*sizeZ + t*sizeZ*sizeC. This is the + // IFD only when planes are stored contiguously from IFD 0; TiffData can say otherwise. + std::size_t canonicalPlaneOrdinal(std::size_t z, std::size_t c, std::size_t t) const { std::size_t idx = 0, stride = 1; for (char ax : omeDimensionOrder) @@ -147,6 +159,17 @@ namespace Nyxus return idx; } + // Map a (z,c,t) plane to the physical IFD holding its pixels. Honors an explicit + // plane->IFD map when present (writers that start at a non-zero IFD or + // reorder planes); otherwise falls back to the canonical ordinal. + std::size_t ifdForPlane(std::size_t z, std::size_t c, std::size_t t) const + { + std::size_t ord = canonicalPlaneOrdinal(z, c, t); + if (!planeToIfd.empty() && ord < planeToIfd.size()) + return planeToIfd[ord]; + return ord; + } + std::size_t numberPyramidLevels() const { return levels.empty() ? 1 : levels.size(); } bool isVolumetric() const { return sizeZ > 1; } bool isMultiChannel() const { return sizeC > 1; } diff --git a/src/nyx/ome/ome_tiff_meta.cpp b/src/nyx/ome/ome_tiff_meta.cpp index c648fde74..9b9b786f9 100644 --- a/src/nyx/ome/ome_tiff_meta.cpp +++ b/src/nyx/ome/ome_tiff_meta.cpp @@ -97,6 +97,71 @@ namespace Nyxus if (std::count(dord.begin(), dord.end(), c) != 1) perm = false; ax.omeDimensionOrder = perm ? dord : "XYZCT"; + // Explicit plane->IFD mapping from (OME-XML). Absent it, planes are stored + // contiguously from IFD 0 in DimensionOrder (the ifdForPlane default), which is what + // tifffile's single "" also means -- so this only changes + // behavior for writers that start at a non-zero IFD or reorder planes (e.g. bioformats + // per-plane blocks, or a multi-image container). Each block maps PlaneCount consecutive + // DimensionOrder planes starting at (FirstZ,FirstC,FirstT) to consecutive IFDs from IFD. + { + const std::size_t totalPlanes = ax.sizeZ * ax.sizeC * ax.sizeT; + // integer attribute of a start-tag, with default (same guards as `i` above) + auto iat = [](const std::string& tag, const char* n, std::size_t def) -> std::size_t { + std::string v; if (!get_attr(tag, n, v)) return def; + std::size_t r = 0; + auto [ptr, ec] = std::from_chars(v.data(), v.data() + v.size(), r); + return (ec != std::errc() || ptr == v.data()) ? def : r; + }; + + bool anyTiffData = false, sawMultiFile = false; + std::vector map; // identity-initialized on first same-file block + std::size_t tp = 0; + while ((tp = xml.find("' || after == '/')) + { tp += 9; continue; } + std::size_t gt = xml.find('>', tp); + if (gt == std::string::npos) break; + const std::string tag = xml.substr(tp, gt - tp + 1); + anyTiffData = true; + + // A with a child names a plane in ANOTHER file (companion / + // multi-file OME-TIFF) -- unsupported here. Detect it (tag not self-closed AND a + // ) and skip mapping it, leaving those planes at + // the canonical fallback rather than pointing them at a wrong local IFD. + bool selfClosed = (gt > 0 && xml[gt - 1] == '/'); + if (!selfClosed) + { + std::size_t close = xml.find("", gt); + std::size_t uuid = xml.find(" 0) + { map.resize(totalPlanes); for (std::size_t k = 0; k < totalPlanes; ++k) map[k] = k; } + for (std::size_t k = 0; k < count && (startOrd + k) < totalPlanes; ++k) + map[startOrd + k] = ifd0 + k; + tp = gt + 1; + } + ax.multiFileTiff = sawMultiFile; + // Only keep a non-identity map (a plain identity means canonical -> leave empty so + // the common no-/canonical-TiffData path stays allocation-free and obviously canonical). + if (anyTiffData && !map.empty()) + { + bool identity = true; + for (std::size_t k = 0; k < map.size(); ++k) if (map[k] != k) { identity = false; break; } + if (!identity) ax.planeToIfd = std::move(map); + } + } + ax.dtype = pixel_type_from_ome_type(s("Type", "uint16")); if (ax.dtype == PixelType::Unknown) ax.dtype = PixelType::UInt16; ax.bitsPerSample = bits_of(ax.dtype); diff --git a/src/nyx/ome/ome_tiff_meta.h b/src/nyx/ome/ome_tiff_meta.h index dd0316138..d98cef450 100644 --- a/src/nyx/ome/ome_tiff_meta.h +++ b/src/nyx/ome/ome_tiff_meta.h @@ -4,9 +4,10 @@ // element (SizeX/Y/Z/C/T, DimensionOrder, Type, PhysicalSize*, // TimeIncrement) plus names. STL-only. // -// Currently resolves dimensions + calibration + dtype. The TiffData plane->IFD -// map and SubIFDs pyramids are not parsed yet; levels here is a single full-res -// level. +// Resolves dimensions + calibration + dtype + the plane->IFD map +// (same-file; a multi-file block sets OmeAxes::multiFileTiff and is left +// at the canonical fallback). SubIFDs pyramids are not parsed yet; levels here is +// a single full-res level. #include #include "ome_axes.h" diff --git a/src/nyx/omezarr.h b/src/nyx/omezarr.h index 1c96f6952..cc2763f33 100644 --- a/src/nyx/omezarr.h +++ b/src/nyx/omezarr.h @@ -23,10 +23,36 @@ // attribute functionality #include "z5/attributes.hxx" +// FIX (IO): z5 Datatype -> the " -class NyxusOmeZarrLoader : public AbstractTileLoader +class NyxusOmeZarrLoader : public AbstractTileLoader { public: @@ -40,20 +66,25 @@ class NyxusOmeZarrLoader : public AbstractTileLoader { // Open the file zarr_ptr_ = std::make_unique(filePath.c_str()); - nlohmann::json file_attributes, ds_attributes; + nlohmann::json file_attributes; z5::readAttributes(*zarr_ptr_, file_attributes); - // open the highest-resolution dataset (level 0) - ds_name_ = file_attributes["multiscales"][0]["datasets"][0]["path"].get(); - const auto ds_handle = z5::filesystem::handle::Dataset(*zarr_ptr_, ds_name_); - fs::path metadata_path; - auto success = z5::filesystem::metadata_detail::getMetadataPath(ds_handle, metadata_path); - z5::filesystem::metadata_detail::readMetadata(metadata_path, ds_attributes); + // Resolve the level-0 dataset path. NGFF 0.5 (Zarr v3) nests 'multiscales' under an + // "ome" key; NGFF 0.4 (Zarr v2) has it at the attributes root. (parse_ome_zarr handles + // both, but we need the path here to open the dataset.) + const nlohmann::json& ms_root = (file_attributes.contains("ome") && file_attributes["ome"].contains("multiscales")) + ? file_attributes["ome"] : file_attributes; + ds_name_ = ms_root["multiscales"][0]["datasets"][0]["path"].get(); - std::vector level0Shape, chunkShape; - for (const auto& d : ds_attributes["shape"]) level0Shape.push_back(d.get()); - for (const auto& d : ds_attributes["chunks"]) chunkShape.push_back(d.get()); - std::string dtype_str = ds_attributes["dtype"].get(); + // FIX (IO): open the dataset via z5, which auto-detects Zarr v2 (.zarray) vs v3 + // (zarr.json) and handles the v3 chunk-key encoding, codecs (zstd/blosc/...) and + // sharding. Query shape/chunking/dtype from the Dataset object (format-agnostic) + // instead of parsing the v2-only .zarray by hand -- this is what lets the loader read + // BOTH OME-Zarr 0.4 and 0.5. The dataset handle is cached (metadata is immutable). + ds_ = z5::openDataset(*zarr_ptr_, ds_name_); + std::vector level0Shape(ds_->shape().begin(), ds_->shape().end()); + std::vector chunkShape(ds_->defaultChunkShape().begin(), ds_->defaultChunkShape().end()); + std::string dtype_str = zarr_dtype_string_of(ds_->getDtype()); // Resolve axis roles from the NGFF 'axes' metadata instead of assuming a // fixed [T,C,Z,Y,X] order. Falls back to legacy 5D TCZYX if 'axes' is absent. @@ -71,6 +102,13 @@ class NyxusOmeZarrLoader : public AbstractTileLoader iz_ = axes.storageIndexOf('Z'); ic_ = axes.storageIndexOf('C'); it_ = axes.storageIndexOf('T'); n_levels_ = axes.numberPyramidLevels(); + // FIX: advertise the real C/T extents so the pipeline iterates channels and + // timeframes; without this the base class default of 1 kept it on plane (c=0,t=0). + n_channels_ = axes.sizeC; + n_timeframes_ = axes.sizeT; + // FIX (IO): keep the parsed physical voxel spacing for opt-in calibration. + phys_x_ = axes.physX; phys_y_ = axes.physY; phys_z_ = axes.physZ; + phys_unit_ = axes.unitXY; } else { @@ -82,6 +120,10 @@ class NyxusOmeZarrLoader : public AbstractTileLoader ic_ = (n >= 4) ? n - 4 : -1; it_ = (n >= 5) ? n - 5 : -1; n_levels_ = 1; + // FIX: derive C/T extents from the positional axes so the fallback path also + // reports multi-channel / time-series counts (1 when the axis is absent). + n_channels_ = (ic_ >= 0) ? level0Shape[ic_] : 1; + n_timeframes_ = (it_ >= 0) ? level0Shape[it_] : 1; } // X and Y must resolve to real dimensions, else the read would index OOB. if (ix_ < 0 || iy_ < 0 || (size_t)ix_ >= level0Shape.size() || (size_t)iy_ >= level0Shape.size()) @@ -108,11 +150,6 @@ class NyxusOmeZarrLoader : public AbstractTileLoader else if (dtype_str == " [[nodiscard]] short bitsPerSample() const override { return bits_per_sample_; } /// @brief Number of resolution (pyramid) levels declared in multiscales [[nodiscard]] size_t numberPyramidLevels() const override { return n_levels_; } + /// @brief Channel (C) extent resolved from the NGFF axes (1 if no channel axis) + [[nodiscard]] size_t numberChannels() const override { return n_channels_; } + /// @brief Time (T) extent resolved from the NGFF axes (1 if no time axis) + [[nodiscard]] size_t fullTimestamps([[maybe_unused]] size_t level) const override { return n_timeframes_; } + /// @brief Physical voxel spacing from the NGFF coordinateTransformations (1.0 if uncalibrated) + [[nodiscard]] double physicalSizeX() const override { return phys_x_; } + [[nodiscard]] double physicalSizeY() const override { return phys_y_; } + [[nodiscard]] double physicalSizeZ() const override { return phys_z_; } + [[nodiscard]] std::string physicalSizeUnit() const override { return phys_unit_; } private: @@ -261,6 +307,10 @@ class NyxusOmeZarrLoader : public AbstractTileLoader size_t ndim_ = 5; ///< Number of on-disk dimensions (2..5) short bits_per_sample_ = 16; ///< Real bit depth size_t n_levels_ = 1; ///< Pyramid level count + size_t n_channels_ = 1; ///< Channel (C) extent + size_t n_timeframes_ = 1; ///< Time (T) extent + double phys_x_ = 1.0, phys_y_ = 1.0, phys_z_ = 1.0; ///< Physical voxel spacing + std::string phys_unit_; ///< Physical-size unit (e.g. "micrometer") short data_format_ = 0; std::unique_ptr zarr_ptr_; diff --git a/src/nyx/output_2_apache.cpp b/src/nyx/output_2_apache.cpp index 6a263ba67..09bc87652 100644 --- a/src/nyx/output_2_apache.cpp +++ b/src/nyx/output_2_apache.cpp @@ -24,9 +24,9 @@ namespace Nyxus { static std::mutex mx1; - std::tuple> save_features_2_apache_wholeslide (Environment & env, const LR & wsi_roi, const std::string & wsi_path) + std::tuple> save_features_2_apache_wholeslide (Environment & env, const LR & wsi_roi, const std::string & wsi_path, size_t t_index, size_t c_index) { std::lock_guard lg (mx1); - return env.arrow_stream.write_arrow_file (Nyxus::get_feature_values_roi (env, env.theFeatureSet, wsi_roi, wsi_path, "")); + return env.arrow_stream.write_arrow_file (Nyxus::get_feature_values_roi (env, env.theFeatureSet, wsi_roi, wsi_path, "", t_index, c_index)); } } \ No newline at end of file diff --git a/src/nyx/output_2_buffer.cpp b/src/nyx/output_2_buffer.cpp index 619aa6bf3..abece24f5 100644 --- a/src/nyx/output_2_buffer.cpp +++ b/src/nyx/output_2_buffer.cpp @@ -25,12 +25,14 @@ namespace Nyxus bool save_features_2_buffer_wholeslide ( // out - ResultsCache & rescache, + ResultsCache & rescache, // in Environment & env, const LR & r, const std::string& ifpath, - const std::string& mfpath) + const std::string& mfpath, + size_t t_index, + size_t c_index) { std::lock_guard lg (mx1); @@ -44,7 +46,11 @@ namespace Nyxus if (fill_header) { - rescache.add_to_header({ Nyxus::colname_intensity_image, Nyxus::colname_mask_image, Nyxus::colname_roi_label, Nyxus::colname_t_index }); + // FIX (IO): phys_unit is a leading STRING column (kept with intensity/mask so the + // Python "string columns first" split still works); phys_x/y/z are numeric. + rescache.add_to_header({ Nyxus::colname_intensity_image, Nyxus::colname_mask_image, Nyxus::colname_phys_unit, + Nyxus::colname_roi_label, Nyxus::colname_t_index, Nyxus::colname_c_index, + Nyxus::colname_phys_x, Nyxus::colname_phys_y, Nyxus::colname_phys_z }); for (auto& enabdF : F) { @@ -178,11 +184,19 @@ namespace Nyxus rescache.inc_num_rows(); - // - slide info + // - slide info + phys_unit (FIX (IO): 3rd leading string column) + const SlideProps& sp = env.dataset.dataset_props[r.slide_idx]; rescache.add_string (ifpath); rescache.add_string (mfpath); + rescache.add_string (sp.phys_unit); rescache.add_numeric (r.label); - rescache.add_numeric (DEFAULT_T_INDEX); + // FIX (IO): emit the real time/channel indices (was hard-coded DEFAULT_T_INDEX with no channel) + rescache.add_numeric ((double)t_index); + rescache.add_numeric ((double)c_index); + // FIX (IO): physical voxel spacing (numeric columns) + rescache.add_numeric (sp.phys_x); + rescache.add_numeric (sp.phys_y); + rescache.add_numeric (sp.phys_z); // - features for (auto& enabdF : F) @@ -300,7 +314,7 @@ namespace Nyxus } /// @brief Copies ROIs' feature values into a ResultsCache structure that will then shape them as a table - bool save_features_2_buffer (ResultsCache& rescache, Environment & env, size_t t_index) + bool save_features_2_buffer (ResultsCache& rescache, Environment & env, size_t t_index, size_t c_index) { std::vector L{ env.uniqueLabels.begin(), env.uniqueLabels.end() }; std::sort(L.begin(), L.end()); @@ -313,7 +327,11 @@ namespace Nyxus // -- Header if (fill_header) { - rescache.add_to_header({ Nyxus::colname_intensity_image, Nyxus::colname_mask_image, Nyxus::colname_roi_label, Nyxus::colname_t_index }); + // FIX (IO): phys_unit is a leading STRING column (kept with intensity/mask so the + // Python "string columns first" split still works); phys_x/y/z are numeric. + rescache.add_to_header({ Nyxus::colname_intensity_image, Nyxus::colname_mask_image, Nyxus::colname_phys_unit, + Nyxus::colname_roi_label, Nyxus::colname_t_index, Nyxus::colname_c_index, + Nyxus::colname_phys_x, Nyxus::colname_phys_y, Nyxus::colname_phys_z }); for (auto& enabdF : F) { @@ -463,8 +481,13 @@ namespace Nyxus rescache.add_string (intfname); rescache.add_string (segfname); + rescache.add_string (slide.phys_unit); // FIX (IO): 3rd leading string column rescache.add_numeric (l); - rescache.add_numeric (t_index); + rescache.add_numeric ((double)t_index); + rescache.add_numeric ((double)c_index); // FIX (IO): channel index column, mirrors t_index + rescache.add_numeric (slide.phys_x); // FIX (IO): physical voxel spacing (numeric) + rescache.add_numeric (slide.phys_y); + rescache.add_numeric (slide.phys_z); for (auto& enabdF : F) { diff --git a/src/nyx/output_2_csv.cpp b/src/nyx/output_2_csv.cpp index e282c4325..5ad5af9f6 100644 --- a/src/nyx/output_2_csv.cpp +++ b/src/nyx/output_2_csv.cpp @@ -63,6 +63,10 @@ namespace Nyxus head.push_back (Nyxus::colname_intensity_image); head.push_back (Nyxus::colname_mask_image); + // Physical-size unit (FIX (IO): a leading STRING column so the Python buffer's + // "string columns first" reconstruction still holds; the numeric phys_x/y/z go later) + head.push_back (Nyxus::colname_phys_unit); + // Annotation columns if (env.resultOptions.need_annotation()) { @@ -80,6 +84,14 @@ namespace Nyxus // time head.push_back (Nyxus::colname_t_index); + // channel (FIX (IO): mirrors t_index; one column per channel plane) + head.push_back (Nyxus::colname_c_index); + + // physical voxel spacing (FIX (IO): numeric columns) + head.push_back (Nyxus::colname_phys_x); + head.push_back (Nyxus::colname_phys_y); + head.push_back (Nyxus::colname_phys_z); + // Optional columns for (auto& enabdF : F) { @@ -211,11 +223,12 @@ namespace Nyxus bool save_features_2_csv_wholeslide ( Environment & env, - const LR & r, - const std::string & ifpath, - const std::string & mfpath, + const LR & r, + const std::string & ifpath, + const std::string & mfpath, const std::string & outdir, - size_t t_index) + size_t t_index, + size_t c_index) { std::lock_guard lg (mutex1); // Lock the mutex @@ -224,16 +237,18 @@ namespace Nyxus char rvbuf[VAL_BUF_LEN]; // real value buffer large enough to fit a float64 value in range (2.2250738585072014E-308 to 1.79769313486231570e+308) const char rvfmt[] = "%g"; // instead of "%20.12f" which produces a too massive output - static bool mustRenderHeader = true; // In 'singlecsv' scenario this flag flips from 'T' to 'F' when necessary (after the header is rendered) - // Make the file name and write mode std::string fullPath = get_feature_output_fname (env, ifpath, mfpath); VERBOSLVL2 (env.get_verbosity_level(), std::cout << "\t--> " << fullPath << "\n"); - // Single CSV: create or continue? - const char* mode = "w"; - if (!env.separateCsv) - mode = mustRenderHeader ? "w" : "a"; + // FIX: create or continue, per PATH. This sink is called once per (channel, timeframe) + // plane, but separatecsv derives one path per slide and used to open it "w" every time, + // so each plane truncated the previous and the file kept only the last one. Truncate and + // render the header on the first write to a path, append after that -- which is what the + // t_index/c_index columns are for, and matches the Arrow and buffer sinks. Replaces a + // function-static 'mustRenderHeader' that could only express this for singlecsv. + const bool mustRenderHeader = env.csv_claim_first_write (fullPath); + const char* mode = mustRenderHeader ? "w" : "a"; // Open it FILE* fp = nullptr; @@ -272,10 +287,7 @@ namespace Nyxus auto head_string = ssHead.str(); fprintf(fp, "%s\n", head_string.c_str()); - - // Prevent rendering the header again for another image's portion of labels - if (env.separateCsv == false) - mustRenderHeader = false; + // (the path is already recorded, so later writes to it append without a header) } // ********** Values @@ -288,10 +300,16 @@ namespace Nyxus ssVals << std::fixed; // slide info - ssVals << ifpath << "," << mfpath; - - // ROI and time - ssVals << "," << r.label << "," << t_index; + // FIX (IO): physical calibration of this slide (phys_unit is a leading string column, + // phys_x/y/z are numeric; 1.0/"" when uncalibrated). r.slide_idx set for the vROI. + const SlideProps& sp = env.dataset.dataset_props[r.slide_idx]; + ssVals << ifpath << "," << mfpath << "," << sp.phys_unit; + + // ROI, time and channel + ssVals << "," << r.label << "," << t_index << "," << c_index; + + // physical voxel spacing (numeric) + ssVals << "," << sp.phys_x << "," << sp.phys_y << "," << sp.phys_z; // features for (auto& enabdF : F) @@ -418,10 +436,11 @@ namespace Nyxus // Saves the result of image scanning and feature calculation. Must be called after the reduction phase. bool save_features_2_csv ( Environment & env, - const std::string& intFpath, - const std::string& segFpath, + const std::string& intFpath, + const std::string& segFpath, const std::string& outputDir, size_t t_index, + size_t c_index, bool need_aggregation) { // Non-exotic formatting for compatibility with the buffer output (Python API, Apache) @@ -433,16 +452,15 @@ namespace Nyxus std::vector L{ env.uniqueLabels.begin(), env.uniqueLabels.end() }; std::sort(L.begin(), L.end()); - static bool mustRenderHeader = true; // In 'singlecsv' scenario this flag flips from 'T' to 'F' when necessary (after the header is rendered) - // Make the file name and write mode std::string fullPath = get_feature_output_fname (env, intFpath, segFpath); VERBOSLVL2 (env.get_verbosity_level(), std::cout << "\t--> " << fullPath << "\n"); - // Single CSV: create or continue? - const char* mode = "w"; - if (!env.separateCsv) - mode = mustRenderHeader ? "w" : "a"; + // FIX: create or continue, per PATH -- see the whole-slide sink above. The 3D segmented + // workflow also calls this once per (channel, timeframe), and separatecsv truncated the + // file on every call, keeping only the last plane's ROI rows. + const bool mustRenderHeader = env.csv_claim_first_write (fullPath); + const char* mode = mustRenderHeader ? "w" : "a"; // Open it FILE* fp = nullptr; @@ -482,20 +500,17 @@ namespace Nyxus auto head_string = ssHead.str(); fprintf(fp, "%s\n", head_string.c_str()); - - // Prevent rendering the header again for another image's portion of labels - if (env.separateCsv == false) - mustRenderHeader = false; + // (the path is already recorded, so later writes to it append without a header) } if (need_aggregation) { - auto allres = Nyxus::get_feature_values (env, env.theFeatureSet, env.uniqueLabels, env.roiData, env.dataset); // shape: td::vector, int, std::vector>> + auto allres = Nyxus::get_feature_values (env, env.theFeatureSet, env.uniqueLabels, env.roiData, env.dataset, t_index, c_index); // shape: vector, label, t_index, c_index, vector>> if (allres.size()) { // aggregate const auto& tup0 = allres[0]; - const auto& v0 = std::get<3>(tup0); + const auto& v0 = std::get(tup0); // FIX (IO): FBEGIN shifted 3->4 after c_index; was hard-coded std::get<3> auto n_feats = v0.size(); std::vector a (n_feats); @@ -507,7 +522,7 @@ namespace Nyxus // we ned to add Skipping blacklisted ROI // ... - const auto& v = std::get<3>(tup); + const auto& v = std::get(tup); // FIX (IO): FBEGIN shifted 3->4 after c_index for (size_t i = 0; i < n_feats; i++) { double x = v[i], @@ -521,11 +536,11 @@ namespace Nyxus } } - // send the aggregate as ROI #-1 to output + // send the aggregate as ROI #-1 to output const std::vector & fnames = std::get<0>(tup0); std::stringstream ssVals; - // ... slide/mask file info - ssVals << fnames[0] << "," << fnames[1]; + // ... slide/mask file info + phys_unit (FIX (IO): fnames[2] is the leading unit string) + ssVals << fnames[0] << "," << fnames[1] << "," << fnames[2]; // ... annotation info auto lab0 = std::get<1>(tup0); // annotation info is per slide, so OK to grab it from the 1st ROI LR& r0 = env.roiData [lab0]; @@ -534,6 +549,11 @@ namespace Nyxus ssVals << "," << a; // ... ROI id ssVals << "," << -1; + // ... time and channel (FIX (IO): the aggregate row previously omitted these, + // misaligning it against the header; emit both so columns line up) + ssVals << "," << t_index << "," << c_index; + // ... physical voxel spacing (FIX (IO): numeric columns, from the 1st ROI's tuple) + ssVals << "," << std::get(tup0) << "," << std::get(tup0) << "," << std::get(tup0); // ... aggregated feature values for (size_t i = 0; i < n_feats; i++) ssVals << "," << a[i]; @@ -560,7 +580,8 @@ namespace Nyxus const SlideProps& sli = env.dataset.dataset_props [r.slide_idx]; fs::path pseg (sli.fname_seg), pint (sli.fname_int); - ssVals << pint.filename() << "," << pseg.filename(); + // FIX (IO): phys_unit is a leading string column (after the file names) + ssVals << pint.filename() << "," << pseg.filename() << "," << sli.phys_unit; // annotation if (env.resultOptions.need_annotation()) @@ -569,8 +590,11 @@ namespace Nyxus ssVals << "," << a; } - // ROI label and time - ssVals << "," << l << "," << t_index; + // ROI label, time and channel + ssVals << "," << l << "," << t_index << "," << c_index; + + // physical voxel spacing (FIX (IO): numeric columns) + ssVals << "," << sli.phys_x << "," << sli.phys_y << "," << sli.phys_z; for (auto& enabdF : F) { @@ -761,7 +785,9 @@ namespace Nyxus const FeatureSet & fset, const LR & r, const std::string & ifpath, - const std::string & mfpath) + const std::string & mfpath, + size_t t_index, + size_t c_index) { std::vector features; @@ -878,13 +904,21 @@ namespace Nyxus fvals.push_back(vv[0]); } - // other columns + // physical calibration of this slide (FIX (IO)) + const SlideProps& sp = env.dataset.dataset_props[r.slide_idx]; + + // other columns (textcols[2] = phys_unit, a leading string column) std::vector textcols; textcols.push_back ((fs::path(ifpath)).filename().string()); textcols.push_back (""); + textcols.push_back (sp.phys_unit); int roilabl = r.label; // whole-slide roi # - features.push_back (std::make_tuple(textcols, roilabl, -999.88, fvals)); + // FIX (IO): emit the real time/channel indices (was hard-coded -999.88 for time + // and had no channel field) + physical voxel spacing, so the Apache/buffer whole-slide + // row is addressable by (t,c) and carries calibration. + features.push_back (std::make_tuple(textcols, roilabl, (double)t_index, (double)c_index, + sp.phys_x, sp.phys_y, sp.phys_z, fvals)); return features; } @@ -894,7 +928,9 @@ namespace Nyxus const FeatureSet & fset, const Uniqueids & uniqueLabels, const Roidata & roiData, - const Dataset & dataset) + const Dataset & dataset, + size_t t_index, + size_t c_index) { std::vector features; @@ -923,6 +959,7 @@ namespace Nyxus std::vector filenames; filenames.push_back(pint.filename().string()); filenames.push_back(pseg.filename().string()); + filenames.push_back(sli.phys_unit); // FIX (IO): phys_unit leading string column for (auto& enabdF : F) { @@ -1031,7 +1068,10 @@ namespace Nyxus feature_values.push_back(vv[0]); } - features.push_back (std::make_tuple(filenames, l, DEFAULT_T_INDEX, feature_values)); + // FIX (IO): thread the real time/channel indices instead of the hard-coded + // DEFAULT_T_INDEX (which pinned every Apache/Parquet row to t=0,c=0). + features.push_back (std::make_tuple(filenames, l, (double)t_index, (double)c_index, + sli.phys_x, sli.phys_y, sli.phys_z, feature_values)); } return features; diff --git a/src/nyx/output_writers.cpp b/src/nyx/output_writers.cpp index 39c5c6508..c4ace62a0 100644 --- a/src/nyx/output_writers.cpp +++ b/src/nyx/output_writers.cpp @@ -18,11 +18,14 @@ arrow::Status ParquetWriter::setup(const std::vector& header) { std::vector> fields; + // FIX (IO): schema is [intensity, mask, phys_unit] (utf8) + ROI_label (int32) + the rest + // (t_index, c_index, phys_x/y/z, features) as float64 - matches get_header's column order. fields.push_back(arrow::field(header[0], arrow::utf8())); fields.push_back(arrow::field(header[1], arrow::utf8())); - fields.push_back(arrow::field(header[2], arrow::int32())); + fields.push_back(arrow::field(header[2], arrow::utf8())); + fields.push_back(arrow::field(header[3], arrow::int32())); - for (int i = 3; i < header.size(); ++i) + for (int i = 4; i < header.size(); ++i) { fields.push_back(arrow::field(header[i], arrow::float64())); } @@ -114,6 +117,19 @@ arrow::Status ParquetWriter::write (const std::vector& features) } A.push_back (A_s); + + //cccccccc phys_unit (FIX (IO): 3rd leading string column) + b.Reset(); + std::shared_ptr A_pu; + for (int i = 0; i < num_rows; ++i) { + append_status = b.Append (std::get(features[i])[2]); + if (!append_status.ok()) + return append_status; + } + append_status = b.Finish (&A_pu); + if (!append_status.ok()) + return append_status; + A.push_back (A_pu); } //cccccccc ROI label @@ -160,9 +176,51 @@ arrow::Status ParquetWriter::write (const std::vector& features) A.push_back (A_t); } + //cccccccc channel (FIX (IO): mirrors the time column so multi-channel input is addressable) + { + arrow::DoubleBuilder b; + std::shared_ptr A_c; + for (int i = 0; i < num_rows; ++i) + { + append_status = b.Append (std::get(features[i])); + if (!append_status.ok()) { + // Handle read error + return append_status; + } + } + + append_status = b.Finish (&A_c); + if (!append_status.ok()) + { + // Handle read error + return append_status; + } + A.push_back (A_c); + } + + //cccccccc physical voxel spacing (FIX (IO): phys_x/y/z numeric columns) + for (int axis = FTABLE_PXPOS; axis <= FTABLE_PZPOS; ++axis) + { + arrow::DoubleBuilder b; + std::shared_ptr A_ps; + for (int i = 0; i < num_rows; ++i) + { + double v = (axis == FTABLE_PXPOS) ? std::get(features[i]) + : (axis == FTABLE_PYPOS) ? std::get(features[i]) + : std::get(features[i]); + append_status = b.Append (v); + if (!append_status.ok()) + return append_status; + } + append_status = b.Finish (&A_ps); + if (!append_status.ok()) + return append_status; + A.push_back (A_ps); + } + //cccccccc features { - // construct columns for each feature + // construct columns for each feature for (int j = 0; j < std::get(features[0]).size(); ++j) { arrow::DoubleBuilder b; @@ -225,11 +283,13 @@ arrow::Status ArrowIPCWriter::setup(const std::vector& header) { std::vector> fields; - fields.push_back(arrow::field("intensity_image", arrow::utf8())); - fields.push_back(arrow::field("mask_image", arrow::utf8())); - fields.push_back(arrow::field("ROI_label", arrow::int32())); + // FIX (IO): [intensity, mask, phys_unit] (utf8) + ROI_label (int32) + float64 rest + fields.push_back(arrow::field(header[0], arrow::utf8())); + fields.push_back(arrow::field(header[1], arrow::utf8())); + fields.push_back(arrow::field(header[2], arrow::utf8())); + fields.push_back(arrow::field(header[3], arrow::int32())); - for (int i = 3; i < header.size(); ++i) + for (int i = 4; i < header.size(); ++i) { fields.push_back(arrow::field(header[i], arrow::float64())); } @@ -306,6 +366,19 @@ arrow::Status ArrowIPCWriter::write (const std::vector& features) } A.push_back (A_s); + + //cccccccc phys_unit (FIX (IO): 3rd leading string column) + b.Reset(); + std::shared_ptr A_pu; + for (int i = 0; i < num_rows; ++i) { + append_status = b.Append (std::get(features[i])[2]); + if (!append_status.ok()) + return append_status; + } + append_status = b.Finish (&A_pu); + if (!append_status.ok()) + return append_status; + A.push_back (A_pu); } //cccccccc ROI label @@ -351,9 +424,49 @@ arrow::Status ArrowIPCWriter::write (const std::vector& features) A.push_back (A_t); } + //cccccccc channel (FIX (IO): mirrors the time column so multi-channel input is addressable) + { + arrow::DoubleBuilder b; + std::shared_ptr A_c; + for (int i = 0; i < num_rows; ++i) { + append_status = b.Append (std::get(features[i])); + if (!append_status.ok()) { + // Handle read error + return append_status; + } + } + + append_status = b.Finish (&A_c); + if (!append_status.ok()) { + // Handle read error + return append_status; + } + A.push_back (A_c); + } + + //cccccccc physical voxel spacing (FIX (IO): phys_x/y/z numeric columns) + for (int axis = FTABLE_PXPOS; axis <= FTABLE_PZPOS; ++axis) + { + arrow::DoubleBuilder b; + std::shared_ptr A_ps; + for (int i = 0; i < num_rows; ++i) + { + double v = (axis == FTABLE_PXPOS) ? std::get(features[i]) + : (axis == FTABLE_PYPOS) ? std::get(features[i]) + : std::get(features[i]); + append_status = b.Append (v); + if (!append_status.ok()) + return append_status; + } + append_status = b.Finish (&A_ps); + if (!append_status.ok()) + return append_status; + A.push_back (A_ps); + } + //cccccccc feature values { - // construct columns for each feature + // construct columns for each feature for (int j = 0; j < std::get(features[0]).size(); ++j) { arrow::DoubleBuilder b; diff --git a/src/nyx/output_writers.h b/src/nyx/output_writers.h index 53555ce71..fc4d6765f 100644 --- a/src/nyx/output_writers.h +++ b/src/nyx/output_writers.h @@ -1,10 +1,21 @@ #pragma once -#define FTABLE_RECORD std::tuple,int,double,std::vector> +// FIX (IO): the record carries the index/calibration columns before the feature block: +// [0] text columns = {intensity_image, mask_image, phys_unit} (all leading strings, +// so the Python buffer's "strings first" reconstruction still holds) +// [1] ROI label (int) +// [2] t_index, [3] c_index, [4..6] phys_x/phys_y/phys_z (physical voxel spacing) +// [7..] feature values +// c_index was added for per-channel OME output; phys_* for physical calibration. +#define FTABLE_RECORD std::tuple,int,double,double,double,double,double,std::vector> #define FTABLE_INTSEG 0 #define FTABLE_ROILBL 1 #define FTABLE_TIMEPOS 2 -#define FTABLE_FBEGIN 3 +#define FTABLE_CPOS 3 +#define FTABLE_PXPOS 4 +#define FTABLE_PYPOS 5 +#define FTABLE_PZPOS 6 +#define FTABLE_FBEGIN 7 #define FTABLE_SAFENAN -0.0 #ifdef USE_ARROW diff --git a/src/nyx/phase1.cpp b/src/nyx/phase1.cpp index 00dded8bb..724c2aeca 100644 --- a/src/nyx/phase1.cpp +++ b/src/nyx/phase1.cpp @@ -245,7 +245,7 @@ namespace Nyxus // segmented 3D case (true volumetric images e.g. .nii, .nii.gz, .dcm, etc) // prerequisite: 'env.theImLoader' needs to be pre-opened ! // - bool gatherRoisMetrics_3D (Environment& env, size_t sidx, const std::string& intens_fpath, const std::string& mask_fpath, size_t t_index) + bool gatherRoisMetrics_3D (Environment& env, size_t sidx, const std::string& intens_fpath, const std::string& mask_fpath, size_t t_index, size_t channel) { SlideProps & sprp = env.dataset.dataset_props [sidx]; if (! env.theImLoader.open(sprp, env.fpimageOptions)) @@ -284,7 +284,7 @@ namespace Nyxus // real 3D volume here too — Phase-1 ROI metrics must see the same voxels the // Phase-2 scan does. Mask on frame t_index (1:1) or frame 0 (1 mask : N int). const size_t maskFrame = (nVoxI == nVoxM) ? t_index : 0; - bool ok = env.theImLoader.load_volume (0/*channel*/, t_index, maskFrame); + bool ok = env.theImLoader.load_volume (channel, t_index, maskFrame); if (!ok) { std::string erm = "Error fetching tile (0,0) from I:" + intens_fpath + " M:" + mask_fpath; diff --git a/src/nyx/phase2_3d.cpp b/src/nyx/phase2_3d.cpp index d942a8511..178bc8a74 100644 --- a/src/nyx/phase2_3d.cpp +++ b/src/nyx/phase2_3d.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -13,15 +14,51 @@ #include "environment.h" #include "globals.h" +#include "helpers/helpers.h" // FIX (IO): Nyxus::near_eq for the physical-spacing resolver #include "helpers/fsystem.h" #include "helpers/timing.h" namespace Nyxus { + // FIX (IO): resolve the effective 3D voxel spacing for slide `sidx`. Explicit --aniso* + // (anisoOptions.customized()) always wins. Otherwise, when --use-physical-spacing is on, + // use the slide's OME PhysicalSize* ratio-normalized so the smallest axis == 1 (Nyxus's + // anisotropic path resamples by the multiplier, so ratios - not absolute units - are what + // correct for non-cubic voxels). Returns false + (1,1,1) when the grid is isotropic. + bool resolve_slide_anisotropy (const Environment& env, size_t sidx, double& ax, double& ay, double& az) + { + ax = ay = az = 1.0; + + if (env.anisoOptions.customized()) + { + ax = env.anisoOptions.get_aniso_x(); + ay = env.anisoOptions.get_aniso_y(); + az = env.anisoOptions.get_aniso_z(); + return true; + } + + if (env.use_physical_spacing() && sidx < env.dataset.dataset_props.size()) + { + const SlideProps& p = env.dataset.dataset_props[sidx]; + double sx = p.phys_x, sy = p.phys_y, sz = p.phys_z; + double mn = std::min(sx, std::min(sy, sz)); + if (mn > 0.0) + { + ax = sx / mn; ay = sy / mn; az = sz / mn; + // only take the anisotropic path if the voxels are actually non-cubic + if (! (Nyxus::near_eq(ax, 1.0) && Nyxus::near_eq(ay, 1.0) && Nyxus::near_eq(az, 1.0))) + return true; + } + ax = ay = az = 1.0; + } + + return false; + } + // // Loads ROI voxels into voxel clouds // - bool scanTrivialRois_3D (Environment & env, const std::vector& batch_labels, const std::string& intens_fpath, const std::string& label_fpath, size_t t_index) + bool scanTrivialRois_3D (Environment & env, const std::vector& batch_labels, const std::string& intens_fpath, const std::string& label_fpath, size_t t_index, size_t channel) { // Sort the batch's labels to enable binary searching in it std::vector whiteList = batch_labels; @@ -89,7 +126,7 @@ namespace Nyxus // read a real 3D volume. The mask sits on frame t_index (1:1) or frame 0 (the // 1-mask : N-intensity case); the intensity sits on frame t_index. const size_t maskFrame = (timeI == timeM) ? t_index : 0; - bool ok = env.theImLoader.load_volume (0/*channel*/, t_index, maskFrame); + bool ok = env.theImLoader.load_volume (channel, t_index, maskFrame); if (!ok) { std::string erm = "Error fetching segmented data from " + intens_fpath + "(I) " + label_fpath + "(M)"; @@ -280,6 +317,7 @@ namespace Nyxus const std::string& intens_fpath, const std::string& label_fpath, size_t t_index, + size_t channel, double aniso_x, double aniso_y, double aniso_z) @@ -337,7 +375,7 @@ namespace Nyxus // loops Z), so plane-by-plane loaders (OME-Zarr, multi-page/OME-TIFF) read a // real 3D volume here too. Mask on frame t_index (1:1) or frame 0 (1 mask : N). const size_t maskFrame = (timeI == timeM) ? t_index : 0; - if (!env.theImLoader.load_volume (0/*channel*/, t_index, maskFrame)) + if (!env.theImLoader.load_volume (channel, t_index, maskFrame)) { std::string erm = "Error fetching data from file pair " + intens_fpath + "(I) " + label_fpath + "(M)"; #ifdef WITH_PYTHON_H @@ -433,7 +471,9 @@ namespace Nyxus bool scan_trivial_wholevolume ( LR& vroi, const std::string& intens_fpath, - ImageLoader& ilo) + ImageLoader& ilo, + size_t channel, + size_t timeframe) { int lvl = 0, // Pyramid level lyr = 0; // Layer @@ -446,10 +486,10 @@ namespace Nyxus sliceSize = fullwidth * fullheight, nVox = sliceSize * fullD; - // FIX (IO): assemble the whole X*Y*Z volume (load_volume loops Z) so plane-by- - // plane loaders (OME-Zarr, multi-page/OME-TIFF) read the full volume, not just - // plane z=0. Whole-slide is single-channel, single-timeframe. - if (!ilo.load_volume(0/*channel*/, 0/*timeframe*/, 0/*mask*/)) + // FIX (IO): assemble the whole X*Y*Z volume (load_volume loops Z) for the requested + // (channel, timeframe) so plane-by-plane loaders (OME-Zarr, multi-page/OME-TIFF) read + // the full volume of THIS C/T plane, not just plane z=0 of (c=0,t=0). No mask in WSI. + if (!ilo.load_volume(channel, timeframe, timeframe)) { #ifdef WITH_PYTHON_H throw "Error fetching tile"; @@ -495,7 +535,9 @@ namespace Nyxus ImageLoader& ilo, double aniso_x, double aniso_y, - double aniso_z) + double aniso_z, + size_t channel, + size_t timeframe) { int lvl = 0, // Pyramid level lyr = 0; // Layer @@ -511,9 +553,9 @@ namespace Nyxus vw = (size_t) (double(fullW) * aniso_x), vd = (size_t) (double(fullD) * aniso_z); - // FIX (IO): assemble the whole X*Y*Z volume (load_volume loops Z) so plane-by- - // plane loaders read the full volume, not just plane z=0. - if (! ilo.load_volume(0/*channel*/, 0/*timeframe*/, 0/*mask*/)) + // FIX (IO): assemble the whole X*Y*Z volume (load_volume loops Z) for the requested + // (channel, timeframe) so plane-by-plane loaders read the full volume of this C/T plane. + if (! ilo.load_volume(channel, timeframe, timeframe)) { #ifdef WITH_PYTHON_H throw "Error loading volume data"; @@ -630,7 +672,7 @@ namespace Nyxus } - bool processTrivialRois_3D (Environment & env, size_t sidx, size_t t_index, const std::vector& trivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t memory_limit) + bool processTrivialRois_3D (Environment & env, size_t sidx, size_t t_index, size_t channel, const std::vector& trivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t memory_limit) { std::vector Pending; size_t batchDemand = 0; @@ -661,16 +703,15 @@ namespace Nyxus std::cout << ">>> (ROI labels " << Pending[0] << " ... " << Pending[Pending.size() - 1] << ")\n"; ); - if (env.anisoOptions.customized() == false) + // FIX (IO): --aniso* (explicit) or opt-in OME physical spacing selects the anisotropic path + double ax, ay, az; + if (! resolve_slide_anisotropy (env, sidx, ax, ay, az)) { - scanTrivialRois_3D (env, Pending, intens_fpath, label_fpath, t_index); + scanTrivialRois_3D (env, Pending, intens_fpath, label_fpath, t_index, channel); } else { - double ax = env.anisoOptions.get_aniso_x(), - ay = env.anisoOptions.get_aniso_y(), - az = env.anisoOptions.get_aniso_z(); - scanTrivialRois_3D_anisotropic (env, Pending, intens_fpath, label_fpath, t_index, ax, ay, az); + scanTrivialRois_3D_anisotropic (env, Pending, intens_fpath, label_fpath, t_index, channel, ax, ay, az); // rescan and update ROI's AABB for (auto lbl : Pending) @@ -727,16 +768,15 @@ namespace Nyxus std::cout << ">>> (labels " << Pending[0] << " ... " << Pending[Pending.size() - 1] << ")\n"; ); - if (env.anisoOptions.customized() == false) + // FIX (IO): --aniso* (explicit) or opt-in OME physical spacing selects the anisotropic path + double ax, ay, az; + if (! resolve_slide_anisotropy (env, sidx, ax, ay, az)) { - scanTrivialRois_3D (env, Pending, intens_fpath, label_fpath, t_index); + scanTrivialRois_3D (env, Pending, intens_fpath, label_fpath, t_index, channel); } else { - double ax = env.anisoOptions.get_aniso_x(), - ay = env.anisoOptions.get_aniso_y(), - az = env.anisoOptions.get_aniso_z(); - scanTrivialRois_3D_anisotropic (env, Pending, intens_fpath, label_fpath, t_index, ax, ay, az); + scanTrivialRois_3D_anisotropic (env, Pending, intens_fpath, label_fpath, t_index, channel, ax, ay, az); // rescan and update ROI's AABB for (auto lbl : Pending) diff --git a/src/nyx/python/new_bindings_py.cpp b/src/nyx/python/new_bindings_py.cpp index c23c29ce8..e55822435 100644 --- a/src/nyx/python/new_bindings_py.cpp +++ b/src/nyx/python/new_bindings_py.cpp @@ -81,7 +81,8 @@ void initialize_environment( float aniso_y, float aniso_z, bool merge_labels = false, - bool preserve_hu = false) // CT/HU: preserve absolute Hounsfield intensities + bool preserve_hu = false, // CT/HU: preserve absolute Hounsfield intensities + bool use_physical_spacing = false) // FIX (IO): opt-in OME PhysicalSize* voxel spacing (3D) { Environment & theEnvironment = Nyxus::findenv (instid); @@ -103,6 +104,7 @@ void initialize_environment( theEnvironment.fpimageOptions.set_min_intensity(min_intensity); theEnvironment.fpimageOptions.set_max_intensity(max_intensity); theEnvironment.fpimageOptions.set_preserve_hu(preserve_hu); + theEnvironment.use_physical_spacing_ = use_physical_spacing; // FIX (IO): opt-in physical spacing // Throws exception if invalid feature is passed theEnvironment.expand_featuregroups(); diff --git a/src/nyx/python/nyxus/nyxus.py b/src/nyx/python/nyxus/nyxus.py index 05a878745..d940b6076 100644 --- a/src/nyx/python/nyxus/nyxus.py +++ b/src/nyx/python/nyxus/nyxus.py @@ -154,7 +154,7 @@ def __init__( 'gabor_thold', 'gabor_thetas', 'gabor_freqs', 'channel_signature', 'parent_channel', 'child_channel', 'aggregate', 'dynamic_range', 'min_intensity', 'max_intensity', 'ram_limit', 'verbose', - 'anisotropy_x', 'anisotropy_y', 'mergerois', 'preserve_hu' + 'anisotropy_x', 'anisotropy_y', 'mergerois', 'preserve_hu', 'use_physical_spacing' } # Check for unexpected keyword arguments @@ -181,6 +181,7 @@ def __init__( min_intensity = kwargs.get('min_intensity', 0.0) max_intensity = kwargs.get('max_intensity', 1.0) preserve_hu = kwargs.get('preserve_hu', False) # CT/HU: preserve absolute Hounsfield values + use_physical_spacing = kwargs.get('use_physical_spacing', False) # opt-in OME PhysicalSize* voxel spacing (3D) ram_limit = kwargs.get('ram_limit', -1) verb_lvl = kwargs.get('verbose', 0) aniso_x = kwargs.get('anisotropy_x', 1.0) @@ -233,7 +234,8 @@ def __init__( aniso_y, aniso_z, mergerois, - preserve_hu) + preserve_hu, + use_physical_spacing) self.set_gabor_feature_params( kersize = gabor_kersize, @@ -1001,7 +1003,7 @@ def __init__( 'verbose', 'anisotropy_x', 'anisotropy_y', - 'anisotropy_z', 'preserve_hu' + 'anisotropy_z', 'preserve_hu', 'use_physical_spacing' } # Check for unexpected keyword arguments @@ -1021,6 +1023,7 @@ def __init__( min_intensity = kwargs.get('min_intensity', 0.0) max_intensity = kwargs.get('max_intensity', 1.0) preserve_hu = kwargs.get('preserve_hu', False) # CT/HU: preserve absolute Hounsfield values + use_physical_spacing = kwargs.get('use_physical_spacing', False) # opt-in OME PhysicalSize* voxel spacing (3D) verb_lvl = kwargs.get ('verbose', 0) aniso_x = kwargs.get('anisotropy_x', 1.0) aniso_y = kwargs.get('anisotropy_y', 1.0) @@ -1080,7 +1083,8 @@ def __init__( aniso_y, aniso_z, False, # merge_labels: 2D-segmented only - preserve_hu) + preserve_hu, + use_physical_spacing) # list of valid outputs that are used throughout featurize functions self._valid_output_types = ['pandas', 'arrowipc', 'parquet'] @@ -1570,6 +1574,7 @@ def __init__( min_intensity = kwargs.get('min_intensity', 0.0) max_intensity = kwargs.get('max_intensity', 1.0) preserve_hu = kwargs.get('preserve_hu', False) # CT/HU: preserve absolute Hounsfield values + use_physical_spacing = kwargs.get('use_physical_spacing', False) # opt-in OME PhysicalSize* voxel spacing (3D) ram_limit = kwargs.get('ram_limit', -1) verb_lvl = kwargs.get ('verbose', 0) aniso_x = kwargs.get('anisotropy_x', 1.0) @@ -1626,7 +1631,8 @@ def __init__( aniso_y, aniso_z, False, # merge_labels: 2D-segmented only - preserve_hu) + preserve_hu, + use_physical_spacing) # list of valid outputs that are used throughout featurize functions self._valid_output_types = ['pandas', 'arrowipc', 'parquet'] diff --git a/src/nyx/raw_format.h b/src/nyx/raw_format.h index 27c13d75c..76ad8cb3e 100644 --- a/src/nyx/raw_format.h +++ b/src/nyx/raw_format.h @@ -46,6 +46,13 @@ class RawFormatLoader return 1; } + // Physical voxel spacing along X/Y/Z (default 1.0 == uncalibrated) + unit string. + // OME loaders return the parsed PhysicalSize*; everything else stays 1.0 / "". + [[nodiscard]] virtual double physicalSizeX() const { return 1.0; } + [[nodiscard]] virtual double physicalSizeY() const { return 1.0; } + [[nodiscard]] virtual double physicalSizeZ() const { return 1.0; } + [[nodiscard]] virtual std::string physicalSizeUnit() const { return std::string(); } + [[nodiscard]] virtual size_t tileWidth(size_t level) const = 0; [[nodiscard]] virtual size_t tileHeight(size_t level) const = 0; diff --git a/src/nyx/raw_image_loader.cpp b/src/nyx/raw_image_loader.cpp index d85232240..5a388fc2f 100644 --- a/src/nyx/raw_image_loader.cpp +++ b/src/nyx/raw_image_loader.cpp @@ -294,6 +294,31 @@ size_t RawImageLoader::get_mask_time() return 0; // whole-slide mode } +size_t RawImageLoader::get_inten_channels() +{ + return intFL->numberChannels(); // FIX (IO): OME loaders report the real C; others default to 1 +} + +double RawImageLoader::get_physical_size_x() +{ + return intFL->physicalSizeX(); // FIX (IO): OME PhysicalSizeX; 1.0 if uncalibrated +} + +double RawImageLoader::get_physical_size_y() +{ + return intFL->physicalSizeY(); +} + +double RawImageLoader::get_physical_size_z() +{ + return intFL->physicalSizeZ(); +} + +std::string RawImageLoader::get_physical_size_unit() +{ + return intFL->physicalSizeUnit(); +} + std::string RawImageLoader::get_slide_descr() { std::string s = get_fp_phys_pixvoxels() ? "R-" : "N-"; diff --git a/src/nyx/raw_image_loader.h b/src/nyx/raw_image_loader.h index c871e6968..eb5c1db03 100644 --- a/src/nyx/raw_image_loader.h +++ b/src/nyx/raw_image_loader.h @@ -21,6 +21,84 @@ class RawImageLoader uint32_t get_cur_tile_seg_pixel(size_t pixel_idx); double get_cur_tile_dpequiv_pixel(size_t idx); + // FIX: visit every voxel of one (channel, timeframe) volume WITHOUT materializing it. + // The 3D prescan used to do ONE load_tile(0,0) and then index W*H*D voxels off the tile + // buffer -- but a per-plane loader (OME-TIFF / OME-Zarr) fills only ONE Z-plane, so + // everything past the first slice read out of bounds and corrupted the slide min/max. + // (It also assumed the tile stride equals the full width.) Streaming rather than filling + // a W*H*D staging buffer matters because the prescan repeats this for every (c,t) and + // only needs running min/max plus mask-driven ROI geometry: a double buffer would cost + // 4x the raw uint16 volume in RAM per pass and buy nothing. + // + // Walks the tile GRID of each plane, so planes spanning several tiles/chunks are handled; + // NIfTI-style whole-4D loaders (tileDepth == full depth) are slabbed out via frameBase. + // fn is invoked ONLY for in-mask voxels (msk != 0), as fn(x, y, z, intensity, msk); in + // whole-slide mode every voxel is in-mask with msk == 1. + template + bool for_each_voxel (size_t channel, size_t timeframe, F&& fn) + { + const bool haveSeg = (segFL != nullptr); + + // Use each loader's OWN layout: per-plane loaders (OME-Zarr, multi-page/OME-TIFF) + // deliver one Z-plane per read (tileDepth == 1); a whole-4D loader delivers the entire + // x*y*z*t blob in one read and ignores the layer arg. Mirrors ImageLoader::assemble_volume. + const size_t ltd = intFL->tileDepth (lvl), + ltt = intFL->tileTimestamps (lvl), + lntd = intFL->numberTileDepth (lvl), + lnth = intFL->numberTileHeight (lvl), + lntw = intFL->numberTileWidth (lvl), + frameStride = ltd * th * tw, + frameBase = (ltt > 1) ? timeframe * frameStride : 0; + + // The mask is channel-/timeframe-agnostic unless it genuinely has that many. FIX: a + // single-timeframe mask (the common 1-mask : N-timeframe-intensity case) was read at + // the intensity's `timeframe`, so for T>1 the TIFF mask loader computed an IFD past the + // end (ifdForPlane with t>0) and TIFFSetDirectory threw -- uncaught here, crashing the + // process (0xC0000409). Clamp the mask timeframe like the channel. (Zarr masks have no + // T axis to over-index, so only TIFF crashed.) + const size_t maskChannel = (haveSeg && channel < segFL->numberChannels()) ? channel : 0; + const size_t maskTimeframe = (haveSeg && timeframe < segFL->fullTimestamps (lvl)) ? timeframe : 0; + + for (size_t lz = 0; lz < lntd; lz++) + { + for (size_t tr = 0; tr < lnth; tr++) + for (size_t tc = 0; tc < lntw; tc++) + { + intFL->loadTileFromFile (tr, tc, lz, channel, timeframe, lvl); + if (haveSeg) + segFL->loadTileFromFile (tr, tc, lz, maskChannel, maskTimeframe, lvl); + + const size_t row0 = tr * th, col0 = tc * tw; + if (row0 < fh && col0 < fw) + { + const size_t validH = (std::min) (th, fh - row0), + validW = (std::min) (tw, fw - col0); + + for (size_t pz = 0; pz < ltd && (lz * ltd + pz) < fd; pz++) + { + const size_t gz = lz * ltd + pz; + for (size_t row = 0; row < validH; row++) + for (size_t col = 0; col < validW; col++) + { + const size_t src = frameBase + (pz * th + row) * tw + col; + const uint32_t msk = haveSeg ? segFL->get_uint32_pixel (src) : (uint32_t)1; + if (!msk) + continue; // off-ROI: skip before paying for the intensity read + fn (col0 + col, row0 + row, gz, intFL->get_dpequiv_pixel (src), msk); + } + } + } + + // the raw TIFF loaders malloc their tile buffer on each read (no-op elsewhere) + intFL->free_tile(); + if (haveSeg) + segFL->free_tile(); + } + } + + return true; + } + size_t get_tile_size(); size_t get_num_tiles_vert(); size_t get_num_tiles_hor(); @@ -34,6 +112,11 @@ class RawImageLoader size_t get_full_depth(); size_t get_inten_time(); size_t get_mask_time(); + size_t get_inten_channels(); // FIX (IO): number of intensity channels (>=1) + double get_physical_size_x(); // FIX (IO): physical voxel spacing (1.0 if uncalibrated) + double get_physical_size_y(); + double get_physical_size_z(); + std::string get_physical_size_unit(); // Select which channel (C) / timeframe (T) plane subsequent load_tile() calls // read. Default 0/0 preserves the single-channel, single-timepoint behavior. diff --git a/src/nyx/raw_omezarr.h b/src/nyx/raw_omezarr.h index d1f2af085..9c5dd42fc 100644 --- a/src/nyx/raw_omezarr.h +++ b/src/nyx/raw_omezarr.h @@ -24,6 +24,29 @@ #include "raw_format.h" +// FIX (IO): z5 Datatype -> the "(filePath.c_str()); - nlohmann::json file_attributes, ds_attributes; + nlohmann::json file_attributes; z5::readAttributes(*zarr_ptr_, file_attributes); - // open the highest-resolution dataset (level 0) - ds_name_ = file_attributes["multiscales"][0]["datasets"][0]["path"].get(); - const auto ds_handle = z5::filesystem::handle::Dataset(*zarr_ptr_, ds_name_); - fs::path metadata_path; - auto success = z5::filesystem::metadata_detail::getMetadataPath(ds_handle, metadata_path); - z5::filesystem::metadata_detail::readMetadata(metadata_path, ds_attributes); + // Resolve the level-0 dataset path (NGFF 0.5 nests 'multiscales' under "ome"; 0.4 has + // it at the attributes root). + const nlohmann::json& ms_root = (file_attributes.contains("ome") && file_attributes["ome"].contains("multiscales")) + ? file_attributes["ome"] : file_attributes; + ds_name_ = ms_root["multiscales"][0]["datasets"][0]["path"].get(); - std::vector level0Shape, chunkShape; - for (const auto& d : ds_attributes["shape"]) level0Shape.push_back(d.get()); - for (const auto& d : ds_attributes["chunks"]) chunkShape.push_back(d.get()); - std::string dtype_str = ds_attributes["dtype"].get(); + // FIX (IO): open via z5 (auto-detects Zarr v2 .zarray vs v3 zarr.json, handles v3 + // chunk-key encoding, codecs and sharding) and query shape/chunking/dtype from the + // Dataset object -- format-agnostic, so this reads BOTH OME-Zarr 0.4 and 0.5. + ds_ = z5::openDataset(*zarr_ptr_, ds_name_); + std::vector level0Shape(ds_->shape().begin(), ds_->shape().end()); + std::vector chunkShape(ds_->defaultChunkShape().begin(), ds_->defaultChunkShape().end()); + std::string dtype_str = zarr_dtype_string_of(ds_->getDtype()); // Resolve axis roles from the NGFF 'axes' metadata instead of assuming a // fixed [T,C,Z,Y,X] order. Falls back to legacy 5D TCZYX if 'axes' is absent. @@ -63,6 +88,13 @@ class RawOmezarrLoader: public RawFormatLoader iz_ = axes.storageIndexOf('Z'); ic_ = axes.storageIndexOf('C'); it_ = axes.storageIndexOf('T'); n_levels_ = axes.numberPyramidLevels(); + // FIX: advertise the real C/T extents so the pipeline iterates channels and + // timeframes; without this the base class default of 1 kept it on plane (c=0,t=0). + n_channels_ = axes.sizeC; + n_timeframes_ = axes.sizeT; + // FIX (IO): keep the parsed physical voxel spacing for opt-in calibration. + phys_x_ = axes.physX; phys_y_ = axes.physY; phys_z_ = axes.physZ; + phys_unit_ = axes.unitXY; } else { @@ -74,6 +106,10 @@ class RawOmezarrLoader: public RawFormatLoader ic_ = (n >= 4) ? n - 4 : -1; it_ = (n >= 5) ? n - 5 : -1; n_levels_ = 1; + // FIX: derive C/T extents from the positional axes so the fallback path also + // reports multi-channel / time-series counts (1 when the axis is absent). + n_channels_ = (ic_ >= 0) ? level0Shape[ic_] : 1; + n_timeframes_ = (it_ >= 0) ? level0Shape[it_] : 1; } // X and Y must resolve to real dimensions, else the read would index OOB. if (ix_ < 0 || iy_ < 0 || (size_t)ix_ >= level0Shape.size() || (size_t)iy_ >= level0Shape.size()) @@ -103,11 +139,6 @@ class RawOmezarrLoader: public RawFormatLoader // allocate the buffer dest = std::vector (tile_height_ * tile_width_); - - // Open the dataset once and cache the handle. The dataset metadata is - // immutable for the lifetime of this loader, so there is no need to - // re-open (and re-parse the .zarray metadata) on every tile read. - ds_ = z5::openDataset(*zarr_ptr_, ds_name_); } ~RawOmezarrLoader() override @@ -251,6 +282,15 @@ class RawOmezarrLoader: public RawFormatLoader [[nodiscard]] short bitsPerSample() const override { return bits_per_sample_; } /// @brief Number of resolution (pyramid) levels declared in multiscales [[nodiscard]] size_t numberPyramidLevels() const override { return n_levels_; } + /// @brief Channel (C) extent resolved from the NGFF axes (1 if no channel axis) + [[nodiscard]] size_t numberChannels() const override { return n_channels_; } + /// @brief Time (T) extent resolved from the NGFF axes (1 if no time axis) + [[nodiscard]] size_t fullTimestamps([[maybe_unused]] size_t level) const override { return n_timeframes_; } + /// @brief Physical voxel spacing from the NGFF coordinateTransformations (1.0 if uncalibrated) + [[nodiscard]] double physicalSizeX() const override { return phys_x_; } + [[nodiscard]] double physicalSizeY() const override { return phys_y_; } + [[nodiscard]] double physicalSizeZ() const override { return phys_z_; } + [[nodiscard]] std::string physicalSizeUnit() const override { return phys_unit_; } private: @@ -267,6 +307,10 @@ class RawOmezarrLoader: public RawFormatLoader size_t ndim_ = 5; ///< Number of on-disk dimensions (2..5) short bits_per_sample_ = 16; ///< Real bit depth size_t n_levels_ = 1; ///< Pyramid level count + size_t n_channels_ = 1; ///< Channel (C) extent + size_t n_timeframes_ = 1; ///< Time (T) extent + double phys_x_ = 1.0, phys_y_ = 1.0, phys_z_ = 1.0; ///< Physical voxel spacing + std::string phys_unit_; ///< Physical-size unit (e.g. "micrometer") short data_format_ = 0; std::unique_ptr zarr_ptr_; diff --git a/src/nyx/raw_tiff.h b/src/nyx/raw_tiff.h index 01e52d520..88070c69f 100644 --- a/src/nyx/raw_tiff.h +++ b/src/nyx/raw_tiff.h @@ -57,15 +57,33 @@ class RawTiffTileLoader : public RawFormatLoader TIFFGetField(tiff_, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel); TIFFGetField(tiff_, TIFFTAG_BITSPERSAMPLE, &(this->bitsPerSample_)); TIFFGetField(tiff_, TIFFTAG_SAMPLEFORMAT, &(this->sampleFormat_)); + // FIX (IO): default absent SAMPLEFORMAT to 1 (unsigned int), like the strip loader, + // so tifffile-written tiled uint OME-TIFFs (which omit the tag) read correctly. + if (sampleFormat_ < 1 || sampleFormat_ > 3) + sampleFormat_ = 1; // Test if the file is greyscale - if (samplesPerPixel != 1) + if (samplesPerPixel != 1) { std::string erm = "RawTiffTileLoader error: file " + filePath + " is not greyscale, SamplesPerPixel = " + std::to_string(samplesPerPixel); std::cerr << erm << "\n"; throw (std::runtime_error(erm)); } + // FIX (IO): parse a tiled OME-TIFF's IFD-0 OME-XML so (z,c,t) map to the right IFD; + // a plain tiled TIFF stays 2D (fullDepth = directory count, usually 1). + fullDepth_ = TIFFNumberOfDirectories(tiff_); + char* desc = nullptr; + if (TIFFGetField(tiff_, TIFFTAG_IMAGEDESCRIPTION, &desc) && desc != nullptr) + { + ome_ = Nyxus::parse_ome_xml(desc); + if (ome_.valid) + { + is_ome_ = true; + fullDepth_ = ome_.sizeZ; // depth is SizeZ, not the total IFD count (= Z*C*T) + } + } + // Prepare the right typed getter function std::string message; switch (sampleFormat_) @@ -168,11 +186,23 @@ class RawTiffTileLoader : public RawFormatLoader void loadTileFromFile ( size_t indexRowGlobalTile, size_t indexColGlobalTile, - size_t indexLayerGlobalTile, - [[maybe_unused]] size_t indexChannel, // plain 2D TIFF: single channel - [[maybe_unused]] size_t indexTimeframe, // plain 2D TIFF: no time series + size_t indexLayerGlobalTile, // Z plane + size_t indexChannel, // C plane (OME); 0 for plain 2D TIFF + size_t indexTimeframe, // T plane (OME); 0 for plain 2D TIFF size_t level) override { + // FIX (IO): select the (z,c,t) plane's IFD before reading the tile (see grayscale_tiff.h). + if (is_ome_ && (indexLayerGlobalTile >= ome_.sizeZ + || indexChannel >= ome_.sizeC || indexTimeframe >= ome_.sizeT)) + { + throw std::runtime_error("RawTiffTileLoader: (z,c,t)=(" + std::to_string(indexLayerGlobalTile) + + "," + std::to_string(indexChannel) + "," + std::to_string(indexTimeframe) + ") out of range"); + } + size_t ifd = is_ome_ ? ome_.ifdForPlane(indexLayerGlobalTile, indexChannel, indexTimeframe) + : indexLayerGlobalTile; + if (TIFFSetDirectory(tiff_, (uint16_t)ifd) != 1) + throw std::runtime_error("RawTiffTileLoader: TIFFSetDirectory(ifd=" + std::to_string(ifd) + ") failed"); + // Low level read TIFF bytes auto t_szb = TIFFTileSize(tiff_); tiffTile = _TIFFmalloc(t_szb); @@ -218,10 +248,18 @@ class RawTiffTileLoader : public RawFormatLoader [[nodiscard]] size_t fullHeight([[maybe_unused]] size_t level) const { return fullHeight_; } [[nodiscard]] size_t fullWidth([[maybe_unused]] size_t level) const { return fullWidth_; } + [[nodiscard]] size_t fullDepth([[maybe_unused]] size_t level) const override { return fullDepth_; } [[nodiscard]] size_t tileWidth([[maybe_unused]] size_t level) const { return tileWidth_; } [[nodiscard]] size_t tileHeight([[maybe_unused]] size_t level) const { return tileHeight_; } [[nodiscard]] short bitsPerSample() const { return bitsPerSample_; } [[nodiscard]] size_t numberPyramidLevels() const { return 1; } + // FIX (IO): advertise C/T + physical spacing from the parsed OME-XML (1 / plain otherwise) + [[nodiscard]] size_t numberChannels() const override { return is_ome_ ? ome_.sizeC : 1; } + [[nodiscard]] size_t fullTimestamps([[maybe_unused]] size_t level) const override { return is_ome_ ? ome_.sizeT : 1; } + [[nodiscard]] double physicalSizeX() const override { return is_ome_ ? ome_.physX : 1.0; } + [[nodiscard]] double physicalSizeY() const override { return is_ome_ ? ome_.physY : 1.0; } + [[nodiscard]] double physicalSizeZ() const override { return is_ome_ ? ome_.physZ : 1.0; } + [[nodiscard]] std::string physicalSizeUnit() const override { return is_ome_ ? ome_.unitXY : std::string(); } private: @@ -285,18 +323,22 @@ class RawTiffTileLoader : public RawFormatLoader size_t fullHeight_ = 0, ///< Full height in pixel fullWidth_ = 0, ///< Full width in pixel + fullDepth_ = 1, ///< Full depth (Z); >1 for multi-plane OME-TIFF tileHeight_ = 0, ///< Tile height tileWidth_ = 0; ///< Tile width + bool is_ome_ = false; ///< true when IFD-0 carries an OME-XML block + Nyxus::OmeAxes ome_; ///< parsed OME dimensions (drives the (z,c,t)->IFD map) + short sampleFormat_ = 0, ///< Sample format as defined by libtiff bitsPerSample_ = 0; ///< Bit Per Sample as defined by libtiff - double minval, maxval; + double minval, maxval; // low level buffer tdata_t tiffTile = nullptr; - size_t t_szb = 0; + size_t t_szb = 0; }; class RawTiffStripLoader : public RawFormatLoader @@ -465,6 +507,15 @@ class RawTiffStripLoader : public RawFormatLoader size_t indexTimeframe, // T plane (OME-TIFF only) [[maybe_unused]] size_t level) override { + // FIX: (re)allocate lazily so this loader honors "free_tile() follows each + // loadTileFromFile()" -- see free_tile(). The ctor allocation covers the first read. + if (buf == nullptr) + { + buf = _TIFFmalloc (scanline_szb * tileHeight_); + if (!buf) + throw std::runtime_error("RawTiffStripLoader: _TIFFmalloc failed"); + } + size_t startLayer = indexLayerGlobalTile * tileDepth_, endLayer = std::min((indexLayerGlobalTile + 1) * tileDepth_, fullDepth_), @@ -501,15 +552,33 @@ class RawTiffStripLoader : public RawFormatLoader [[nodiscard]] size_t fullHeight([[maybe_unused]] size_t level) const override { return fullHeight_; } [[nodiscard]] size_t fullWidth([[maybe_unused]] size_t level) const override { return fullWidth_; } [[nodiscard]] size_t fullDepth([[maybe_unused]] size_t level) const override { return fullDepth_; } + // FIX: advertise C/T extents from the parsed OME-XML so the volumetric pipeline + // iterates channels/timeframes; non-OME TIFF keeps the single-plane default of 1. + [[nodiscard]] size_t numberChannels() const override { return is_ome_ ? ome_.sizeC : 1; } + [[nodiscard]] size_t fullTimestamps([[maybe_unused]] size_t level) const override { return is_ome_ ? ome_.sizeT : 1; } + // FIX (IO): physical voxel spacing from OME-XML PhysicalSize* (1.0 for plain TIFF) + [[nodiscard]] double physicalSizeX() const override { return is_ome_ ? ome_.physX : 1.0; } + [[nodiscard]] double physicalSizeY() const override { return is_ome_ ? ome_.physY : 1.0; } + [[nodiscard]] double physicalSizeZ() const override { return is_ome_ ? ome_.physZ : 1.0; } + [[nodiscard]] std::string physicalSizeUnit() const override { return is_ome_ ? ome_.unitXY : std::string(); } [[nodiscard]] size_t tileWidth([[maybe_unused]] size_t level) const override { return tileWidth_; } [[nodiscard]] size_t tileHeight([[maybe_unused]] size_t level) const override { return tileHeight_; } [[nodiscard]] size_t tileDepth([[maybe_unused]] size_t level) const override { return tileDepth_; } [[nodiscard]] short bitsPerSample() const override { return bitsPerSample_; } [[nodiscard]] size_t numberPyramidLevels() const override { return 1; } + // FIX: honor the RawFormatLoader contract that free_tile() follows EACH + // loadTileFromFile(). This buffer used to be allocated once in the ctor and never + // re-allocated, so a second load after a free wrote into freed memory (hit when + // looping Z to assemble a volume, and latent for multi-tile 2D scans which free per + // tile). Freeing is now idempotent and loadTileFromFile lazily re-allocates. void free_tile() override { - _TIFFfree(buf); + if (buf) + { + _TIFFfree(buf); + buf = nullptr; + } } uint32_t get_uint32_pixel(size_t idx) const diff --git a/src/nyx/reduce_trivial_rois.cpp b/src/nyx/reduce_trivial_rois.cpp index 03245098e..a9066381f 100644 --- a/src/nyx/reduce_trivial_rois.cpp +++ b/src/nyx/reduce_trivial_rois.cpp @@ -660,7 +660,7 @@ namespace Nyxus if (D3_VoxelIntensityFeatures::required(env.theFeatureSet)) { Fsettings& s = env.fsett_D3_VoxelIntensity; - D3_VoxelIntensityFeatures::extract (r, s); + D3_VoxelIntensityFeatures::extract (r, s, env.dataset); // FIX: pass the Dataset (see 3d_intensity.h) } //==== shape if (D3_SurfaceFeature::required(env.theFeatureSet)) diff --git a/src/nyx/slideprops.cpp b/src/nyx/slideprops.cpp index f96de6bbc..86ddf1e21 100644 --- a/src/nyx/slideprops.cpp +++ b/src/nyx/slideprops.cpp @@ -95,6 +95,11 @@ namespace Nyxus // time series p.inten_time = ilo.get_inten_time(); p.mask_time = ilo.get_mask_time(); + p.inten_channels = ilo.get_inten_channels(); // FIX (IO): number of channels (>=1) + p.phys_x = ilo.get_physical_size_x(); // FIX (IO): physical voxel spacing (1.0 if uncalibrated) + p.phys_y = ilo.get_physical_size_y(); + p.phys_z = ilo.get_physical_size_z(); + p.phys_unit = ilo.get_physical_size_unit(); // scan intensity slide's data @@ -270,95 +275,91 @@ namespace Nyxus // time series p.inten_time = ilo.get_inten_time(); p.mask_time = ilo.get_mask_time(); + p.inten_channels = ilo.get_inten_channels(); // FIX (IO): number of channels (>=1) + p.phys_x = ilo.get_physical_size_x(); // FIX (IO): physical voxel spacing (1.0 if uncalibrated) + p.phys_y = ilo.get_physical_size_y(); + p.phys_z = ilo.get_physical_size_z(); + p.phys_unit = ilo.get_physical_size_unit(); // scan intensity slide's data - bool wholeslide = p.fname_seg.empty(); - double slide_I_max = (std::numeric_limits::lowest)(), slide_I_min = (std::numeric_limits::max)(); std::unordered_set U; // unique ROI mask labels std::unordered_map R; // ROI data - // Read the volume. The image loader is in the open state by previously called processDataset_XX_YY () + // The image loader is in the open state by previously called processDataset_XX_YY (). + // FIX: no whole-volume staging buffer and no flat voxel index any more -- the scan + // streams tiles and receives (x,y,z) directly, so sliceSize/nVox/wholeslide are gone. size_t fullW = ilo.get_full_width(), fullH = ilo.get_full_height(), - fullD = ilo.get_full_depth(), - sliceSize = fullW * fullH, - nVox = sliceSize * fullD; - - // in the 3D case tiling is a formality, so fetch the only tile in the file - if (!ilo.load_tile(0, 0)) + fullD = ilo.get_full_depth(); + + // FIX: scan EVERY (channel, timeframe) volume, not just (0,0). + // (a) for_each_voxel() streams the whole X*Y*Z volume, walking each plane's tile grid. + // The old single load_tile(0,0) filled only ONE tile of ONE Z-plane for per-plane + // loaders (OME-TIFF/OME-Zarr), so the W*H*D voxel scan read past the tile buffer + // -- garbage that corrupted the slide min/max. + // (b) the pipeline featurizes every (c,t) plane, so the slide intensity range must + // cover all of them. Scanning only (c0,t0) yields a range that is too small for + // c>0 / t>0, and intensity-indexed buffers sized from it overflow. + // ROI geometry is taken from the first pass only (the mask is channel-agnostic). + const size_t n_chan = (std::max)((size_t)1, p.inten_channels), + n_time = (std::max)((size_t)1, p.inten_time); + bool first_pass = true; + + for (size_t scan_c = 0; scan_c < n_chan; scan_c++) + for (size_t scan_t = 0; scan_t < n_time; scan_t++) { -#ifdef WITH_PYTHON_H - throw "Error fetching tile"; -#endif - std::cerr << "Error fetching tile\n"; - return false; - } - - // iterate abstract tiles (in a tiled slide /e.g. tiled tiff/ they correspond to physical tiles, in a nontiled slide /e.g. scanline tiff or strip tiff/ they correspond to ) - int cnt = 1; - - // iterate voxels - for (size_t i = 0; i < nVox; i++) + // Streams tile by tile -- (a) above needs the whole volume scanned, but nothing here + // needs it resident, and staging W*H*D doubles once per (c,t) is 4x the raw uint16 + // volume in RAM for no benefit. for_each_voxel calls back only for in-mask voxels. + bool ok = ilo.for_each_voxel (scan_c, scan_t, + [&](size_t x, size_t y, size_t z, double dxequiv_I, uint32_t msk) { - // Mask - uint32_t msk = 1; // wholeslide by default - if (!wholeslide) - msk = ilo.get_cur_tile_seg_pixel(i); - - // Skip non-mask pixels - if (!msk) - continue; - - int z = i / sliceSize, - y = (i - z * sliceSize) / fullW, - x = (i - z * sliceSize) % fullW; - - // Skip tile buffer pixels beyond the image's bounds - if (x >= fullW || y >= fullH || z >= fullD) - continue; - // dynamic range within- and off-ROI - double dxequiv_I = ilo.get_cur_tile_dpequiv_pixel(i); slide_I_max = (std::max)(slide_I_max, dxequiv_I); slide_I_min = (std::min)(slide_I_min, dxequiv_I); - // Update pixel's ROI metrics - // - the following block mocks feed_pixel_2_metrics (x, y, dataI[i], msk, tidx) - if (U.find(msk) == U.end()) + // Update pixel's ROI metrics. FIX: geometry (labels, area, AABB) is derived from + // the FIRST (c,t) pass only -- the mask is the same for every channel/timeframe, + // so re-accumulating it per pass would multiply aux_area by n_chan*n_time. + if (first_pass) { - // Remember this label - U.insert(msk); + // - the following block mocks feed_pixel_2_metrics (x, y, dataI[i], msk, tidx) + if (U.find(msk) == U.end()) + { + // Remember this label + U.insert(msk); - // Initialize the ROI label record - LR r(msk); + // Initialize the ROI label record + LR r(msk); - // - mocking init_label_record_3 (newData, theSegFname, theIntFname, x, y, label, intensity, tile_index) - // Initialize basic counters - r.aux_area = 1; - r.aux_min = r.aux_max = 0; //we don't have uint-cast intensities at this moment - r.init_aabb_3D(x, y, z); + // - mocking init_label_record_3 (newData, theSegFname, theIntFname, x, y, label, intensity, tile_index) + // Initialize basic counters + r.aux_area = 1; + r.aux_min = r.aux_max = 0; //we don't have uint-cast intensities at this moment + r.init_aabb_3D((int)x, (int)y, (int)z); - // - not storing file names (r.segFname = segFile, r.intFname = intFile) but will do so in the future + // - not storing file names (r.segFname = segFile, r.intFname = intFile) but will do so in the future - // Attach - R[msk] = r; - } - else - { - // Update basic ROI info (info that doesn't require costly calculations) - LR& r = R[msk]; + // Attach + R[msk] = r; + } + else + { + // Update basic ROI info (info that doesn't require costly calculations) + LR& r = R[msk]; - // - mocking update_label_record_2 (r, x, y, label, intensity, tile_index) + // - mocking update_label_record_2 (r, x, y, label, intensity, tile_index) - // Per-ROI - r.aux_area++; + // Per-ROI + r.aux_area++; - // save - r.update_aabb_3D(x, y, z); + // save + r.update_aabb_3D((int)x, (int)y, (int)z); + } } #ifdef WITH_PYTHON_H @@ -367,7 +368,19 @@ namespace Nyxus throw pybind11::error_already_set(); #endif - } //- all voxels + }); //- all voxels + + if (!ok) + { +#ifdef WITH_PYTHON_H + throw "Error fetching volume"; +#endif + std::cerr << "Error fetching volume\n"; + return false; + } + + first_pass = false; + } //- all (channel, timeframe) volumes //****** fix ROIs' AABBs with respect to anisotropy diff --git a/src/nyx/slideprops.h b/src/nyx/slideprops.h index 5bbfcf0d3..5a64c9eee 100644 --- a/src/nyx/slideprops.h +++ b/src/nyx/slideprops.h @@ -28,6 +28,9 @@ class SlideProps max_roi_h = 0; max_roi_d = 0; inten_time = mask_time = 0; + inten_channels = 1; // FIX (IO): >=1 so the channel loop runs at least once; set from the loader in scan_slide_props + phys_x = phys_y = phys_z = 1.0; // FIX (IO): physical voxel spacing (1.0 == uncalibrated); set in scan_slide_props + phys_unit = ""; } // pre-ROI intensity range in DP @@ -86,6 +89,14 @@ class SlideProps // time series size_t inten_time, mask_time; // number of time frames + + // channels (FIX (IO): number of intensity channels; drives the per-channel output rows) + size_t inten_channels; + + // physical voxel spacing (FIX (IO): OME PhysicalSize*; 1.0 == uncalibrated). Used only + // when --use-physical-spacing is set; also emitted as phys_x/y/z + phys_unit output columns. + double phys_x, phys_y, phys_z; + std::string phys_unit; }; namespace Nyxus diff --git a/src/nyx/workflow_2d_segmented.cpp b/src/nyx/workflow_2d_segmented.cpp index ebcc051ce..c416957b7 100644 --- a/src/nyx/workflow_2d_segmented.cpp +++ b/src/nyx/workflow_2d_segmented.cpp @@ -166,11 +166,12 @@ namespace Nyxus const SaveOption saveOption, const std::string& outputPath) { + env.reset_csv_output_state(); // FIX: this run's CSV files start fresh (see Environment::csv_paths_written) #ifdef CHECKTIMING if (Stopwatch::inclusive()) Stopwatch::reset(); -#endif +#endif //********************** prescan *********************** @@ -322,7 +323,7 @@ namespace Nyxus if (write_apache) { - auto [status, msg] = env.arrow_stream.write_arrow_file (Nyxus::get_feature_values(env, env.theFeatureSet, env.uniqueLabels, env.roiData, env.dataset)); + auto [status, msg] = env.arrow_stream.write_arrow_file (Nyxus::get_feature_values(env, env.theFeatureSet, env.uniqueLabels, env.roiData, env.dataset, DEFAULT_T_INDEX, DEFAULT_C_INDEX)); if (!status) { std::cerr << "Error writing Arrow file: " << msg.value() << std::endl; @@ -332,7 +333,7 @@ namespace Nyxus else if (saveOption == SaveOption::saveCSV) { - ok = save_features_2_csv (env, ifp, lfp, outputPath, 0/*pass 0 as t_index is not used in 2D scenario*/ , env.resultOptions.need_aggregation()); + ok = save_features_2_csv (env, ifp, lfp, outputPath, 0/*t_index unused in 2D*/, DEFAULT_C_INDEX/*channel unused in 2D*/, env.resultOptions.need_aggregation()); if (ok == false) { @@ -342,7 +343,7 @@ namespace Nyxus } else { - ok = save_features_2_buffer (env.theResultsCache, env, DEFAULT_T_INDEX); + ok = save_features_2_buffer (env.theResultsCache, env, DEFAULT_T_INDEX, DEFAULT_C_INDEX); if (ok == false) { diff --git a/src/nyx/workflow_2d_whole.cpp b/src/nyx/workflow_2d_whole.cpp index 9911f404f..54fa617ff 100644 --- a/src/nyx/workflow_2d_whole.cpp +++ b/src/nyx/workflow_2d_whole.cpp @@ -177,7 +177,7 @@ namespace Nyxus // thread-safely save results of this single slide if (write_apache) { - auto [status, msg] = save_features_2_apache_wholeslide (env, vroi, p.fname_int); + auto [status, msg] = save_features_2_apache_wholeslide (env, vroi, p.fname_int, 0/*t_index*/, 0/*c_index*/); if (! status) { std::cerr << "Error writing Arrow file: " << msg.value() << std::endl; @@ -187,7 +187,7 @@ namespace Nyxus else if (saveOption == SaveOption::saveCSV) { - if (save_features_2_csv_wholeslide(env, vroi, p.fname_int, "", outputPath, 0 /*pass 0 as t_index is not used in 2D scenario*/) == false) + if (save_features_2_csv_wholeslide(env, vroi, p.fname_int, "", outputPath, 0 /*t_index unused in 2D*/, 0 /*c_index unused in 2D*/) == false) { std::cout << "error saving results to CSV file, details: " << __FILE__ << ":" << __LINE__ << std::endl; rv = 2; @@ -196,7 +196,7 @@ namespace Nyxus else { // pulls feature values from 'vroi' and appends them to global object 'theResultsCache' exposed to Python API - if (save_features_2_buffer_wholeslide (env.theResultsCache, env, vroi, p.fname_int, "") == false) + if (save_features_2_buffer_wholeslide (env.theResultsCache, env, vroi, p.fname_int, "", 0/*t_index*/, 0/*c_index*/) == false) { std::cerr << "Error saving features to the results buffer" << std::endl; rv = 2; @@ -219,7 +219,9 @@ namespace Nyxus const SaveOption saveOption, const std::string& outputPath) { - // create a vector of blank mask file names. Blank mask counterparts + env.reset_csv_output_state(); // FIX: this run's CSV files start fresh (see Environment::csv_paths_written) + + // create a vector of blank mask file names. Blank mask counterparts // of intensity files will serve as the condition of the whole-slide scenario in the prescan phase std::vector labelFiles (intensFiles.size()); diff --git a/src/nyx/workflow_3d_segmented.cpp b/src/nyx/workflow_3d_segmented.cpp index aa3630eb6..3b382d607 100644 --- a/src/nyx/workflow_3d_segmented.cpp +++ b/src/nyx/workflow_3d_segmented.cpp @@ -32,7 +32,7 @@ namespace Nyxus { - bool processIntSegImagePair_3D (Environment & env, const std::string& intens_fpath, const std::string& label_fpath, size_t filepair_index, size_t t_index, const std::vector& z_indices) + bool processIntSegImagePair_3D (Environment & env, const std::string& intens_fpath, const std::string& label_fpath, size_t filepair_index, size_t t_index, size_t channel, const std::vector& z_indices) { std::vector trivRois, nontrivRois; @@ -51,7 +51,7 @@ namespace Nyxus if (z_indices.size()) okGather = gatherRoisMetrics_25D (env, filepair_index, intens_fpath, label_fpath, z_indices); else - okGather = gatherRoisMetrics_3D (env, filepair_index, intens_fpath, label_fpath, t_index); + okGather = gatherRoisMetrics_3D (env, filepair_index, intens_fpath, label_fpath, t_index, channel); if (!okGather) { std::string msg = "Error gathering ROI metrics from " + intens_fpath + " / " + label_fpath + "\n"; @@ -124,7 +124,7 @@ namespace Nyxus if (z_indices.size()) processTrivialRois_25D (env, trivRois, intens_fpath, label_fpath, env.get_ram_limit(), z_indices); else - processTrivialRois_3D (env, filepair_index, t_index, trivRois, intens_fpath, label_fpath, env.get_ram_limit()); + processTrivialRois_3D (env, filepair_index, t_index, channel, trivRois, intens_fpath, label_fpath, env.get_ram_limit()); } // Phase 3: process nontrivial (oversized) ROIs, if any @@ -145,6 +145,8 @@ namespace Nyxus const SaveOption saveOption, const std::string& outputPath) { + env.reset_csv_output_state(); // FIX: this run's CSV files start fresh (see Environment::csv_paths_written) + //********************** prescan *********************** // slide properties @@ -202,6 +204,10 @@ namespace Nyxus // iterate intensity-mask pairs for (size_t i=0; i1 channel, so each C-plane is featurized and tagged with c_index) + for (size_t c=0; c < env.dataset.dataset_props[i].inten_channels; c++) + { // iterate time frames for (size_t t=0; t < env.dataset.dataset_props[i].inten_time; t++) { @@ -219,19 +225,19 @@ namespace Nyxus // Display (1) dataset progress info and (2) file pair info int digits = 2, k = (int)std::pow(10.f, digits); float perCent = float(i * 100 * k / nf) / float(k); - VERBOSLVL1(env.get_verbosity_level(), std::cout << "[ " << std::setw(digits + 2) << perCent << "% ]\t" << " INT: " << ifile.fname << " SEG: " << mfile.fname << " T:" << t << "\n") - - bool ok = processIntSegImagePair_3D (env, ifile.fdir+ifile.fname, mfile.fdir+mfile.fname, i, t, intensFiles[i].z_indices); + VERBOSLVL1(env.get_verbosity_level(), std::cout << "[ " << std::setw(digits + 2) << perCent << "% ]\t" << " INT: " << ifile.fname << " SEG: " << mfile.fname << " C:" << c << " T:" << t << "\n") + + bool ok = processIntSegImagePair_3D (env, ifile.fdir+ifile.fname, mfile.fdir+mfile.fname, i, t, c, intensFiles[i].z_indices); if (ok == false) { std::cerr << "processIntSegImagePair() returned an error code while processing file pair " << ifile.fname << " - " << mfile.fname << '\n'; return 1; } - // Output features + // Output features (tag each row with its timeframe t and channel c) if (writeApache) { - auto [status, msg] = env.arrow_stream.write_arrow_file(Nyxus::get_feature_values(env, env.theFeatureSet, env.uniqueLabels, env.roiData, env.dataset)); + auto [status, msg] = env.arrow_stream.write_arrow_file(Nyxus::get_feature_values(env, env.theFeatureSet, env.uniqueLabels, env.roiData, env.dataset, t, c)); if (!status) { std::cout << "Error writing Arrow file: " << msg.value() << std::endl; @@ -241,7 +247,7 @@ namespace Nyxus else if (saveOption == SaveOption::saveCSV) { - if (!save_features_2_csv(env, ifile.fname, mfile.fname, outputPath, t, env.resultOptions.need_aggregation())) + if (!save_features_2_csv(env, ifile.fname, mfile.fname, outputPath, t, c, env.resultOptions.need_aggregation())) { std::cout << "error saving results to CSV file, details: " << __FILE__ << ":" << __LINE__ << std::endl; return 2; @@ -249,7 +255,7 @@ namespace Nyxus } else { - if (!save_features_2_buffer(env.theResultsCache, env, t)) + if (!save_features_2_buffer(env.theResultsCache, env, t, c)) { std::cout << "error saving results to a buffer, details: " << __FILE__ << ":" << __LINE__ << std::endl; return 2; @@ -269,6 +275,7 @@ namespace Nyxus } #endif } //- time frames + } //- channels } //- inten-mask pairs if (writeApache) diff --git a/src/nyx/workflow_3d_whole.cpp b/src/nyx/workflow_3d_whole.cpp index a452600b2..0ff28ca08 100644 --- a/src/nyx/workflow_3d_whole.cpp +++ b/src/nyx/workflow_3d_whole.cpp @@ -32,7 +32,7 @@ namespace Nyxus { - bool featurize_triv_wholevolume (Environment & env, size_t sidx, ImageLoader& imlo, size_t memory_limit, LR& vroi) + bool featurize_triv_wholevolume (Environment & env, size_t sidx, ImageLoader& imlo, size_t memory_limit, LR& vroi, size_t channel, size_t timeframe) { const std::string& ifpath = env.dataset.dataset_props[sidx].fname_int; @@ -49,21 +49,25 @@ namespace Nyxus } // read the slide into a pixel cloud - if (env.anisoOptions.customized() == false) + // FIX (IO): --aniso* (explicit) or opt-in OME physical spacing selects the anisotropic path + double ax, ay, az; + if (! resolve_slide_anisotropy (env, sidx, ax, ay, az)) { VERBOSLVL2(env.get_verbosity_level(), std::cout << "\nscan_trivial_wholeslide()\n"); - scan_trivial_wholevolume (vroi, ifpath, imlo); + scan_trivial_wholevolume (vroi, ifpath, imlo, channel, timeframe); } else { VERBOSLVL2(env.get_verbosity_level(), std::cout << "\nscan_trivial_wholeslide_ANISO()\n"); scan_trivial_wholevolume_anisotropic ( - vroi, - ifpath, - imlo, - env.anisoOptions.get_aniso_x(), - env.anisoOptions.get_aniso_y(), - env.anisoOptions.get_aniso_z()); + vroi, + ifpath, + imlo, + ax, + ay, + az, + channel, + timeframe); } // allocate memory for feature helpers (image matrix, etc) @@ -87,7 +91,7 @@ namespace Nyxus return true; } - bool featurize_wholevolume (Environment & env, size_t sidx, ImageLoader& imlo, LR& vroi) + bool featurize_wholevolume (Environment & env, size_t sidx, ImageLoader& imlo, LR& vroi, size_t channel, size_t timeframe) { //***** phase 1: copy ROI metrics from the slide properties, thanks to the WSI scenario const SlideProps& p = env.dataset.dataset_props[sidx]; @@ -132,7 +136,7 @@ namespace Nyxus } //***** phase 2: extract features - featurize_triv_wholevolume (env, sidx, imlo, env.get_ram_limit(), vroi); // segmented counterpart: phase2.cpp / processTrivialRois () + featurize_triv_wholevolume (env, sidx, imlo, env.get_ram_limit(), vroi, channel, timeframe); // segmented counterpart: phase2.cpp / processTrivialRois () return true; } @@ -158,44 +162,49 @@ namespace Nyxus rv = 1; } - LR vroi(1); // virtual ROI representing the whole slide ROI-labelled as '1' - - if (featurize_wholevolume (env, slide_idx, imlo, vroi) == false) // non-wsi counterpart: processIntSegImagePair() - { - std::cerr << "Error featurizing slide " << p.fname_int << " @ " << __FILE__ << ":" << __LINE__ << "\n"; - rv = 1; - } + // FIX (IO): featurize every (channel, timeframe) plane and emit one whole-slide row + // per plane, tagged with c_index/t_index — mirroring the segmented path. A + // single-channel, single-timeframe slide reports 1/1, so its output is unchanged. + for (size_t c = 0; c < p.inten_channels; c++) + for (size_t t = 0; t < p.inten_time; t++) + { + LR vroi(1); // virtual ROI representing the whole slide ROI-labelled as '1' - // thread-safely save results of this single slide - if (write_apache) - { - auto [status, msg] = save_features_2_apache_wholeslide (env, vroi, p.fname_int); - if (!status) + if (featurize_wholevolume (env, slide_idx, imlo, vroi, c, t) == false) // non-wsi counterpart: processIntSegImagePair() { - std::cerr << "Error writing Arrow file: " << msg.value() << std::endl; - rv = 2; + std::cerr << "Error featurizing slide " << p.fname_int << " @ " << __FILE__ << ":" << __LINE__ << "\n"; + rv = 1; } - } - else - if (saveOption == SaveOption::saveCSV) + + // thread-safely save results of this single (slide, channel, timeframe) + if (write_apache) { - if (save_features_2_csv_wholeslide (env, vroi, p.fname_int, "", outputPath, - 0 // pass 0 as t_index - ) == false) + auto [status, msg] = save_features_2_apache_wholeslide (env, vroi, p.fname_int, t, c); + if (!status) { - std::cout << "error saving results to CSV file, details: " << __FILE__ << ":" << __LINE__ << std::endl; + std::cerr << "Error writing Arrow file: " << msg.value() << std::endl; rv = 2; } } else - { - // pulls feature values from 'vroi' and appends them to global object 'theResultsCache' exposed to Python API - if (save_features_2_buffer_wholeslide (env.theResultsCache, env, vroi, p.fname_int, "") == false) + if (saveOption == SaveOption::saveCSV) { - std::cerr << "Error saving features to the results buffer" << std::endl; - rv = 2; + if (save_features_2_csv_wholeslide (env, vroi, p.fname_int, "", outputPath, t, c) == false) + { + std::cout << "error saving results to CSV file, details: " << __FILE__ << ":" << __LINE__ << std::endl; + rv = 2; + } } - } + else + { + // pulls feature values from 'vroi' and appends them to global object 'theResultsCache' exposed to Python API + if (save_features_2_buffer_wholeslide (env.theResultsCache, env, vroi, p.fname_int, "", t, c) == false) + { + std::cerr << "Error saving features to the results buffer" << std::endl; + rv = 2; + } + } + } //- channels x timeframes imlo.close(); @@ -215,6 +224,8 @@ namespace Nyxus const SaveOption saveOption, const std::string& outputPath) { + env.reset_csv_output_state(); // FIX: this run's CSV files start fresh (see Environment::csv_paths_written) + //**** prescan all files size_t nf = intensFiles.size(); diff --git a/src/nyx/workflow_pythonapi.cpp b/src/nyx/workflow_pythonapi.cpp index 5f336e261..33dcd4035 100644 --- a/src/nyx/workflow_pythonapi.cpp +++ b/src/nyx/workflow_pythonapi.cpp @@ -147,13 +147,13 @@ namespace Nyxus if (write_apache) { - auto [status, msg] = env.arrow_stream.write_arrow_file (Nyxus::get_feature_values(env, env.theFeatureSet, env.uniqueLabels, env.roiData, env.dataset)); - if (!status) + auto [status, msg] = env.arrow_stream.write_arrow_file (Nyxus::get_feature_values(env, env.theFeatureSet, env.uniqueLabels, env.roiData, env.dataset, DEFAULT_T_INDEX, DEFAULT_C_INDEX)); + if (!status) return { "error writing Arrow file: " + msg.value() }; - } - else + } + else { - if (! save_features_2_buffer(env.theResultsCache, env, DEFAULT_T_INDEX)) + if (! save_features_2_buffer(env.theResultsCache, env, DEFAULT_T_INDEX, DEFAULT_C_INDEX)) return { "error saving results to a buffer" }; } diff --git a/tests/data/ometiff/dim3_emptymask.ome.tif b/tests/data/ometiff/dim3_emptymask.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..644030e823d9f683b1e70dc1a69d772cf9448e93 GIT binary patch literal 1743 zcmeHHO^ee|6n&YF#f1#IP(e50twhLItsxVVjL)l?pB_gr3}-bMJlko@6k1iS8qWP7qR#kpf8GDK%~bK2lm` z{2}00jU}($z$eNP`gDZQ`(uP2)!HAxdEDSUY2RtbduWr5c29~{#mwtjrZoTviaMMfe z)?M#NA04L4&j;-C%U|mf!8{H%OX;)bdnmDYwoPL71I-JTavxi|iB-y@bk6wF$J5EV z=HRoyyS#X-0+AO!UI~%8hOyaf^faS9VNsqIQJU@aG+!FyXhjoJ7$7h@re$lUjVqP``f#?y0^ke6RK~EIaa6BLEp#g zoV)8ao4ZyR*^y1C=5(D%v#f=sg_aX)VK;mo+NT|79zxf>oDz}dK5l&F)Q;J)bz8S^ z;0+S8q^e9@A{XZUjwH0cHGwf=TN)Q7rGk>vX9?wUN0<4WLiX8J?|OpO$u0xwnsH&l zoX6}Vo%z_Q(3OwtSPvFk#jbtat*Ff` literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim3_mask.ome.tif b/tests/data/ometiff/dim3_mask.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..5377cad0317b1bb9537ed0447df6fa88e47e8612 GIT binary patch literal 1743 zcmcgs&2G~`5MDPW;(#hR01{lRJs~9jQry(CU8ICcky09wL`r+=#@-|=*=x&oNP@(> z@Bo}Sa^yL93XX_b+mYNJB7y?8*8X<dz5&iN>0aUgE8yo9{s#C(O>2Pv z2VnaS>jvV8`1O^u>2%6MswQjrvi*`kxw!>%gT?z#_62FnV^@E7`CD8UpYHN^-TzKUsd>W0qh3S(XUuv9DT+SrYZD zFXwC(h4KfQW-R6|HdGzUl>2eO_{_zl;knYnXP$F;@m2;R&s;pu^TgJ)<#MUU3FQ&< z(>U|vWUa>OOdI%f8j(x`foAH4rRWwemG&xQx75p5g~HTzLmOZ8N~OYhmJ{w%P-V={ z3U|HOCwYp(g UFL*A(32u210L7u<4fEll-+p)FB>(^b literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim4_mask_c2.ome.tif b/tests/data/ometiff/dim4_mask_c2.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..bba34ac484b112883424d2c8f3ff1f0ba9cd59c6 GIT binary patch literal 2859 zcmeHJ-HOvd6i&9gs~589g$fE1=1Ro;+6^%@U7>ZM&~*!Kp}SX`pJ_XiOhS@wTQ9^b zpTno{QWV4o@Co)&JozcjE?A7DH)2aV-%P&moUdncl5{%HnA;4)954*K%dmiw8JpvM zz&mU$UB3&smg9uCU%&_K4)bk?VcvrHUaq|Y&iw-CK_TYD9A5zE5xbYh{|xwXj=unY zTF}Z6{|>DEfa9tL{$MN~F&U4?#Kl6egfp8zEGSpsgt@}W_$Tv}wBe_0yqo+Dq0>`0 z`DN$-kmpF`wPYoms65=qmeTUGAfrf))UPDLC#_7px4u~`|&@_$%T@>f@x!?zwdc+R> z$o7M!;D-~jZ%?sjMj{AAMUqusQcP z*Y#~PPW_cm&Xrb(zK4ZHD4zUc)nj(RvuO=RpYnM~uJ8BfJ#L(J*Fiy@baF@Sf)wi%mT=NL)@ c`FpNOam%04IQc_KzN{Dk(!#+A*I1MJ1qm1iZ~y=R literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim5_badifd.ome.tif b/tests/data/ometiff/dim5_badifd.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..2d91ed85af1be14ac1f6369513333b90b96d09fb GIT binary patch literal 1634 zcmcJP`F9gl6vsd5CTlG!#TLXkf+EsPQXsS?lZq*kur<&iZ9ySTChbTv2}w#?WN|@R z+>w1p1;Gt)0|kXYfgkzNKfyo2JIVAI&*5;O2Y;CJKJUJJzwgd__uWuvHe-NVa8%(C zvStpOYlJHtl|_4dQ2z*t}8v8y(d}{oCR{ z)#f|Gv&v#UqCY`={}Owq3ViKDsibp@p3NC)Q&T)Hx8l^zSUPT){hG2S5^QQw+Wo%H z6^otXXy!C!AfL~)scNB6aHTW4nKEM8bS{?847<|Vezhw$pr@iaRRYvzx5wM$_9}i~ zC>8D3ouLj*DN>tkvND>|HKi{aHxhC22pdCsGFNmdq2<$Iq;%6ttTP+evzpS~(;J8= z&d6{^LI({q@9`?mE@P|Sttl-f>d_S2)~hM=O16OfSBWAP`F#;1k?4r#qt2j_&E;iT z_9~*RkD!DLhJuo_jNxR|)C1{3GjA!V_bnm&7A0gHtwggeA>(Kz=Gqdit#{E-OXuzP zsl~gIQSQz!l`B#cH~Uu!dJzOE*3AvX1q<#_McgBT@Q@ z5l1IMKLZ%N!6uTVFiDePGg)%v8Dt9uh8SimZ?cW;>|iHv@iy=9F1vV-_u0)J_Og!; z_>hnIm{0hW&)CoB9N-{_ILr}_a*X4g;3TIw%^A+}1z++N=Qz*TT;L+#@Gal*JwI@X z%Ut0{e&T0-kzR0>Yy8IVT;~RV@FzF9WxZni?T@Cn(BXhORaEuZ>g7g idBs@#aI(Do5VO4Tzhzk`{q#Zl^u5lq^uqpop8o*mFa2)- literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim5_calibrated.ome.tif b/tests/data/ometiff/dim5_calibrated.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..d557df93416959643ceff7cb4aeaa36be1329c15 GIT binary patch literal 7273 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 literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim5_multitile.ome.tif b/tests/data/ometiff/dim5_multitile.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..862ff649077c62c936906e4efc20ec3b59e434bd GIT binary patch literal 13939 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 bonqobyA=LczJHY?ME)*aXc;=o3QGS2c7z<4 literal 0 HcmV?d00001 diff --git a/tests/data/ometiff/dim5_oddtile.ome.tif b/tests/data/ometiff/dim5_oddtile.ome.tif new file mode 100644 index 0000000000000000000000000000000000000000..d32bdd89df4b7df249a89cb5a2086b216173ed9c GIT binary patch literal 7347 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 path, distinct from the strip loaders.""" + path = os.path.join(HERE, name) + data = _data_for(order) + tifffile.imwrite(path, data, photometric="minisblack", metadata={"axes": order}, + tile=(16, 16), ome=True) + with tifffile.TiffFile(path) as tf: + import re + dimord = re.search(r'DimensionOrder="([A-Z]+)"', tf.ome_metadata).group(1) + print("wrote %-20s axes=%-6s DimensionOrder=%s IFDs=%d tiled=%s" + % (name, order, dimord, len(tf.pages), tf.pages[0].is_tiled)) + + +# Multi-tile fixture dims. TIFF tile sizes must be multiples of 16, so the plane has to +# exceed 16 in BOTH directions to produce a real tile grid -- hence its own (larger Y/X, +# smaller C/Z/T) shape rather than the 6x8 plane the other fixtures share. +MT_T, MT_C, MT_Z, MT_Y, MT_X = 1, 2, 2, 32, 48 + + +def write_multitile(name, T=MT_T, C=MT_C, Z=MT_Z, Y=MT_Y, X=MT_X): + """A tiled OME-TIFF whose every plane spans a GRID of 16x16 tiles (default 32x48 plane + -> 2x3 = 6 tiles), unlike dim5_tiled.ome.tif where a single tile covers the whole plane. + A volumetric read that fetches only tile (0,0) returns wrong data for everything outside + the first 16x16 corner. Same coordinate encoding as encoded_tczyx, its own dims. + + When Y or X is not a multiple of 16 the last tile of that row/column is PARTIAL, which + exercises the validH/validW seam clamp in the volumetric assembly.""" + t, c, z, y, x = np.meshgrid(np.arange(T), np.arange(C), np.arange(Z), + np.arange(Y), np.arange(X), indexing="ij") + data = (1 + ((((t * C + c) * Z + z) * Y + y) * X + x)).astype("uint16") + path = os.path.join(HERE, name) + tifffile.imwrite(path, data, photometric="minisblack", metadata={"axes": "TCZYX"}, + tile=(16, 16), ome=True) + with tifffile.TiffFile(path) as tf: + print("wrote %-24s %dx%d plane, tile grid %dx%d (last %dx%d), IFDs=%d tiled=%s max=%d" + % (name, Y, X, -(-Y // 16), -(-X // 16), Y % 16 or 16, X % 16 or 16, + len(tf.pages), tf.pages[0].is_tiled, data.max())) + + +def write_mask(name): + """Single-channel, single-timeframe 3D (ZYX) label mask matching dim5's geometry. + Used to test the 1-channel-mask : N-channel-intensity pairing: the mask is + channel-agnostic and must be reused for every intensity channel.""" + path = os.path.join(HERE, name) + m = np.zeros((Z, Y, X), "uint16") + m[:, 1:5, 1:7] = 1 # one ROI (label 1), interior so it has a real bbox + tifffile.imwrite(path, m, photometric="minisblack", metadata={"axes": "ZYX"}, ome=True) + print("wrote %-20s (single-channel ZYX label mask, ROI voxels=%d)" % (name, int(m.sum()))) + + +def write_reordered(name): + """An OME-TIFF whose planes are physically stored in REVERSED IFD order, with explicit + blocks mapping each logical (z,c,t) to its scrambled IFD. This is what a reader + that ignores TiffData and assumes canonical (contiguous-from-IFD-0) plane order gets wrong: + it would read the reversed plane's pixels. Writers like bioformats emit per-plane TiffData; + a non-canonical mapping (or a non-zero starting IFD) is exactly where the assumption breaks. + + Dims T=1,C=2,Z=3, plane 6x8, DimensionOrder XYZCT -> canonical ordinal ord = z + c*Z. The + logical plane with ordinal `ord` is stored at physical IFD (5 - ord). Same encoded value as + encoded_tczyx so the standard facade test can check it.""" + rT, rC, rZ = 1, 2, 3 + total = rZ * rC * rT + # physical IFD p holds the logical plane whose ordinal is (total-1 - p) + phys = np.empty((total, Y, X), "uint16") + tiffdata = [] + for c in range(rC): + for z in range(rZ): + ord_ = z + c * rZ # canonical XYZCT ordinal (t=0) + ifd = (total - 1) - ord_ # reversed physical IFD + yy, xx = np.meshgrid(np.arange(Y), np.arange(X), indexing="ij") + phys[ifd] = (1 + ((((0 * rC + c) * rZ + z) * Y + yy) * X + xx)).astype("uint16") + tiffdata.append('' + % (c, z, ifd)) + ome = ('' + '' + '' + '%s' + '' + % (X, Y, rZ, rC, rT, "".join(tiffdata))) + path = os.path.join(HERE, name) + tifffile.imwrite(path, phys, description=ome, metadata=None, photometric="minisblack") + with tifffile.TiffFile(path) as tf: + got = (tf.pages[0].description or "")[:4] + print("wrote %-24s IFDs=%d reversed, TiffData blocks=%d, desc starts %r" + % (name, len(tf.pages), len(tiffdata), got)) + + +def write_pyramid(name): + """A PYRAMIDAL OME-TIFF: every full-res plane's IFD carries downsampled resolution levels + as SubIFDs (TIFF tag 330). SubIFDs live OUTSIDE the main IFD chain, so they must not + disturb full-res (z,c,t) plane addressing -- TIFFNumberOfDirectories still returns Z (not + Z*levels) and ifdForPlane(z,..)->main-chain IFD still lands on the full-res plane. nyxus + featurizes level 0 only; this fixture guards that a pyramid does not silently shift which + plane we read. Z=6 z-stack, 32x48, tiled 16x16 (so also multi-tile), + 2 sub-levels. + Full-res encoding matches ometiff_enc_dims(x,y,z,0,0,C=1,Z=6,Y=32,X=48).""" + Zp, Yp, Xp = 6, 32, 48 + z, y, x = np.meshgrid(np.arange(Zp), np.arange(Yp), np.arange(Xp), indexing="ij") + base = (1 + ((z * Yp + y) * Xp + x)).astype("uint16") # full-res planes + path = os.path.join(HERE, name) + opts = dict(photometric="minisblack", tile=(16, 16)) + with tifffile.TiffWriter(path, ome=True) as tw: + tw.write(base, subifds=2, metadata={"axes": "ZYX"}, **opts) # reserve 2 subifds/plane + tw.write(base[:, ::2, ::2], subfiletype=1, **opts) # level 1 (16x24) + tw.write(base[:, ::4, ::4], subfiletype=1, **opts) # level 2 (8x12) + with tifffile.TiffFile(path) as tf: + print("wrote %-24s main-chain IFDs=%d (=Z), sub-levels/plane=%d, %dx%d tiled" + % (name, len(tf.pages), len(tf.series[0].levels) - 1, Yp, Xp)) + + +def write_calibrated_tiff(name): + """P2: an OME-TIFF carrying physical voxel spacing (PhysicalSizeX/Y/Z + units) in its + OME-XML, so the loader must surface physicalSize*/unit into SlideProps -- the TIFF twin of + dim5_calibrated.ome.zarr. Anisotropic: X=Y=0.5, Z=2.0 micrometer. TCZYX 2,3,4,6,8.""" + data = _data_for("TCZYX") + path = os.path.join(HERE, name) + tifffile.imwrite(path, data, photometric="minisblack", ome=True, metadata={ + "axes": "TCZYX", "PhysicalSizeX": 0.5, "PhysicalSizeY": 0.5, "PhysicalSizeZ": 2.0, + "PhysicalSizeXUnit": "micrometer", "PhysicalSizeYUnit": "micrometer", + "PhysicalSizeZUnit": "micrometer"}) + print("wrote %-24s (calibrated: physX/Y=0.5 physZ=2.0 micrometer)" % name) + + +def write_bad_ifd(name): + """N2: an OME-TIFF whose maps one plane to an IFD PAST the end of the file (an + in-file overrun, not a multi-file UUID). ifdForPlane returns it, so the read must throw + cleanly at TIFFSetDirectory, not crash. 4 planes stored, but plane (z3) claims IFD=99.""" + rT, rC, rZ = 1, 1, 4 + phys = np.empty((rZ, Y, X), "uint16") + yy, xx = np.meshgrid(np.arange(Y), np.arange(X), indexing="ij") + td = [] + for z in range(rZ): + phys[z] = (1 + ((z * Y + yy) * X + xx)).astype("uint16") + ifd = 99 if z == rZ - 1 else z # last plane points past EOF + td.append('' % (z, ifd)) + ome = ('' + '' + '%s' + '' % (X, Y, rZ, "".join(td))) + tifffile.imwrite(os.path.join(HERE, name), phys, description=ome, metadata=None, + photometric="minisblack") + print("wrote %-24s (TiffData IFD=99 on plane z3 -> read must throw)" % name) + + +def write_empty_mask(name): + """N3: an all-background (all-zero) single-channel ZYX mask. A segmented run finds ZERO + ROIs -- the pipeline must handle it (no ROIs, no crash), not divide-by-zero or read garbage.""" + m = np.zeros((Z, Y, X), "uint16") + tifffile.imwrite(os.path.join(HERE, name), m, photometric="minisblack", + metadata={"axes": "ZYX"}, ome=True) + print("wrote %-24s (empty mask -> 0 ROIs)" % name) + + +def write_multichannel_mask(name): + """N4: a MULTI-channel (CZYX, C=2) label mask, to pair with single-channel intensity. The + featurize loop iterates the INTENSITY's channels, so the extra mask channel must simply be + ignored (mask channel clamped to what the intensity asks for), not crash. Same ROI on both + mask channels so the result is channel-independent.""" + m = np.zeros((2, Z, Y, X), "uint16") + m[:, :, 1:5, 1:7] = 1 + tifffile.imwrite(os.path.join(HERE, name), m, photometric="minisblack", + metadata={"axes": "CZYX"}, ome=True) + print("wrote %-24s (C=2 label mask -> extra channel ignored)" % name) + + def write_bad_rgb(name): """RGB OME-TIFF -- nyxus is grayscale-only, so the loader must reject it.""" path = os.path.join(HERE, name) @@ -89,6 +258,19 @@ def main(): write_ome("dim3_zyx.ome.tif", "ZYX") write_ome("dim2_yx.ome.tif", "YX") write_plain("dim3_plain.tif", "ZYX") + # single-channel label mask (for the 1-mask : N-channel-intensity pairing test) + write_mask("dim3_mask.ome.tif") + # non-canonical plane->IFD mapping (planes stored reversed) -> honors TiffData + write_reordered("dim5_reordered.ome.tif") + # pyramidal OME-TIFF (SubIFD downsample levels) -> full-res addressing must be unaffected + write_pyramid("dim5_pyramid.ome.tif") + # TILED multi-plane OME-TIFF (5D) -> exercises the tile-loader (z,c,t)->IFD path + write_tiled("dim5_tiled.ome.tif", "TCZYX") + # planes spanning a real 2x3 tile grid -> multi-tile volumetric assembly + write_multitile("dim5_multitile.ome.tif") + # PARTIAL edge tiles: 40x24 plane / 16 -> 3x2 tiles, last row-tile 8 tall, last col-tile + # 8 wide. Exercises the seam clamp the exact-multiple multitile fixture leaves untested. + write_multitile("dim5_oddtile.ome.tif", T=1, C=2, Z=1, Y=40, X=24) # --- illegal / adversarial: must be rejected cleanly, not crash --- write_bad_rgb("bad_rgb.ome.tif") write_bad_corrupt("bad_corrupt.tif") diff --git a/tests/data/omezarr/dim3_mask.ome.zarr/.zattrs b/tests/data/omezarr/dim3_mask.ome.zarr/.zattrs new file mode 100644 index 000000000..3215e60cd --- /dev/null +++ b/tests/data/omezarr/dim3_mask.ome.zarr/.zattrs @@ -0,0 +1,36 @@ +{ + "multiscales": [ + { + "version": "0.4", + "axes": [ + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim3_mask.ome.zarr/.zgroup b/tests/data/omezarr/dim3_mask.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim3_mask.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim3_mask.ome.zarr/0/.zarray b/tests/data/omezarr/dim3_mask.ome.zarr/0/.zarray new file mode 100644 index 000000000..eb95421ba --- /dev/null +++ b/tests/data/omezarr/dim3_mask.ome.zarr/0/.zarray @@ -0,0 +1,19 @@ +{ + "shape": [ + 4, + 6, + 8 + ], + "chunks": [ + 1, + 6, + 8 + ], + "dtype": "{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_t& zMhQw$hH_M(5>=>14Qf$`dNiOBO=w07TG57fbf6Pm=td8E(T9EvU=TwX#t23+hH*?_ z5>uGQ3}!Khc`RTNmw1_1c$L?9oi})sw|JX(c$fEhpAYzukNB8R_>|9>&*yx>0>0!c zzUCXgWg*}3JwNaxKk+la@GHOZJAd#efAKf}@Gt*yi7mBdw%k_ON?T>CZH?txo~^ZY zw%#__M%!eYZHsNSZMNNZ*iPGJyKRr{wSBhV4%k6EWQXmD9kpY2+)mg@J7uTsjGeV} zcHS=7MWalTgB;}~XSv8#ZgQ80Jmn>C`N&s(@>hTY6{KK=C{$qzSA-%JrD(+{R&k0~ zf)bUaWThxoX-ZdyGL@xl<)}!-Dp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKPf|rK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_calibrated.ome.zarr/0/0.0.1.0.0 b/tests/data/omezarr/dim5_calibrated.ome.zarr/0/0.0.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..678ed89e2b57253198aa620a5b4f198239f69bdc GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?wBaw>(Sc5Mp&LEuMIZVxfI$r5ABF*vM$s9-5e#GygE^9;IGQ00Wf;R5 z!7+?v6r&l#v5aLL<2jBAOk@(1nZmO?$Md|vi@e0kyuz!z#_PPno4m!_yu-V^$NPN1 zhkV4xe8Q)E#^-#&mwd(7e8abV$M^iekNm{X{KBvN#_#;WpZvw&{KL66&*s|#TWE`H zu`RKsw#=5>3R`KbY_+YiwYJXI+XmZcn{2afv8}evw%ZQdX}fHc2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(37+B^p5p~x;uT)w4c_7%-s1y4;uAjO3%=qT zzT*de;un775B}mGgdnLjuHZ_p;%ctpTCU@IZs104;%08)R&L{V?%+-?aFM&Xn|rvI z`?#M6c#wy9m`8Y&$9SA4c#@}hnrC>H=XjnMc#)U5#AROQ6<*~vUgr(oW~iWh>q%*j_ZU@>Xc6FjLzzu&Z~ntnv*%3i@BPcxtoW1nwNQ-kNKLP`CEVmT95@> uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFAnOcPU@^K>Z)$)t{&>CUh1tr>Z^Y0uK^mUK^m+f8meI$t`QokQ5vl=8mn;{uL+u{ zNt&!FnyP7dmnyY!5uM4mci?A3=uoTO%94oLAtFRhtuommE9viR`o3I&M zuoc^|9XqfSyRaL3uowHV9|v#{hj182a1_UI94BxRr*Il)a2Drq9v5&Cmv9+Za23~Z z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=#pYR!9@D<@E89eEMOsvSj-ZZvW(@dU?r9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxRJWSa literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/.zattrs b/tests/data/omezarr/dim5_multichunk.ome.zarr/.zattrs new file mode 100644 index 000000000..0a8629e58 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/.zattrs @@ -0,0 +1,46 @@ +{ + "multiscales": [ + { + "version": "0.4", + "axes": [ + { + "name": "t", + "type": "time" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/.zgroup b/tests/data/omezarr/dim5_multichunk.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/.zarray b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/.zarray new file mode 100644 index 000000000..6b9be87e6 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/.zarray @@ -0,0 +1,23 @@ +{ + "shape": [ + 2, + 3, + 4, + 6, + 8 + ], + "chunks": [ + 1, + 1, + 1, + 3, + 4 + ], + "dtype": "tI1 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..bb3726bb64376d0c9d42517768e52c5076fd6225 GIT binary patch literal 24 fcmXqHFk&!fFk!G{uwt-guwigyaAI&~aA5!dA)o@k literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..2d89624b18a13bf062d7a8e5fde82235f3e6f30f GIT binary patch literal 24 fcmXqIFk>)huwbxduw$@iaA0s{aAR<1@L&J{BoG4c literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..8a0fbda12bd5168a8a854434533c1eb391c73e88 GIT binary patch literal 24 fcmeZd@M7?0@L>pK2x16k2w{k1h+>Flh+zN#FhB#} literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.1.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..f073787438e087df6d53c0e4bc7f2ebbb925205e GIT binary patch literal 24 fcmeZe@MG|22w(_h2xACmh+v3ih+~LnNMHZ}GOz>> literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..5705ab6aeb98b9b0c213fc3ef5979fdfc4b1ec69 GIT binary patch literal 24 fcmYdfNMcB4NMXoi$YRK5$YCgCC}JpPC}98qKHvoZ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..0bc4aa490b242bdc95e9d526d4c83415d529d526 GIT binary patch literal 24 fcmYdgNMlH6$Y982$YaQ7C}1dMC}SvRs9*p9K~M!R literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.2.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..d4e4e5da42d0d01c41bc6ae3c9cc58f128ca4bf7 GIT binary patch literal 24 fcmbZvX%Q literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.3.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.3.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..4b96183570889da2eee0d8eb41da5e3de67a699c GIT binary patch literal 24 gcmZ3Gi+hl$*_xIH^UwV0BXPpUjP6A literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.3.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.0.3.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..938a7ef30bc80160b5810370ddeb034070fb1efb GIT binary patch literal 24 gcmZ3>u#RCp!v=<}4BHsCGwfj4%dn4OKf?hA0BtY`j{pDw literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..a9c345b7f58b09a752daba5f4370e1010744ec0c GIT binary patch literal 24 gcmX@eaEReB!x4s)45t`QGn`?#$Z(0_GQ$-H0C`{ue*gdg literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..6dc3ccae9088d1ee2d0599e1a714254f8d495fcc GIT binary patch literal 24 gcmX@gaE#$N!wH774CffmGhAS}%5aV0I>QYH0DI60uK)l5 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..954cbeaa437402cdb9b13acbfe8ff0c2a28131bd GIT binary patch literal 24 gcmcb~aEswK!ySf)438KdGdy8<$?%HdHNzVQ0Ehqzp8x;= literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.0.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..102e4619adf54a8f1651bddfec0824aa5797f5b1 GIT binary patch literal 24 gcmcc1aF5|W!vlt=49^&zGrVAU%kYljJ;MhE0E%!5&j0`b literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..23f59fde65f3b0132fe67b1f0cf3d951c8df55bd GIT binary patch literal 24 gcmey!@QL9w!xx6148ItDGyGv-WMpDwW@KRm0D`aw!vFvP literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..77aaaed325f8366edb803c335f43457723932ebb GIT binary patch literal 24 gcmey$@QvX+!w-hP4F4GZGcYi+GO{tUGjcEj0DCD0^Z)<= literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.1.0 new file mode 100644 index 000000000..20efad3c8 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.1.0 @@ -0,0 +1,2 @@ +  +   \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.1.1 new file mode 100644 index 000000000..50e6aad38 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.1.1.1 @@ -0,0 +1 @@ +   \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.0.0 new file mode 100644 index 000000000..7505c70f2 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.0.0 @@ -0,0 +1 @@ +!"#$)*+,1234 \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.0.1 new file mode 100644 index 000000000..896e76dc8 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.0.1 @@ -0,0 +1 @@ +%&'(-./05678 \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.1.0 new file mode 100644 index 000000000..10348b294 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.1.0 @@ -0,0 +1 @@ +9:;<ABCDIJKL \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.1.1 new file mode 100644 index 000000000..5a561cc2a --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.2.1.1 @@ -0,0 +1 @@ +=>?@EFGHMNOP \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.0.0 new file mode 100644 index 000000000..03e422fd6 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.0.0 @@ -0,0 +1 @@ +QRSTYZ[\abcd \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.0.1 new file mode 100644 index 000000000..532ad6295 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.0.1 @@ -0,0 +1 @@ +UVWX]^_`efgh \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.1.0 new file mode 100644 index 000000000..08d7221e3 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.1.0 @@ -0,0 +1 @@ +ijklqrstyz{| \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.1.1 new file mode 100644 index 000000000..ecf4d9353 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.1.3.1.1 @@ -0,0 +1 @@ +mnopuvwx}~ \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.0.0 new file mode 100644 index 000000000..ccd40725f --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.0.1 new file mode 100644 index 000000000..33a72bc45 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.0.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.1.0 new file mode 100644 index 000000000..38bb58d91 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.1.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.1.1 new file mode 100644 index 000000000..ae8b9729e --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.0.1.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.0.0 new file mode 100644 index 000000000..ef8c57048 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.0.1 new file mode 100644 index 000000000..1bc969c02 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.0.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.1.0 new file mode 100644 index 000000000..498c077bd --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.1.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.1.1 new file mode 100644 index 000000000..69b4e588e --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.1.1.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.0.0 new file mode 100644 index 000000000..32030d08b --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.0.1 new file mode 100644 index 000000000..a7b244a42 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.0.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.1.0 new file mode 100644 index 000000000..9dab26a86 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.1.0 @@ -0,0 +1,2 @@ +  +   \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.2.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..c4e759841edb88ae5e93e63a77af25ba036c1633 GIT binary patch literal 24 fcmey%_>b{FBLfpF6B`pd69*G76CV>llK>L{M9~9e literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.0.0 new file mode 100644 index 000000000..ba4ab3774 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.0.0 @@ -0,0 +1 @@ +!"#$ \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.0.1 new file mode 100644 index 000000000..8ceadd6ce --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.0.1 @@ -0,0 +1 @@ + %&'( \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.1.0 new file mode 100644 index 000000000..e288e02a0 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.1.0 @@ -0,0 +1 @@ +)*+,12349:;< \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.1.1 new file mode 100644 index 000000000..d0032195f --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/0.2.3.1.1 @@ -0,0 +1 @@ +-./05678=>?@ \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.0.0 new file mode 100644 index 000000000..90eba08ef --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.0.0 @@ -0,0 +1 @@ +ABCDIJKLQRST \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.0.1 new file mode 100644 index 000000000..c8f3df211 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.0.1 @@ -0,0 +1 @@ +EFGHMNOPUVWX \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.1.0 new file mode 100644 index 000000000..f2534bd15 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.1.0 @@ -0,0 +1 @@ +YZ[\abcdijkl \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.1.1 new file mode 100644 index 000000000..0771cddb3 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.0.1.1 @@ -0,0 +1 @@ +]^_`efghmnop \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.0.0 new file mode 100644 index 000000000..da21a11f3 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.0.0 @@ -0,0 +1 @@ +qrstyz{| \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.0.1 new file mode 100644 index 000000000..11ff0eb49 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.0.1 @@ -0,0 +1 @@ +uvwx}~ \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.1.0 new file mode 100644 index 000000000..ef326d17a --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.1.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.1.1 new file mode 100644 index 000000000..095916ce2 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.1.1.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.0.0 new file mode 100644 index 000000000..c754de7d9 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.0.1 new file mode 100644 index 000000000..3fc7c5626 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.0.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.1.0 new file mode 100644 index 000000000..f1ab8d54e --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.1.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.1.1 new file mode 100644 index 000000000..79919cf8f --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.2.1.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.0.0 new file mode 100644 index 000000000..2498f594e --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.0.1 new file mode 100644 index 000000000..402050ce9 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.0.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.1.0 new file mode 100644 index 000000000..e77d3f52f --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.1.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.0.3.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..9f39c2bb49638d7a72af7154982f83e3bc082c65 GIT binary patch literal 24 gcmaFM^p5F0(+8%nOy8KkGyP!t%k+=wKNABp0FzM)r~m)} literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.0.0 new file mode 100644 index 000000000..18af2a38f --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.0.0 @@ -0,0 +1,2 @@ +  +   \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.0.1 new file mode 100644 index 000000000..4c6e6ccdf --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.0.1 @@ -0,0 +1 @@ +  \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.1.0 new file mode 100644 index 000000000..34ca0c10e --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.1.0 @@ -0,0 +1 @@ +!"#$)*+, \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.1.1 new file mode 100644 index 000000000..a62a88bd6 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.0.1.1 @@ -0,0 +1 @@ + %&'(-./0 \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.0.0 new file mode 100644 index 000000000..095103132 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.0.0 @@ -0,0 +1 @@ +12349:;<ABCD \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.0.1 new file mode 100644 index 000000000..1800462e5 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.0.1 @@ -0,0 +1 @@ +5678=>?@EFGH \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.1.0 new file mode 100644 index 000000000..555fdeb6d --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.1.0 @@ -0,0 +1 @@ +IJKLQRSTYZ[\ \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.1.1 new file mode 100644 index 000000000..f3cd39495 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.1.1.1 @@ -0,0 +1 @@ +MNOPUVWX]^_` \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.0.0 new file mode 100644 index 000000000..9d717834e --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.0.0 @@ -0,0 +1 @@ +abcdijklqrst \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.0.1 new file mode 100644 index 000000000..e4cef603f --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.0.1 @@ -0,0 +1 @@ +efghmnopuvwx \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.1.0 new file mode 100644 index 000000000..5573f5f09 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.1.0 @@ -0,0 +1 @@ +yz{| \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.1.1 new file mode 100644 index 000000000..68c391241 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.2.1.1 @@ -0,0 +1 @@ +}~ \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.0.0 new file mode 100644 index 000000000..15832f810 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.0.1 new file mode 100644 index 000000000..afbcd4db4 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.0.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.1.0 new file mode 100644 index 000000000..e431e7f10 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.1.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.1.1 new file mode 100644 index 000000000..2108967af --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.1.3.1.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.0.0 new file mode 100644 index 000000000..929d22a8d --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.0.1 new file mode 100644 index 000000000..6a48804c6 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.0.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.1.0 new file mode 100644 index 000000000..04e074163 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.1.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.1.1 new file mode 100644 index 000000000..2b0cec5ce --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.0.1.1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.0.0 new file mode 100644 index 000000000..acbf29f9c --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..0f3d38f4b358bde665c22df8178752b4500a3ee4 GIT binary patch literal 24 gcmey${EhiL^AF~~%>S7GGc&NTvaqqRvv9Bg0DRd87ytkO literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.1.0 new file mode 100644 index 000000000..81234e044 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.1.0 @@ -0,0 +1,2 @@ +  +   \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.1.1 new file mode 100644 index 000000000..18e42d2b3 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.1.1.1 @@ -0,0 +1 @@ +   \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.0.0 new file mode 100644 index 000000000..0ec251090 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.0.0 @@ -0,0 +1 @@ +!"#$)*+,1234 \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.0.1 new file mode 100644 index 000000000..62eeb98e4 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.0.1 @@ -0,0 +1 @@ +%&'(-./05678 \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.1.0 new file mode 100644 index 000000000..6787fc38c --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.1.0 @@ -0,0 +1 @@ +9:;<ABCDIJKL \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.1.1 new file mode 100644 index 000000000..e8472acbb --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.2.1.1 @@ -0,0 +1 @@ +=>?@EFGHMNOP \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.0.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.0.0 new file mode 100644 index 000000000..bf9cdef34 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.0.0 @@ -0,0 +1 @@ +QRSTYZ[\abcd \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.0.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.0.1 new file mode 100644 index 000000000..34681a52a --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.0.1 @@ -0,0 +1 @@ +UVWX]^_`efgh \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.1.0 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.1.0 new file mode 100644 index 000000000..b95127c15 --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.1.0 @@ -0,0 +1 @@ +ijklqrstyz{| \ No newline at end of file diff --git a/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.1.1 b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.1.1 new file mode 100644 index 000000000..35fd5cfdc --- /dev/null +++ b/tests/data/omezarr/dim5_multichunk.ome.zarr/0/1.2.3.1.1 @@ -0,0 +1 @@ +mnopuvwx}~ \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/.zattrs b/tests/data/omezarr/dim5_oddchunk.ome.zarr/.zattrs new file mode 100644 index 000000000..0a8629e58 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/.zattrs @@ -0,0 +1,46 @@ +{ + "multiscales": [ + { + "version": "0.4", + "axes": [ + { + "name": "t", + "type": "time" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/.zgroup b/tests/data/omezarr/dim5_oddchunk.ome.zarr/.zgroup new file mode 100644 index 000000000..cab13da6e --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/.zarray b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/.zarray new file mode 100644 index 000000000..346491b10 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/.zarray @@ -0,0 +1,23 @@ +{ + "shape": [ + 2, + 3, + 4, + 6, + 8 + ], + "chunks": [ + 1, + 1, + 1, + 4, + 5 + ], + "dtype": "=P006*K8WICBDEH%yP$^7#P}qya4e literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.0.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.0.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..afd281029e8bffa7926681764ffc4842e0fecaf6 GIT binary patch literal 40 icmZQ$U}xZ900BM*eg*+BON>FBK?2N@V~}T10J8uO=P006-IlG2bEh(YQ9-{~&c?4rXdxnxDv+`3_Uyq3=oUt|RB literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.1.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.1.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..692ef43915ce65fc281064e2da4a35c5c1e6aa57 GIT binary patch literal 40 icmXqGFlVq}00BD&dj=P006-Is?v}ch(YP^-36OnbT}oKtf-n>H%yP$^7#R-BnYhl literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.2.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.2.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..9894ec667dca4e05d36f1d0603afe62ca25d5c43 GIT binary patch literal 40 icmYdeNN31k0D(M)e1-xrtBj$Xp#sdRW2k3n0J8vhWCdja literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.2.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.2.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..f854da56ec95177d64597a927f8d4bf41203e8aa GIT binary patch literal 40 ccmZo=P006-I|E4q~24Yb9V|T%37adN?B`d1t)(z9+wS0d39 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.3.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.3.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..050bec0f02ea4c98a67975102b8a440491dd5817 GIT binary patch literal 40 kcmbQnFr8rr0|?Axn9r~P%v#2@(g!C1 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.3.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.0.3.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..31bc37e4a5828b981ef90310f2a0dcccfdc5b293 GIT binary patch literal 40 YcmdnSu$^HC0|@M6*w1hP%)&+h0FX)qV*mgE literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.0.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..2e746188bb1166e28eb9a9816d550361b5a8c2f6 GIT binary patch literal 40 rcmV~$F#>=P006-I|F4vW#6S#6f9WpR?4rXdxnxDv+`3_Uyq3=oOS2Fy literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.0.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.0.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..493692693abd9fc460f68ad9fb555550b9013944 GIT binary patch literal 40 kcmX@caGc=;0|=aBIL~ka%(}*Ko#6(Ub&ugb!vio20N&&Ylhc+ck literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..d6b3714698a141e0bf232a484cd3cd477a2af619 GIT binary patch literal 40 tcmV~$!4`lJ006;VaZvgH?@Syb@0oe1yfiu=gUQcAgNM;%_Vn_$xB)EJ3HJa1 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..26e7b6a61e07d2b1c7ba1db570ac6b2aa1c94756 GIT binary patch literal 40 kcmeyy@SWiY0|@+M_|L$=2x74@vNLjkS$vHAi~?X50ILNBEC2ui literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..36fe9e0264ee6c6166d8b2e5bc0486ae9b091c3a GIT binary patch literal 40 dcmWe+6k-%+6k!x)lw_1*lxCD+lx1YV0stI70Pz3- literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.1.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..6d94ae5a7fc0a6702141a81f9f33a9622a4c2466 GIT binary patch literal 40 ZcmWe*6lauRWB>v=MtMdBFbl*(2LKgH0I2`~ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.0.0 new file mode 100644 index 000000000..1b796e717 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.0.0 @@ -0,0 +1 @@ +!"#$%)*+,-123459:;<= \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..efbd57413a5f8dc3425bd8251cfcd588fe244f4c GIT binary patch literal 40 jcmY#mRAv^Mtw#DFw2b5oY4ZzvSYMobO5seGi(BJ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..aacfafa578fa59c509527d255318bb79df6a06dc GIT binary patch literal 40 dcmZ={bYgU7bYXO5^knp6^k(#7^krng0svNC0?_~f literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.2.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..22a9a324c5a1b0e49e81677bfa2485a961205bb3 GIT binary patch literal 40 ZcmZ=`bZ7KnWB>v`Mt{ZtFbl*(2LL*10m%RW literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.0.0 new file mode 100644 index 000000000..2295039d6 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.0.0 @@ -0,0 +1 @@ +QRSTUYZ[\]abcdeijklm \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..0148872c2aab17734b4ede69130cc832940bfd70 GIT binary patch literal 40 jcmWGz3}=jBWB`IV#(2gAFe{BQoiPK<%45uDEC90rXDkG< literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..ddd255b9afcf7d30c4a9a02d844164a47ca845b8 GIT binary patch literal 40 dcmXS9EMhEXEMY8VtYoZWtY)lXtYu`t0sxSH1hD`B literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.1.3.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..3c25885ac5ae1a271aee93c8992abd89ac76a696 GIT binary patch literal 40 ZcmXS8EN84>WB`IX#(KsEFbl*(2LNA+0_gw% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.0.0 new file mode 100644 index 000000000..067b7edf3 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..f53f9246a50e57ea08d55c58a846612b635b5e63 GIT binary patch literal 40 kcmZo;Y-j9XWB`Ib#(u^LVAeFo>5MbLta*&{85e+A0GjLu@c;k- literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..ef189c6c9c464860a3a6156707c1994c5661b207 GIT binary patch literal 40 fcmZ3;xQKBv;}XWDj4K&eF|KA@!?>1_0Sf>C%AN+1 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.0.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..4b1e7cc96c0235781b49de55fb3f9dbbb0854c8a GIT binary patch literal 40 acmZ3+xSVkXBLfhuV_eU;0n7sN&;bC1sssrD literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.0.0 new file mode 100644 index 000000000..da7c04c89 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..945668e751021852ee17d4227115525caa0a6302 GIT binary patch literal 40 lcmdnSxSeqaBLfiZW8BYp0L(hZc%1PBn01cvJmUp03jofn2{8Zw literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..20e6506481929eaf2abfef15d3b855bdda416ba6 GIT binary patch literal 40 fcmcb}c!}{c;}yoMj5ir?G2Ujp!+4jG0Sf>C1kDI> literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.1.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..cdf4b2637d0530eaf08389b37c3a8733be5b91c1 GIT binary patch literal 40 acmcb{c%AVEBLfiJW4zD!0L%jM&;bCc$^|I^ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.0.0 new file mode 100644 index 000000000..0453f3ed3 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..85a5bef83105ec9c70821f6701bbf290fa6aed2f GIT binary patch literal 40 mcmaFH_?+r}0nGZw_?__wnDvkGKO+MZNEQJ6aSCz( literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..800e79b618e04a899746e121ad63f97b1509a580 GIT binary patch literal 40 dcmZQ%Vq#)uVqs!s;$-4t;%4Gu;$>pM0ssmM09yb6 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.2.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..3f8fe2e52f08dd126def479bd1ea7807ad796481 GIT binary patch literal 40 ZcmZQ$VrSxDVgLd@CVnOXFbl*(2LK7G08an_ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.0.0 new file mode 100644 index 000000000..6881dd599 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.0.0 @@ -0,0 +1 @@ +!"#$%)*+,- \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..e863633f3ff7f9f37bd2251522bb2d8aed5ec782 GIT binary patch literal 40 jcmWe*5@(WNVgLd;CV3_WFiVX|ok;`C(qqzRG61sxBQycM literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..4a3d0d1740a1c894bc1859b16f0d7d28025d1670 GIT binary patch literal 40 dcmXqHGGa1jGGQ`hvShMivSzYjvSnhx0surR0y_Wz literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/0.2.3.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..a9d8e3ae67fc1d2b80662f2e2eae4b471fd9c262 GIT binary patch literal 40 ZcmXqGGH0@2VgLd=CVM6aFbl*(2LLY00dD{R literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.0.0 new file mode 100644 index 000000000..1c1dede81 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.0.0 @@ -0,0 +1 @@ +ABCDEIJKLMQRSTUYZ[\] \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..c692e4ca2975ac97881b663d3a723042e24b628f GIT binary patch literal 40 jcmZ=`a%b{jVgLd^CV!>?Fe{8HoGAj#ierjrN&vF}R`dh? literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..99e0b2606a1e5f2ae3e52759d2601bc7c60507c4 GIT binary patch literal 40 dcmYdfN@7Z8N?}T6%4Eu7%4W)8%4K4}0swwW1RDSV literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.0.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..d4f3d927d5f7138cf753cb5fa0220e5605c6bd48 GIT binary patch literal 40 ZcmYdeN@vPoVgQ0XrhKLXFbl*(2LMy*0*?Ry literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.0.0 new file mode 100644 index 000000000..53d610014 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.0.0 @@ -0,0 +1 @@ +qrstuyz{|} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..3859597a27b98ebb63ea32835eaa7bee67a67aee GIT binary patch literal 40 kcmXS8Drc%-VgQ0Vrh29ZFsqHJov8!N>SO9>ngC`20E)B*Jpcdz literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..3dd1b6b75ec0148bcb4d1af021eef889fbf6b349 GIT binary patch literal 40 fcmbQpG>K_4(-fwuOf#8gG0kS0!!(zP0Sf>CxM~Ia literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.1.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..0040245d00ba0aa72374a707469c933477321273 GIT binary patch literal 40 acmbQnG@WS%69W*;W17#j0L%jM&;bB>3IneI literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.0.0 new file mode 100644 index 000000000..2db1cb9d3 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..d2f636085ea68fb3bf1ff879f4b564f8dddf88df GIT binary patch literal 40 lcmZ3+w47-L69W*eV_MI&0nFOQw4G@On6-~-Khps)3jn@!2z>wm literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..cd08d53345289a125c59df593a855b654ae5f36d GIT binary patch literal 40 fcmX@ebcpFN(-EelOedL6F`Z^Q!*rI30Sf>C@`?xB literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.2.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..ecfeb4ad3e57964b14e82fbbab4c472eb575661e GIT binary patch literal 40 acmX@cbe!n~69W*OV>-`t0n7sN&;bCRDg@5} literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.0.0 new file mode 100644 index 000000000..95eda433f --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..f1eafaccb95d000f088b83a0ecf362e03e7fe515 GIT binary patch literal 40 lcmcb{be-u269W+3W4h1u0L*&E^qlDhnDvh7J<|s;3jp#o3cUaT literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..ee582c1ae4d26247a442748833a79b433d40e009 GIT binary patch literal 40 fcmey!^oi*+(-)?%Oh1`^G5u!x!}OPl0Sf>CEV&80 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.0.3.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..7a2e6248c743eefa221dd79ec2e2e4daac55cb51 GIT binary patch literal 40 Zcmeyy^quJk69W+ZWBSj;zzkwx0|2RG1n>X= literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.0.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.0.0.0 new file mode 100644 index 000000000..2697929e6 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.0.0.0 @@ -0,0 +1,2 @@ +  +    \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.0.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.0.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..67da0a1d3c4e2a9beefb3bb617f3b66cfb731ff0 GIT binary patch literal 40 jcmZQ$W@qMLW&i>{W`1S?FiVVCoLK_Ql4F)`W_@M@Fbl*(2LK}~0TloM literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.0.0 new file mode 100644 index 000000000..510744058 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.0.0 @@ -0,0 +1 @@ +123459:;<=ABCDEIJKLM \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..5bc5f4691df177ece8dc51e88313ae832f3e466a GIT binary patch literal 40 jcmXqGHfOeAW&i>^W_xA_Fw2eEo!JA-@?-XA4gj+NM!W+_ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..aee9943751a50e49b340fc1a81f7b3602375e0d2 GIT binary patch literal 40 dcmWG!4q^^w4q*;uj%1Evj%JQwj%8-R0sw3l1BCzp literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.1.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..e2d717019dea6d7a4dce8acf4bd86d899ced6143 GIT binary patch literal 40 ZcmWGz4rh*FW&naX=6L1=Fbl*(2LMP)0yO{t literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.0.0 new file mode 100644 index 000000000..6b14f9717 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.0.0 @@ -0,0 +1 @@ +abcdeijklmqrstuyz{|} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..d2f3ec59492fbfea891d6d838fb7dc9858ab9c5a GIT binary patch literal 40 jcmYdePG``?qz1c0sy8q1!VvL literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.2.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..916adecbfecf046b2a34f586e688d324b64e1f7b GIT binary patch literal 40 ZcmZo;ZfEXbW&nad=6>c0U>1mn4ghUx162S3 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.0.0 new file mode 100644 index 000000000..17c9c63e8 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..03e045ef03e8f0773c05a0df617c0a24504e7001 GIT binary patch literal 40 lcmbQnJe_$4GXoIJW1i2v0L)s(yqtLjn6-|1J@W=I3jnS>2gv{c literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..4b9752263013fa5f3b2612e21b61b2357139a5b8 GIT binary patch literal 40 fcmdnUyoq@;^A_f<%sZKPG4E#H!@QT70Sf>C;8q7k literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.1.3.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..875a85532c507f9aa84533e214fe7e42dd53da22 GIT binary patch literal 40 acmdnSyq$RmGXoIpW8Tkv0L%jM&;bCFiUf54 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.0.0 new file mode 100644 index 000000000..439c8135e --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.0.0 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..61f639f87961f06799d40d7b399a4693d301ed17 GIT binary patch literal 40 lcmX@ce4P0NGXoHuV?NJ(0nEC_e4Y6Qn01f&KJx=G3jpE#3JCxJ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..324a30aeb404e8bf4ea1c95c7f4ed4045eb9ee7c GIT binary patch literal 40 fcmaFJ{D}E6^AqN$%rBW=F~4Sh!~B++0Sf>C8iffZ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.0.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..56870d4fba2f621115e125b5811c8b50d50ac4f9 GIT binary patch literal 40 acmaFH{G9m(GXoI3V}8&40n7sN&;bCqss)t* literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.0.0 new file mode 100644 index 000000000..c4ec83492 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.0.0 @@ -0,0 +1,2 @@ +  +    \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..9d4859413a861941b139f9a950d4b8fa3786de7a GIT binary patch literal 40 lcmeyy{GIs+GXoI(WB$*~zye~iv9PmnfLVMj{44@s767gt1yBG0 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..a239d8568fc7855b7f87d641def0e05ff6643394 GIT binary patch literal 40 dcmWe+5n>T$5n&N!kz|o#k!F!$k!4}P0stSv0S^EG literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.1.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..52e40880414f06b502a58a2d5b424b2cd91e905a GIT binary patch literal 40 ZcmWe*5oeKLVE_U-7I_v0Fbl*(2LKm}0J{JH literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.0.0 new file mode 100644 index 000000000..673be84d9 --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.0.0 @@ -0,0 +1 @@ +!"#$%)*+,-123459:;<= \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..9f380b5631118e7c14d236f5497add9e33cdb6b3 GIT binary patch literal 40 jcmY#mQD@O$VE_U>7JU{2Fw2a^oW%mnvSYDlaR9RbHiQC| literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..d955cc5697955ebab64a14c9dcb6387ae7b0ac83 GIT binary patch literal 40 dcmZ={abj_1aba;~@nrF0@n-R1@nvDa0svX!0`C9- literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.2.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..0ec70c34d9f014476c7db43dc246f72993bef0f1 GIT binary patch literal 40 ZcmZ=`acA*hVE_U@7JrriFbl*(2LL>(0owoo literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.0.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.0.0 new file mode 100644 index 000000000..bf94cb90a --- /dev/null +++ b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.0.0 @@ -0,0 +1 @@ +QRSTUYZ[\]abcdeijklm \ No newline at end of file diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.0.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.0.1 new file mode 100644 index 0000000000000000000000000000000000000000..438a2b92045afa924502db6ee74941932dfc5619 GIT binary patch literal 40 jcmWGz31^95VE}?SmUxx~Fe{BEoh1Xz%45lADFCwoYD5Ip literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.1.0 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.1.0 new file mode 100644 index 0000000000000000000000000000000000000000..625d3bab7be275a53d793e345522d72c613f770d GIT binary patch literal 40 dcmXS9DPk#RDPbvPsbr~Qsb;BRsbyin0sxc(1kV5f literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.1.1 b/tests/data/omezarr/dim5_oddchunk.ome.zarr/0/1.2.3.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..acfc915011380ad1a151dede9991ab54a62ed6c3 GIT binary patch literal 40 ZcmXS8DQBr*VE}?UmU@;3Fbl*(2LNHp0{Z{} literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3.ome.zarr/0/c/0/0/0/0/0 b/tests/data/omezarr/dim5_v3.ome.zarr/0/c/0/0/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..5ad5ba423979d7284d67888e0bdca68c024ffe27 GIT binary patch literal 576 zcmV~$17Z*W7zN<>{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_t& zMhQw$hH_M(5>=>14Qf$`dNiOBO=w07TG57fbf6Pm=td8E(T9EvU=TwX#t23+hH*?_ z5>uGQ3}!Khc`RTNmw1_1c$L?9oi})sw|JX(c$fEhpAYzukNB8R_>|9>&*yx>0>0!c zzUCXgWg*}3JwNaxKk+la@GHOZJAd#efAKf}@Gt*yi7mBdw%k_ON?T>CZH?txo~^ZY zw%#__M%!eYZHsNSZMNNZ*iPGJyKRr{wSBhV4%k6EWQXmD9kpY2+)mg@J7uTsjGeV} zcHS=7MWalTgB;}~XSv8#ZgQ80Jmn>C`N&s(@>hTY6{KK=C{$qzSA-%JrD(+{R&k0~ zf)bUaWThxoX-ZdyGL@xl<)}!-Dp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKPf|rK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3.ome.zarr/0/c/0/0/1/0/0 b/tests/data/omezarr/dim5_v3.ome.zarr/0/c/0/0/1/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..678ed89e2b57253198aa620a5b4f198239f69bdc GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?wBaw>(Sc5Mp&LEuMIZVxfI$r5ABF*vM$s9-5e#GygE^9;IGQ00Wf;R5 z!7+?v6r&l#v5aLL<2jBAOk@(1nZmO?$Md|vi@e0kyuz!z#_PPno4m!_yu-V^$NPN1 zhkV4xe8Q)E#^-#&mwd(7e8abV$M^iekNm{X{KBvN#_#;WpZvw&{KL66&*s|#TWE`H zu`RKsw#=5>3R`KbY_+YiwYJXI+XmZcn{2afv8}evw%ZQdX}fHc2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(37+B^p5p~x;uT)w4c_7%-s1y4;uAjO3%=qT zzT*de;un775B}mGgdnLjuHZ_p;%ctpTCU@IZs104;%08)R&L{V?%+-?aFM&Xn|rvI z`?#M6c#wy9m`8Y&$9SA4c#@}hnrC>H=XjnMc#)U5#AROQ6<*~vUgr(oW~iWh>q%*j_ZU@>Xc6FjLzzu&Z~ntnv*%3i@BPcxtoW1nwNQ-kNKLP`CEVmT95@> uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFAnOcPU@^K>Z)$)t{&>CUh1tr>Z^Y0uK^mUK^m+f8meI$t`QokQ5vl=8mn;{uL+u{ zNt&!FnyP7dmnyY!5uM4mci?A3=uoTO%94oLAtFRhtuommE9viR`o3I&M zuoc^|9XqfSyRaL3uowHV9|v#{hj182a1_UI94BxRr*Il)a2Drq9v5&Cmv9+Za23~Z z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=#pYR!9@D<@E89eEMOsvSj-ZZvW(@dU?r9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxRJWSa literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3.ome.zarr/0/zarr.json b/tests/data/omezarr/dim5_v3.ome.zarr/0/zarr.json new file mode 100644 index 000000000..2ae28718d --- /dev/null +++ b/tests/data/omezarr/dim5_v3.ome.zarr/0/zarr.json @@ -0,0 +1,41 @@ +{ + "shape": [ + 2, + 3, + 4, + 6, + 8 + ], + "data_type": "uint16", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 2, + 3, + 1, + 6, + 8 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_v3.ome.zarr/zarr.json b/tests/data/omezarr/dim5_v3.ome.zarr/zarr.json new file mode 100644 index 000000000..ea6be0fcd --- /dev/null +++ b/tests/data/omezarr/dim5_v3.ome.zarr/zarr.json @@ -0,0 +1,52 @@ +{ + "attributes": { + "ome": { + "version": "0.5", + "multiscales": [ + { + "axes": [ + { + "name": "t", + "type": "time" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/c/0/0/0/0/0 b/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/c/0/0/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..5ad5ba423979d7284d67888e0bdca68c024ffe27 GIT binary patch literal 576 zcmV~$17Z*W7zN<>{x{pUyE9N-8iIKu_5aDzKM z;0Z5y!w0_bgFgZgh#&+b1fd8+I3f^NJBa@kclj0BL_t& zMhQw$hH_M(5>=>14Qf$`dNiOBO=w07TG57fbf6Pm=td8E(T9EvU=TwX#t23+hH*?_ z5>uGQ3}!Khc`RTNmw1_1c$L?9oi})sw|JX(c$fEhpAYzukNB8R_>|9>&*yx>0>0!c zzUCXgWg*}3JwNaxKk+la@GHOZJAd#efAKf}@Gt*yi7mBdw%k_ON?T>CZH?txo~^ZY zw%#__M%!eYZHsNSZMNNZ*iPGJyKRr{wSBhV4%k6EWQXmD9kpY2+)mg@J7uTsjGeV} zcHS=7MWalTgB;}~XSv8#ZgQ80Jmn>C`N&s(@>hTY6{KK=C{$qzSA-%JrD(+{R&k0~ zf)bUaWThxoX-ZdyGL@xl<)}!-Dp9G*RIUnDs!G+WQLXA!uLd=$NzH0etJ>7A4t1(a k-Re=V`qZxh4Qfck8qui6G_DCvYD&|Z(X8e)uLUjYKPf|rK>z>% literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/c/0/0/1/0/0 b/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/c/0/0/1/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..678ed89e2b57253198aa620a5b4f198239f69bdc GIT binary patch literal 576 zcmV~$1ws%25Cp*8+e1L5Bm|_sKuL>82`DI~0t#QSySuyd3wC#RcYZ-S#hFy3AsrdW z#CT*O8#%~D9`aFu2`I!w6k!sIQG!yGp&XM@fl5?i3Z|kOHJFC!s6`#>(SSzGz)Z|S z6PnS2*_eY?wBaw>(Sc5Mp&LEuMIZVxfI$r5ABF*vM$s9-5e#GygE^9;IGQ00Wf;R5 z!7+?v6r&l#v5aLL<2jBAOk@(1nZmO?$Md|vi@e0kyuz!z#_PPno4m!_yu-V^$NPN1 zhkV4xe8Q)E#^-#&mwd(7e8abV$M^iekNm{X{KBvN#_#;WpZvw&{KL66&*s|#TWE`H zu`RKsw#=5>3R`KbY_+YiwYJXI+XmZcn{2afv8}evw%ZQdX}fHc2fHbwQYB%KnkTuilszKrA*4DLMo+7 zs-;G1rB3RlK^mn=nx#crrA^wULpr5Px}`^YrBC{0Kn7(37+B^p5p~x;uT)w4c_7%-s1y4;uAjO3%=qT zzT*de;un775B}mGgdnLjuHZ_p;%ctpTCU@IZs104;%08)R&L{V?%+-?aFM&Xn|rvI z`?#M6c#wy9m`8Y&$9SA4c#@}hnrC>H=XjnMc#)U5#AROQ6<*~vUgr(oW~iWh>q%*j_ZU@>Xc6FjLzzu&Z~ntnv*%3i@BPcxtoW1nwNQ-kNKLP`CEVmT95@> uh=p31g*FAdTtP0}na(kgAzE*;V-UD7Q*(kp$^F9R|tLozHQGAd&-E)y~-Q!*_xGAna3 zFAnOcPU@^K>Z)$)t{&>CUh1tr>Z^Y0uK^mUK^m+f8meI$t`QokQ5vl=8mn;{uL+u{ zNt&!FnyP7dmnyY!5uM4mci?A3=uoTO%94oLAtFRhtuommE9viR`o3I&M zuoc^|9XqfSyRaL3uowHV9|v#{hj182a1_UI94BxRr*Il)a2Drq9v5&Cmv9+Za23~Z z9XD_jw{RPGa2NM*9}n;lkMI~z@D$JR953(^ukadg@D}gz9v|=#pYR!9@D<@E89eEMOsvSj-ZZvW(@dU?r9 v3$aiOvv7;BNQ<&)i?LXXvv^CeL`$+{OR-c-vvkX_Ov|!t%duR`vwZsxRJWSa literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/zarr.json b/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/zarr.json new file mode 100644 index 000000000..6ee039e8a --- /dev/null +++ b/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/0/zarr.json @@ -0,0 +1 @@ +{"shape": [2, 3, 4, 6, 8], "data_type": "uint16", "chunk_grid": {"name": "regular", "configuration": {"chunk_shape": [2, 3, 1, 6, 8]}}, "chunk_key_encoding": {"name": "default", "configuration": {"separator": "/"}}, "fill_value": 0, "codecs": [{"name": "bytes", "configuration": {"endian": "little"}}, {"name": "bogus_codec_xyz", "configuration": {}}], "attributes": {}, "zarr_format": 3, "node_type": "array", "storage_transformers": []} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/zarr.json b/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/zarr.json new file mode 100644 index 000000000..ea6be0fcd --- /dev/null +++ b/tests/data/omezarr/dim5_v3_badcodec.ome.zarr/zarr.json @@ -0,0 +1,52 @@ +{ + "attributes": { + "ome": { + "version": "0.5", + "multiscales": [ + { + "axes": [ + { + "name": "t", + "type": "time" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/c/0/0/0/0/0 b/tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/c/0/0/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..92af2fadbe45a272540cdea54682bc5a13377fe5 GIT binary patch literal 257 zcmZQ#RAh2sVqgGaMn(n(5e5c^(?I-xBO?|V^&0Dwc z+`V`I!NW(7pFDl`{Kd;xuiw0V_x?j;Q*%peTYE=mS9ecuU;l)OlO|7@I&J!lnX_ik znLBU(f`y9~FIl>5`HGdRRp5e5c^(?I-xqoI+piK&^ng{76Xjjf%%gQJtPi>sTvho_gf zkFTG9KwwaCNN8AiL}XNSOl(|y!pBdazkL1n{m0K=zyJLG_n(20iJ66!jh%y&i<^g+ zk6%DgNLWNvOk6@zN?Jx%PF`W-rp;TnZri?N=dRs*_U_w%;NYRdM~)sle&Xb*(`U|} zJAdKgrOQ{YUb}wd=B?Xz?%uoqps=X8q_nKOqOz*GrnauWp|PpCrM0cSqqD2Kr?;p5e5c^(?I-xV`5TrN@`kqMrKxaPHtX)L19sGNoiSmMP*fW zO>JF$Lt|5OOKV$uM`u@ePj6rU1Vtrf6;(BL4NWa=9bG+r14AQY6H_yD3rj0&8(TYj z2S+Do7gslT4^J;|A74NJfQOGBKY9A>`HPpYUcY(!?)`_4pFV&2`tAFVpTBbje8tLDtJkbuw|>LMO`ErD-L`$l&Rx6r?A^Ej zz`;X@j~qRA{KUyqr_Y={cmBcw29E%+Me;!J$TKi9$jCD?GFZzqF*4}NGlRG+j11xd HEFb^?ZoX+_ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/c/0/0/3/0/0 b/tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/c/0/0/3/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..3a8a1604b6d47aa392be57331bca279cfec5575b GIT binary patch literal 262 zcmZQ#RAh2sVqgGaHbw>p5e5c^(?I-xqnoxQ`wOP8-)y>|V^&0Dwc+`V`I!NW(7pFDl`{Kd;xuiw0V z_x{7jPoKYh{r3IG&tJd){QdWz;Q)h20N5gVpm*dM7#U>b89@XSBZId*Gb4knJPRX( JcmN9s000CDZc_jN literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/zarr.json b/tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/zarr.json new file mode 100644 index 000000000..6da3ab887 --- /dev/null +++ b/tests/data/omezarr/dim5_v3_blosc.ome.zarr/0/zarr.json @@ -0,0 +1,51 @@ +{ + "shape": [ + 2, + 3, + 4, + 6, + 8 + ], + "data_type": "uint16", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 2, + 3, + 1, + 6, + 8 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 2, + "cname": "lz4", + "clevel": 5, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_v3_blosc.ome.zarr/zarr.json b/tests/data/omezarr/dim5_v3_blosc.ome.zarr/zarr.json new file mode 100644 index 000000000..ea6be0fcd --- /dev/null +++ b/tests/data/omezarr/dim5_v3_blosc.ome.zarr/zarr.json @@ -0,0 +1,52 @@ +{ + "attributes": { + "ome": { + "version": "0.5", + "multiscales": [ + { + "axes": [ + { + "name": "t", + "type": "time" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/0/0/0/0/0 b/tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/0/0/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..58946b2a6652cc00e4ff345057c95b248dc3aa0a GIT binary patch literal 644 zcmZ9^S6|3c7{Kvwi0qy0Y$2kMksa9~J0vTctjz2^GP6fU*<@tvWq27bUAn0M%{e#E z`JUhTJTdC1tDc4$X{?EsT4}9~!G=hcW`vPO8Ew1?CYq$Z4m#?jo9=q(DM>&54KUR- z)6Fo)T=V2uY>B0E#j3A?I88OvTnlZ*YbU`_!wi=$!x&?YlWDRkvUJu(SBZM*t&hHv z4KzrKnP!e5|{k?U?Kb<;ifJ@C*| z&y=fh&>_VRJK>~LPP^cuOG>=<##`@v@>!KHe){FNYSBrkz2w!-$SHwK0!RO%SrqiF iz!ib311HARJVg8c20c6Ie}bMK^tiwUf&XP>ru_$a)KSy` literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/0/1/0/0/0 b/tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/0/1/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..75c843ace6a1a783b9b9eb4c9db649605419849e GIT binary patch literal 644 zcmZ9`XIBqk7=ZEXB4m$H2-%wulFE$i>=k81_9$e~0SeJ2_{MjB&_E-9`3E+$xh;&f zhDSW+2`_oYYu@mYPkg49wzjjq9qeLP<4v%qz3gq0ee7#L`#Z#;4s*Dp9phM2T;yVx zxYSjyc8$5NcY_<<@)6$2r~!rn=1Ku5hJmU1y&87P#3h7MfwElbvFgGo59&^IhOVbKL1Jce}^^9`K+g z9<$Wro{-2(h|K)J$$?J>j_yTgNzk(cmjyl*I4Sa@=)KyY=LEef=&3=E4O|$wAtNj0 EKi0cpRR910 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/0/2/0/0/0 b/tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/0/2/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..92fe6cbb8d0cb254f76f54d08e677de7ce9a2d08 GIT binary patch literal 644 zcmZ9^S6dHY7=ZEa8;QsY*%7jZ5D6!)%b}0p&_{48duL^@tTHN_tjH#NWJ~rcBSOZ3 z{wMEy@?5{?zMgjqnM`FGvzfzO=CO#yEMY0T*~4D;ahM{GaFmmrqJ-0|WEHDf!$xx0 z#Afo?&JK2Rfs0(?GS|7mO>S|I`#j(w)0x3cW-*@yEF_C%WV4(V?B@UnIm9uJbAn>d zaF%nNXD#bk&jz-zm2Kp*i+ls@T_I9wNUF~L)-TlE&eo+f*J?q=Z#>U#j-uAJt{T$?AhnVUJN1E;^U+|Jw zyygQRsicZ(zVV$JTHD69CfLc&b}`W&_B7dE{^k#ViL!ywHZ;bj#@Wo~_IH2-O>wBh z9B!H!j&_V=jjDSIb#r3il)zbmBX^NGIq2g9=LRkgoXG!0x7Y75y#ugK?FtNqL#uh}eI}tmv#XuAjF|fM}!N7Kq z|8nngp7;EobL*(9p86VTtcj*tYNfR{h8Sv?;YJ%{tZ^opY>KJkb<{~`-6iUwr@s2> zZ-AL*nQe~wGA*#sQp+s2LW~9)iq%YWEyQW7o%T8yVWd$~jF)PHiKa<2-3(oH)lGt4 zdg~*}K!YS3Y_56IWyrF~VoR*F%4%z@wa$7QY>{KDZFbvZuYHPKQ|!9i?zroo$DVlV znL`dc;;55OIqi(|3SDr~Yj3>u&L?HcefHfCKmD@NCYxp3Zik(A+3$c{2iEGI7;TKP(oHnUWK(p}RX5%B(pw*W4KUCkgH1O> zwj8s~G1okcEwR)xjWy9!v{qVcqpglQ=`7YrqohicVVvp82Q@nl>B&U+u!`0AVQepqXrLPa*&Y>TaS*=>&!*IakQP50dQz(bXu zdZx-jha6Vwgp*D=?Ys*vD)-83Z@l%t}x8q`>8YBX^Nm67;OV hRe@^*$4AvIBLDpjdS1}~1U)V2(SeHs|I5fs`44gsR1^RJ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/1/2/0/0/0 b/tests/data/omezarr/dim5_v3_sharded.ome.zarr/0/c/1/2/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..5b66db100a4a01dcbaf5b211f66b09a4033e4756 GIT binary patch literal 644 zcmZ9^*;)=z6o>Khf77f~>Qf<9WUPduL`2f?$&ivtqEa%2jCBbv!(}8P6p=J(6h#`4 zN+<{VPWC=o&u_iY+7DH$N2=2^J=Y60=(XOcS#R}C-}OTi`lT8D)*sok00HglP^aFj zPyPC!0e#jN4eP6%alsWg7UP9CJ}kwb00MceCwi)SHR`2asYxw*r&hJ;rzSO}Y5mo# z=46Kh3vtAWE_JI%z51w68q|qv@x#J0ma~E&Rl?rZhi`(2G zgizM7mW^y;GvRDy8{3KI7{|%v1gFR)k9^K@jv|VQV;Ax4CXxLdAc<7cNXN})ds}Nub5mnOeO+x$bya0Wd0A;maZzDG deqL@)c2;J_{rB|BSD#H6oCn!1c4*2e>vctzl^O%}Sgc9ts3kLp1Getu4(>jScm6wKdgM nl@;Y>r6t8hg$4O}xjETcnHl%r(<@(nHfjAetx~OCCq@PUKNA>7 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/0/2/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/0/2/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..409e68fd2800cc4ac35b7f70d641c8150c0896b3 GIT binary patch literal 88 zcmdPcs{dCZp_Yk(>6nmoz}2(-s~!2-v*b-qzaE+|<}mUsqdG pT~%38URGLCTvS+)pO>4Hot2q!|2@6()n}8|U(+hp>UCmd0093+8m0gM literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/0/3/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/0/3/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..01d745365e442f4181a2bd2fc7c11e4744e22fe0 GIT binary patch literal 93 zcmdPcs{dCZVJQ;>6Pt)a!`ZV82#_#cMA6k_;`~O*86Y&(i_zZJ+S1(A*ic_rTT@+C nSy5hAT2fq8SdgEWo0FZDnQ{L;z4Fy(lh$9;D%I+BVq^dSIZPLC literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/1/0/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/1/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..e84ce5a9bf4ec44852c77f4494941f0e56f6ed9e GIT binary patch literal 93 zcmdPcs{dCZVJQ;>6Pt*_g57BBOxCh8yOcB6A=#$3ke4W0|CF@E_bWVVz1UJbt;WQ0RZzk7Zd;h literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/1/3/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/1/3/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..ebadf319930c456fbc07658b089748bcfbf0151c GIT binary patch literal 87 zcmdPcs{dCZp_GY%sZB^K;Obd^Rof|F0xmgET$VX&S*DmV2y9lDx3{&nG&eOi)YsM4 pR9972l$Vv36c-g16Pt*_g57BuPJNt12tX k%Sua%iwX2m?z{HYiTAq_sKc6xGrba=48 vx4X0be(Ss|GXx*Ic8hBIO_30MEa9k0>CFT literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/2/3/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/0/2/3/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..690b4064a09983b3aba7780349e9185de5d940ed GIT binary patch literal 80 zcmV-W0I&ZjwJ-f3U_Al=0>%&%kgPWY@Lcd*6sS^FRkaEL5GVmZJv%u!H8U|UEh{M} mB_kmp9UB=J6%!E;4GRee1p@)U-Y$2m&0??CDs?K2LID7eHx;Y^ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/0/0/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/0/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..555a2d3be5d9baaa428d2e78c2f3a4068b1646cc GIT binary patch literal 85 zcmdPcs{dCZA(e@NiA_kt;PTo35U~I5A+@a^_<2Aeq|@Hk+S1(A*ic_rTT@+CSy5hA jT2fq8SdgEWo0FZDnQ{L;z4Fy(lh$9;D%I+BVq^dSU=kd6 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/0/1/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/0/1/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..9fea9de9f30fe8ba361331f4cc59eea2a3677e9f GIT binary patch literal 89 zcmdPcs{dCZp_Pe&iA`9h;QU#ItMT=Jzg`Cts;(Y_K`np&gMn40y{)ySxv8CI nML|YFpDu2kSQuz1`0>Jq2?YTL0v}$#E!uud=Udj=pydnz1tBhd literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/0/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..6b0bd007965a58e5e283c7e55d4725b3ad3a0244 GIT binary patch literal 73 zcmdPcs{dCZft87YiH)CEVTCy}gKqFpG{hSO{-L^m#>zI0RU1x5kvq0 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/1/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/1/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..26b7c787393185c621a495eff84334809e94be02 GIT binary patch literal 84 zcmdPcs{dCZA(n}O>6oCn!1c4*2e>vctzl^O%}Sgc9tr~H#`d<>mgc6$hWfhNn(C^` mit@73lH#Jmg8aPPob0U3jQj8Dm9IXVwEmh_saCHOBLe^-(-*V= literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/2/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/1/2/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..a96a6b55c8d36ce4115d8f8c8029768967258ddb GIT binary patch literal 89 zcmdPcs{dCZp_Pe&iA`80;Obd^Rof|F0xmgET$VX&S*DmV1ejPb+S^)NnwuIM>g#H2 ps;eq1%F9Yiii-*h^7C?Yva>QX?!Tv3zWQv^`fFOHTD?w;3;?Iw83O6Pt*_g57BL<^#x3#u3H#IiY*VWckS5;P& kmz9<0MZ2_QUCw| literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/2/1/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/2/1/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..43fbbc4e2a219ffcdcf9ee3ee0667a9d1c66d3ac GIT binary patch literal 97 zcmdPcs{dCZ;V2UW6Pu{QgtJC$AGUwk{^vgosO$cLLdA(G?=8(ujScm6wKdgMl@;Y> rr6t8hg$4O}xjETcnd1+v-g-5}n|JGFAMM^P7hSk{x16_m&B6cxTgNBF literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/2/2/0/0 b/tests/data/omezarr/dim5_v3_zstd.ome.zarr/0/c/1/2/2/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..c529517d50d252f8711259d281d404747731a327 GIT binary patch literal 82 zcmV-Y0ImNhwJ-f3U`+x50!9%P;Mz3 legacy fallback]")) +def write_calibrated(name, order, scale, unit): + """A physically calibrated store: coordinateTransformations 'scale' != 1 and the + space axes carry a unit, so the loader must surface physicalSize*/unit. Drives the + --use-physical-spacing calibration path. Same coordinate encoding as write_store.""" + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + data = _data_for(order) + chunks = tuple(1 if a == "z" else s for a, s in zip(order, data.shape)) + + g = zarr.open_group(path, mode="w", zarr_format=2) + a = g.create_array("0", shape=data.shape, dtype=data.dtype, chunks=chunks, compressors=None) + a[:] = data + + axes = [] + for ax in order: + e = dict(_AX[ax]) + if e["type"] == "space": + e["unit"] = unit + axes.append(e) + ms = {"version": "0.4", "axes": axes, + "datasets": [{"path": "0", "coordinateTransformations": [{"type": "scale", "scale": scale}]}]} + g.attrs.put({"multiscales": [ms]}) + print("wrote %-22s (calibrated scale=%s unit=%s)" % (name, scale, unit)) + + +def write_multichunk(name, order, chunk_y, chunk_x): + """Same coordinate encoding as write_store, but each Y/X plane is split across a GRID of + chunks instead of being one chunk. This is what real OME-Zarr looks like (chunks are + typically 512x512), and it exercises the multi-tile volumetric assembly: a reader that + fetches only chunk (0,0) returns wrong data for everything past the first chunk.""" + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + data = _data_for(order) + chunks = tuple((chunk_y if a == "y" else chunk_x if a == "x" else 1) for a in order) + + g = zarr.open_group(path, mode="w", zarr_format=2) + a = g.create_array("0", shape=data.shape, dtype=data.dtype, chunks=chunks, compressors=None) + a[:] = data + + ms = {"version": "0.4", "axes": [_AX[ax] for ax in order], + "datasets": [{"path": "0", + "coordinateTransformations": [{"type": "scale", "scale": [1.0] * len(order)}]}]} + g.attrs.put({"multiscales": [ms]}) + print("wrote %-24s (chunk grid %dx%d over the %dx%d plane)" + % (name, -(-Y // chunk_y), -(-X // chunk_x), Y, X)) + + +def write_v3(name, order, compressors=None): + """OME-Zarr 0.5 = Zarr **v3** store: metadata in zarr.json (not .zarray/.zattrs), the + NGFF block nested under an 'ome' key, chunks under 0/c//... The nyxus loader reads + v3 via the z5 Dataset API (z5 3.x auto-detects v2 vs v3). Uncompressed by default so it + reads with the current blosc+zlib z5 build (zstd-compressed v3 would need -DWITH_ZSTD).""" + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + data = _data_for(order) + chunks = tuple(1 if a == "z" else s for a, s in zip(order, data.shape)) + + g = zarr.open_group(path, mode="w", zarr_format=3) + a = g.create_array("0", shape=data.shape, dtype=data.dtype, chunks=chunks, compressors=compressors) + a[:] = data + g.attrs["ome"] = {"version": "0.5", + "multiscales": [{"axes": [_AX[ax] for ax in order], + "datasets": [{"path": "0", + "coordinateTransformations": [{"type": "scale", "scale": [1.0] * len(order)}]}]}]} + print("wrote %-24s (Zarr v3, axes=%s, compressors=%s)" % (name, order, compressors)) + + +def write_v3_sharded(name, order): + """Zarr v3 store using the ``sharding_indexed`` codec: several INNER chunks are packed + into one shard object on disk (one file per shard, not per chunk). This is how large v3 + stores (incl. Axle's) actually lay out data. z5 3.x reads it transparently via + ShardedDataset -- the nyxus loader is unchanged, it just calls readSubarray. + + Same TCZYX coordinate encoding as write_v3. Inner chunk (z,y,x)=(1,3,4) -> each 6x8 plane + is a 2x2 grid of inner chunks; shard (1,6,8) packs all 4 inner chunks of a plane into one + shard, so the read must unpack multiple inner chunks from a single shard file.""" + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + data = _data_for(order) # [t,c,z,y,x], order tczyx + inner = tuple((1 if a == "z" else 3 if a == "y" else 4 if a == "x" else 1) for a in order) + shards = tuple((s if a in ("y", "x", "z") else 1) for a, s in zip(order, data.shape)) + + g = zarr.open_group(path, mode="w", zarr_format=3) + a = g.create_array("0", shape=data.shape, dtype=data.dtype, + chunks=inner, shards=shards, compressors=None) + a[:] = data + g.attrs["ome"] = {"version": "0.5", + "multiscales": [{"axes": [_AX[ax] for ax in order], + "datasets": [{"path": "0", + "coordinateTransformations": [{"type": "scale", "scale": [1.0] * len(order)}]}]}]} + print("wrote %-24s (Zarr v3 SHARDED, inner=%s shard=%s)" % (name, inner, shards)) + + +def write_label_mask(name): + """P4/N3: a single-channel ZYX label mask (mostly background, a few small ROIs), the + OME-Zarr twin of dim3_mask.ome.tif. Pairs with a T>1 Zarr intensity to exercise the + 1-mask : N-timeframe reuse on the Zarr path (the crash's positive twin).""" + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + m = np.zeros((Z, Y, X), "uint32") + m[:, 1:5, 1:7] = 1 # one ROI (label 1), interior bbox + g = zarr.open_group(path, mode="w", zarr_format=2) + a = g.create_array("0", shape=m.shape, dtype=m.dtype, chunks=(1, Y, X), compressors=None) + a[:] = m + g.attrs.put({"multiscales": [{"version": "0.4", "axes": [_AX[c] for c in "zyx"], + "datasets": [{"path": "0", "coordinateTransformations": [ + {"type": "scale", "scale": [1.0, 1.0, 1.0]}]}]}]}) + print("wrote %-24s (ZYX label mask, ROI voxels=%d)" % (name, int(m.sum()))) + + +def write_v3_badcodec(name): + """N1: a Zarr v3 store whose zarr.json declares a codec z5 does not support. z5's + readV3CodecsFromJson throws 'unsupported zarr v3 codec' at openDataset (metadata parse, + before any data read), so the loader must surface a clean error, not crash. Written + uncompressed, then the codec list is patched to an unknown name.""" + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + data = _data_for("tczyx") + chunks = tuple(1 if a == "z" else s for a, s in zip("tczyx", data.shape)) + g = zarr.open_group(path, mode="w", zarr_format=3) + a = g.create_array("0", shape=data.shape, dtype=data.dtype, chunks=chunks, compressors=None) + a[:] = data + g.attrs["ome"] = {"version": "0.5", "multiscales": [{"axes": [_AX[c] for c in "tczyx"], + "datasets": [{"path": "0", "coordinateTransformations": [ + {"type": "scale", "scale": [1.0] * 5}]}]}]} + # patch the array's zarr.json to reference an unsupported codec + zj_path = os.path.join(path, "0", "zarr.json") + zj = json.load(open(zj_path)) + zj["codecs"] = [{"name": "bytes", "configuration": {"endian": "little"}}, + {"name": "bogus_codec_xyz", "configuration": {}}] + json.dump(zj, open(zj_path, "w")) + print("wrote %-24s (v3 with unsupported codec 'bogus_codec_xyz')" % name) + + def write_bad(name, shape, axes): """Write a deliberately malformed store (its 'axes' disagrees with the array) so the loader must reject it cleanly, not crash.""" @@ -103,6 +238,21 @@ def main(): write_store("dim3_zyx.ome.zarr", "zyx") write_store("dim2_yx.ome.zarr", "yx") write_store("dim5_noaxes.ome.zarr", "tczyx", include_axes=False) + # physically calibrated: anisotropic voxels (z 4x thicker than x/y after normalization). + # order tczyx -> scale [t,c,z,y,x] = [1,1,2.0,0.5,0.5] -> physX=0.5 physY=0.5 physZ=2.0 um. + write_calibrated("dim5_calibrated.ome.zarr", "tczyx", [1.0, 1.0, 2.0, 0.5, 0.5], "micrometer") + # OME-Zarr 0.5 (Zarr v3): same 5D TCZYX encoding, but zarr.json metadata + 'ome'-wrapped NGFF + write_v3("dim5_v3.ome.zarr", "tczyx") + # Zarr v3 with the BLOSC codec (common alongside zstd in real v3 stores). z5 decodes the + # bytes+blosc v3 pipeline when built WITH_BLOSC (already required for OME-Zarr). + write_v3("dim5_v3_blosc.ome.zarr", "tczyx", + compressors=[BloscCodec(cname="lz4", clevel=5, shuffle="shuffle")]) + # plane split across a 2x2 chunk grid (3x4 chunks over the 6x8 plane) -> multi-tile assembly + write_multichunk("dim5_multichunk.ome.zarr", "tczyx", 3, 4) + # PARTIAL edge chunks: chunk (4,5) does NOT divide the 6x8 plane, so the last row-chunk is + # 2 tall and the last col-chunk is 3 wide. Exercises the validH/validW seam clamp in the + # volumetric assembly, which every exact-multiple fixture (above) leaves untested. + write_multichunk("dim5_oddchunk.ome.zarr", "tczyx", 4, 5) # --- illegal / adversarial: must be rejected cleanly, not crash --- # 3D array but 'axes' declares 5 entries -> indexing the shape by axis role OOBs write_bad("bad_axes_count.ome.zarr", (Z, Y, X), [_AX[k] for k in ("t", "c", "z", "y", "x")]) diff --git a/tests/python/test_nyxus.py b/tests/python/test_nyxus.py index 2b03a95e9..deb5fc878 100644 --- a/tests/python/test_nyxus.py +++ b/tests/python/test_nyxus.py @@ -426,10 +426,15 @@ def test_in_memory_3d(self): means = cpu_features.mean(numeric_only=True) - mean_values = means.tolist() + # Drop the non-feature index/calibration columns by NAME so this stays correct as + # the output schema grows. (This used to pop the 2 leftmost columns positionally, + # which silently averaged c_index/phys_x/y/z into the first feature once the + # channel-index and physical-spacing columns were added.) + for _c in ("ROI_label", "time", "t_index", "c_index", "phys_x", "phys_y", "phys_z"): + if _c in means.index: + means = means.drop(_c) - mean_values.pop(0) # get rid of leftmost ROI label column - mean_values.pop(0) # get rid of leftmost time column + mean_values = means.tolist() averaged_results = [] diff --git a/tests/test_3d_coverage_common.h b/tests/test_3d_coverage_common.h index bd54f0ddb..de57f6f4d 100644 --- a/tests/test_3d_coverage_common.h +++ b/tests/test_3d_coverage_common.h @@ -143,14 +143,14 @@ static Computed3DFeatureValues build_computed_3d_feature_values() e.dataset.update_dataset_props_extrema(); clear_slide_rois(e.uniqueLabels, e.roiData); - if (!gatherRoisMetrics_3D(e, 0, ipath, mpath, 0)) + if (!gatherRoisMetrics_3D(e, 0, ipath, mpath, 0/*t_index*/, 0/*channel*/)) { out.setup_error = "gatherRoisMetrics_3D failed for 3D coverage phantom"; return false; } std::vector batch = { label }; - if (!scanTrivialRois_3D(e, batch, ipath, mpath, 0)) + if (!scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)) { out.setup_error = "scanTrivialRois_3D failed for 3D coverage phantom"; return false; diff --git a/tests/test_3d_firstorder_pyradiomics.h b/tests/test_3d_firstorder_pyradiomics.h index ea165d90f..51252b869 100644 --- a/tests/test_3d_firstorder_pyradiomics.h +++ b/tests/test_3d_firstorder_pyradiomics.h @@ -95,11 +95,11 @@ void test_compat_radiomics_3fo_feature (const Nyxus::Feature3D &expected_fcode, // properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE (gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE (gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE (scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE (scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // buffers ASSERT_NO_THROW (allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_glcm.h b/tests/test_3d_glcm.h index 80677669d..4bfff26c0 100644 --- a/tests/test_3d_glcm.h +++ b/tests/test_3d_glcm.h @@ -85,11 +85,11 @@ void test_3glcm_feature (const Nyxus::Feature3D& expecting_fcode, const std::str // properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_glcm_pyradiomics.h b/tests/test_3d_glcm_pyradiomics.h index 8c4b3e45c..be176b14a 100644 --- a/tests/test_3d_glcm_pyradiomics.h +++ b/tests/test_3d_glcm_pyradiomics.h @@ -146,11 +146,11 @@ void test_compat_3glcm_feature (const Nyxus::Feature3D& expecting_fcode, const s // properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); @@ -214,9 +214,9 @@ void test_3dglcm_equivalence_dump() ASSERT_TRUE(scan_slide_props(sp, 3, e.anisoOptions, e.resultOptions.need_annotation())); e.dataset.update_dataset_props_extrema(); clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0, ipath, mpath, 0)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0, ipath, mpath, 0/*t_index*/, 0/*channel*/)); std::vector batch = { label }; - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); Fsettings s; diff --git a/tests/test_3d_gldm.h b/tests/test_3d_gldm.h index 4972fbb23..5f445bde3 100644 --- a/tests/test_3d_gldm.h +++ b/tests/test_3d_gldm.h @@ -42,9 +42,9 @@ void test_3gldm_feature (const Nyxus::Feature3D& expecting_fcode, const std::str // mock the 3D workflow Environment e; clear_slide_rois (e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); // make it find the feature code by name @@ -84,10 +84,10 @@ void test_3gldm_feature (const Nyxus::Feature3D& expecting_fcode, const std::str e.dataset.update_dataset_props_extrema(); // (2) properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (3) voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (4) buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_gldm_pyradiomics.h b/tests/test_3d_gldm_pyradiomics.h index 87bfb4358..2e58313f0 100644 --- a/tests/test_3d_gldm_pyradiomics.h +++ b/tests/test_3d_gldm_pyradiomics.h @@ -75,11 +75,11 @@ void test_compat_3gldm_feature (const Nyxus::Feature3D & expecting_fcode, const // properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois (e.uniqueLabels, e.roiData); - ASSERT_TRUE (gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE (gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE (scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE (scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // buffers ASSERT_NO_THROW (allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_gldzm_ibsi.h b/tests/test_3d_gldzm_ibsi.h index 70ac2fa86..dfd2937a7 100644 --- a/tests/test_3d_gldzm_ibsi.h +++ b/tests/test_3d_gldzm_ibsi.h @@ -41,9 +41,9 @@ void test_3gldzm_feature (const Nyxus::Feature3D& expecting_fcode, const std::st // mock the 3D workflow Environment e; clear_slide_rois (e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); // make it find the feature code by name @@ -84,10 +84,10 @@ void test_3gldzm_feature (const Nyxus::Feature3D& expecting_fcode, const std::st e.dataset.update_dataset_props_extrema(); // (2) properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (3) voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (4) buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_glrlm.h b/tests/test_3d_glrlm.h index fba745c89..61e8fedae 100644 --- a/tests/test_3d_glrlm.h +++ b/tests/test_3d_glrlm.h @@ -43,9 +43,9 @@ void test_3glrlm_feature (const Nyxus::Feature3D& expecting_fcode, const std::st // mock the 3D workflow Environment e; clear_slide_rois (e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); // make it find the feature code by name @@ -85,10 +85,10 @@ void test_3glrlm_feature (const Nyxus::Feature3D& expecting_fcode, const std::st e.dataset.update_dataset_props_extrema(); // (2) properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (3) voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (4) buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); @@ -210,4 +210,4 @@ void test_3glrlm_rv() void test_3glrlm_re() { test_3glrlm_feature(Nyxus::Feature3D::GLRLM_RE, "3GLRLM_RE"); -} \ No newline at end of file +} \ No newline at end of file diff --git a/tests/test_3d_glrlm_pyradiomics.h b/tests/test_3d_glrlm_pyradiomics.h index 922c69a4a..ff28b09aa 100644 --- a/tests/test_3d_glrlm_pyradiomics.h +++ b/tests/test_3d_glrlm_pyradiomics.h @@ -160,11 +160,11 @@ void test_compat_3glrlm_feature(const Nyxus::Feature3D& expecting_fcode, const s // properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); @@ -229,9 +229,9 @@ void test_compat_3glrlm_ave_features() ASSERT_TRUE(scan_slide_props(sp, 3, e.anisoOptions, e.resultOptions.need_annotation())); e.dataset.update_dataset_props_extrema(); clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0, ipath, mpath, 0)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0, ipath, mpath, 0/*t_index*/, 0/*channel*/)); std::vector batch = { label }; - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); Fsettings s; diff --git a/tests/test_3d_glszm.h b/tests/test_3d_glszm.h index d418e2865..b6c647837 100644 --- a/tests/test_3d_glszm.h +++ b/tests/test_3d_glszm.h @@ -41,9 +41,9 @@ void test_3glszm_feature (const Nyxus::Feature3D& expecting_fcode, const std::st // mock the 3D workflow Environment e; clear_slide_rois (e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); // make it find the feature code by name @@ -83,10 +83,10 @@ void test_3glszm_feature (const Nyxus::Feature3D& expecting_fcode, const std::st e.dataset.update_dataset_props_extrema(); // (2) properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (3) voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (4) buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_glszm_pyradiomics.h b/tests/test_3d_glszm_pyradiomics.h index 1c67b402f..0a18986a2 100644 --- a/tests/test_3d_glszm_pyradiomics.h +++ b/tests/test_3d_glszm_pyradiomics.h @@ -76,11 +76,11 @@ void test_compat_3glszm_feature (const Nyxus::Feature3D& expecting_fcode, const // properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_inten.h b/tests/test_3d_inten.h index 2ff94c237..913bc6ecb 100644 --- a/tests/test_3d_inten.h +++ b/tests/test_3d_inten.h @@ -67,10 +67,10 @@ void test_3inten_feature (const std::string& fname, const Nyxus::Feature3D& expe e.dataset.update_dataset_props_extrema(); // (2) properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois (e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (3) voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (4) buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_morphology_common.h b/tests/test_3d_morphology_common.h index 4f3265d46..901acf6db 100644 --- a/tests/test_3d_morphology_common.h +++ b/tests/test_3d_morphology_common.h @@ -33,9 +33,9 @@ void test_3shape_feature (const std::string& fname, const Nyxus::Feature3D& expe // mock the 3D workflow Environment e; clear_slide_rois (e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); // make it find the feature code by name @@ -78,10 +78,10 @@ void test_3shape_feature (const std::string& fname, const Nyxus::Feature3D& expe e.dataset.update_dataset_props_extrema(); // (2) properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (3) voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (4) buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_ngldm_regression.h b/tests/test_3d_ngldm_regression.h index 217e1d3ac..4a0c47a9a 100644 --- a/tests/test_3d_ngldm_regression.h +++ b/tests/test_3d_ngldm_regression.h @@ -79,9 +79,9 @@ void test_3ngldm_feature (const std::string& fname, const Nyxus::Feature3D& expe // mock the 3D workflow Environment e; clear_slide_rois (e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); // make it find the feature code by name @@ -122,10 +122,10 @@ void test_3ngldm_feature (const std::string& fname, const Nyxus::Feature3D& expe e.dataset.update_dataset_props_extrema(); // (2) properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (3) voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (4) buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_ngtdm.h b/tests/test_3d_ngtdm.h index e0a809d00..f8aa205e5 100644 --- a/tests/test_3d_ngtdm.h +++ b/tests/test_3d_ngtdm.h @@ -30,9 +30,9 @@ void test_3ngtdm_feature(const Nyxus::Feature3D& expecting_fcode, const std::str // mock the 3D workflow Environment e; clear_slide_rois (e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); // make it find the feature code by name @@ -73,10 +73,10 @@ void test_3ngtdm_feature(const Nyxus::Feature3D& expecting_fcode, const std::str e.dataset.update_dataset_props_extrema(); // (2) properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (3) voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // (4) buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_3d_ngtdm_pyradiomics.h b/tests/test_3d_ngtdm_pyradiomics.h index 9466866c1..df57fb6a4 100644 --- a/tests/test_3d_ngtdm_pyradiomics.h +++ b/tests/test_3d_ngtdm_pyradiomics.h @@ -83,11 +83,11 @@ void test_compat_3ngtdm_feature (const Nyxus::Feature3D& expecting_fcode, const // properties of specific ROIs sitting in 'e.uniqueLabels' clear_slide_rois(e.uniqueLabels, e.roiData); - ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(gatherRoisMetrics_3D(e, 0/*slide_index*/, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // voxel clouds std::vector batch = { label }; // expecting this roi label after metrics gathering - ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/)); + ASSERT_TRUE(scanTrivialRois_3D(e, batch, ipath, mpath, 0/*t_index*/, 0/*channel*/)); // buffers ASSERT_NO_THROW(allocateTrivialRoisBuffers_3D(batch, e.roiData, e.hostCache)); diff --git a/tests/test_all.cc b/tests/test_all.cc index cf0a339bd..047bb871a 100644 --- a/tests/test_all.cc +++ b/tests/test_all.cc @@ -1,8 +1,10 @@ #define NOMINMAX // keep Windows min/max macros from breaking dcmtk's OFvariant (DICOM tests) #include +#include // reading a written CSV back in TEST_CSV_MULTICHANNEL_NO_OVERWRITE #include "test_gabor_regression.h" #include "../src/nyx/environment.h" #include "../src/nyx/globals.h" +#include "../src/nyx/ome/format_detect.h" // detect_input_format (P1) #include "test_contour.h" #include "test_ome_meta.h" // native-OME metadata parsers / OmeAxes descriptor #include "test_ometiff_mechanics.h" // OME-TIFF native (z,c,t)->IFD read (core; no USE_Z5) @@ -2574,6 +2576,11 @@ TEST(TEST_NYXUS, TEST_OMEZARR_WHOLEVOLUME_CONSUMER) { } // Facade whole-volume assembly (load_volume loops Z into one X*Y*Z buffer). +// Wired consumer reads the correct plane for every (channel, timeframe), not just (0,0). +TEST(TEST_NYXUS, TEST_OMEZARR_WHOLEVOLUME_CONSUMER_CT) { + ASSERT_NO_THROW (test_omezarr_wholevolume_consumer_ct("dim5.ome.zarr", 2, 3, 4)); +} + TEST(TEST_NYXUS, TEST_OMEZARR_FACADE_VOLUME_3D) { ASSERT_NO_THROW (test_omezarr_facade_volume("dim3_zyx.ome.zarr", 1, 1, 4)); } @@ -2595,6 +2602,71 @@ TEST(TEST_NYXUS, TEST_OMEZARR_2D_YX) { ASSERT_NO_THROW (test_omezarr_addressing("dim2_yx.ome.zarr", 1, 1, 1)); } +// OME-Zarr 0.5 (Zarr v3): zarr.json metadata + 'ome'-wrapped NGFF + 0/c/... chunk keys, +// read through the z5 Dataset API (v2/v3-agnostic). Same coordinate encoding as the v2 +// stores, so the addressing / facade / CT-count helpers apply unchanged. +TEST(TEST_NYXUS, TEST_OMEZARR_V3) { + ASSERT_NO_THROW (test_omezarr_addressing("dim5_v3.ome.zarr", 2, 3, 4)); + ASSERT_NO_THROW (test_raw_omezarr_addressing("dim5_v3.ome.zarr", 2, 3, 4)); +} +TEST(TEST_NYXUS, TEST_OMEZARR_V3_FACADE_VOLUME) { + ASSERT_NO_THROW (test_omezarr_facade_volume("dim5_v3.ome.zarr", 2, 3, 4)); +} +TEST(TEST_NYXUS, TEST_OMEZARR_V3_CT_COUNTS) { + ASSERT_NO_THROW (test_omezarr_ct_counts("dim5_v3.ome.zarr", 2, 3, 4)); +} +// Zstd-compressed Zarr v3 (the zarr 3.x / real-world default codec). Requires the build +// to link libzstd (-DWITH_ZSTD); z5 decodes the bytes+zstd v3 codec pipeline. +TEST(TEST_NYXUS, TEST_OMEZARR_V3_ZSTD) { + ASSERT_NO_THROW (test_omezarr_addressing("dim5_v3_zstd.ome.zarr", 2, 3, 4)); + ASSERT_NO_THROW (test_raw_omezarr_addressing("dim5_v3_zstd.ome.zarr", 2, 3, 4)); +} + +// Sharded Zarr v3 (the ``sharding_indexed`` codec) -- how large v3 stores, including Axle's, +// actually lay out data: many inner chunks packed into one shard object per (t,c). z5 3.x +// reads it via ShardedDataset, chosen automatically when zarr.json carries a shard shape; the +// nyxus loader is UNCHANGED because it reads through readSubarray, which unpacks the inner +// chunks from the shard transparently. The fixture's inner chunk is (z,y,x)=(1,3,4) so each +// 6x8 plane is a 2x2 grid of inner chunks living inside one shard -- the read must assemble +// across inner-chunk boundaries within a shard. Same 1..1152 TCZYX encoding as the other v3. +// +// Coverage is the whole-volume facade + prescan, NOT test_omezarr_addressing: with sharding, +// tileWidth/Height report the INNER chunk (4x3), so a single loadTileFromFile(0,0,...) reads +// only the top-left inner chunk, not the whole plane -- the addressing helper's one-tile-per- +// plane assumption. facade_volume assembles the full inner-chunk grid and checks every voxel, +// which is the correct coverage for a multi-chunk store (same reason the multichunk v2 fixture +// uses it). It drives both loadTileFromFile (abstract stack) and readSubarray under the hood. +TEST(TEST_NYXUS, TEST_OMEZARR_V3_SHARDED_FACADE_VOLUME) { + ASSERT_NO_THROW (test_omezarr_facade_volume("dim5_v3_sharded.ome.zarr", 2, 3, 4)); +} +TEST(TEST_NYXUS, TEST_OMEZARR_V3_SHARDED_CT_COUNTS) { + ASSERT_NO_THROW (test_omezarr_ct_counts("dim5_v3_sharded.ome.zarr", 2, 3, 4)); +} +// Prescan over the sharded store (raw loader's readSubarray, driven through the inner-chunk +// tile grid by for_each_voxel): whole-slide, so the ROI is the whole X*Y*Z volume. +TEST(TEST_NYXUS, TEST_OMEZARR_V3_SHARDED_PRESCAN) { + fs::path ip = omezarr_data_path("dim5_v3_sharded.ome.zarr"); + ASSERT_TRUE(fs::exists(ip)) << ip.string(); + + Environment e; + SlideProps p (ip.string(), ""); // whole-slide: no mask + ASSERT_TRUE(Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + + EXPECT_DOUBLE_EQ(p.min_preroi_inten, 1.0); + EXPECT_DOUBLE_EQ(p.max_preroi_inten, 1152.0); + EXPECT_EQ(p.max_roi_area, (size_t)(8 * 6 * 4)); +} + +// Blosc-compressed Zarr v3 (common in real v3 stores alongside zstd). z5 decodes the +// bytes+blosc v3 codec pipeline when built WITH_BLOSC (already required for OME-Zarr). +TEST(TEST_NYXUS, TEST_OMEZARR_V3_BLOSC) { + ASSERT_NO_THROW (test_omezarr_addressing("dim5_v3_blosc.ome.zarr", 2, 3, 4)); + ASSERT_NO_THROW (test_raw_omezarr_addressing("dim5_v3_blosc.ome.zarr", 2, 3, 4)); +} +TEST(TEST_NYXUS, TEST_OMEZARR_V3_BLOSC_FACADE_VOLUME) { + ASSERT_NO_THROW (test_omezarr_facade_volume("dim5_v3_blosc.ome.zarr", 2, 3, 4)); +} + // No 'axes' metadata -> the loader falls back to legacy 5D TCZYX and still reads. TEST(TEST_NYXUS, TEST_OMEZARR_NOAXES_FALLBACK) { ASSERT_NO_THROW (test_omezarr_addressing("dim5_noaxes.ome.zarr", 2, 3, 4)); @@ -2604,6 +2676,71 @@ TEST(TEST_NYXUS, TEST_RAW_OMEZARR_NOAXES_FALLBACK) { ASSERT_NO_THROW (test_raw_omezarr_addressing("dim5_noaxes.ome.zarr", 2, 3, 4)); } +// Loaders advertise the real C/T extents (numberChannels/fullTimestamps), which +// is what activates the pipeline's channel/timeframe iteration. dim5_noaxes proves +// the positional fallback reports counts too. +TEST(TEST_NYXUS, TEST_OMEZARR_CT_COUNTS) { + ASSERT_NO_THROW (test_omezarr_ct_counts("dim5.ome.zarr", 2, 3, 4)); + ASSERT_NO_THROW (test_omezarr_ct_counts("dim4_tzyx.ome.zarr", 2, 1, 4)); + ASSERT_NO_THROW (test_omezarr_ct_counts("dim4_czyx.ome.zarr", 1, 3, 4)); + ASSERT_NO_THROW (test_omezarr_ct_counts("dim3_zyx.ome.zarr", 1, 1, 4)); + ASSERT_NO_THROW (test_omezarr_ct_counts("dim2_yx.ome.zarr", 1, 1, 1)); + ASSERT_NO_THROW (test_omezarr_ct_counts("dim5_noaxes.ome.zarr", 2, 3, 4)); +} + +// Physical calibration: loaders surface coordinateTransformations scale + unit. +TEST(TEST_NYXUS, TEST_OMEZARR_PHYSICAL_CALIBRATION) { + ASSERT_NO_THROW (test_omezarr_physical_calibration()); +} + +// Multi-CHUNK plane: real OME-Zarr splits each Y/X plane across a chunk grid (typically +// 512x512), and dim5_multichunk uses 3x4 chunks over the 6x8 plane. The volumetric read +// must walk the whole tile grid: fetching only chunk (0,0) returns wrong data past the +// first chunk (and over-reads its buffer). Every other fixture is one-chunk-per-plane, +// which is why this went unnoticed. Covers ImageLoader::assemble_volume... +TEST(TEST_NYXUS, TEST_OMEZARR_MULTICHUNK_FACADE_VOLUME) { + ASSERT_NO_THROW (test_omezarr_facade_volume("dim5_multichunk.ome.zarr", 2, 3, 4)); +} +// ...and RawImageLoader::load_volume (the prescan), which had the same single-tile bug: +// the encoded values are 1..1152 over all (c,t), and the ROI is the whole X*Y*Z volume. +TEST(TEST_NYXUS, TEST_OMEZARR_MULTICHUNK_PRESCAN) { + fs::path ip = omezarr_data_path("dim5_multichunk.ome.zarr"); + ASSERT_TRUE(fs::exists(ip)) << ip.string(); + + Environment e; + SlideProps p (ip.string(), ""); // whole-slide: no mask + ASSERT_TRUE(Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + + EXPECT_DOUBLE_EQ(p.min_preroi_inten, 1.0); + EXPECT_DOUBLE_EQ(p.max_preroi_inten, 1152.0); + EXPECT_EQ(p.max_roi_area, (size_t)(8 * 6 * 4)); +} + +// PARTIAL edge chunks: chunk (4,5) does not divide the 6x8 plane, so the last row-chunk is 2 +// tall and the last col-chunk is 3 wide. dim5_multichunk above (3x4 over 6x8) tiles exactly, +// so the validH/validW seam clamp never ran on the OME-Zarr path either. Asserting the exact +// value at every voxel is the seam check; same 1..1152 TCZYX encoding as dim5_multichunk. +TEST(TEST_NYXUS, TEST_OMEZARR_ODDCHUNK_FACADE_VOLUME) { + ASSERT_NO_THROW (test_omezarr_facade_volume("dim5_oddchunk.ome.zarr", 2, 3, 4)); +} +TEST(TEST_NYXUS, TEST_OMEZARR_ODDCHUNK_PRESCAN) { + fs::path ip = omezarr_data_path("dim5_oddchunk.ome.zarr"); + ASSERT_TRUE(fs::exists(ip)) << ip.string(); + + Environment e; + SlideProps p (ip.string(), ""); // whole-slide: no mask + ASSERT_TRUE(Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + + EXPECT_DOUBLE_EQ(p.min_preroi_inten, 1.0); + EXPECT_DOUBLE_EQ(p.max_preroi_inten, 1152.0); + EXPECT_EQ(p.max_roi_area, (size_t)(8 * 6 * 4)); +} + +// Negative: out-of-range channel/timeframe through the whole-volume facade must throw. +TEST(TEST_NYXUS, TEST_OMEZARR_LOAD_VOLUME_OUT_OF_RANGE) { + ASSERT_NO_THROW (test_omezarr_load_volume_out_of_range()); +} + // Negative: out-of-range Z/C/T plane index must throw. TEST(TEST_NYXUS, TEST_OMEZARR_OUT_OF_RANGE_THROWS) { ASSERT_NO_THROW (test_omezarr_out_of_range_throws()); @@ -2614,6 +2751,31 @@ TEST(TEST_NYXUS, TEST_OMEZARR_MALFORMED_THROWS) { ASSERT_NO_THROW (test_omezarr_malformed_throws()); } +// N1 (negative): a Zarr v3 store whose zarr.json declares a codec z5 does not support. z5's +// readV3CodecsFromJson throws "unsupported zarr v3 codec" during metadata parse (openDataset), +// so the loader must surface a clean error, not crash. Both loader stacks must reject it. +TEST(TEST_NYXUS, TEST_OMEZARR_V3_UNSUPPORTED_CODEC_THROWS) { + fs::path ds = omezarr_data_path("dim5_v3_badcodec.ome.zarr"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + EXPECT_ANY_THROW(NyxusOmeZarrLoader(1, ds.string())); + EXPECT_ANY_THROW(RawOmezarrLoader(ds.string())); +} + +// P4 (positive): the crash's positive twin on the OME-Zarr path -- a T>1 Zarr intensity paired +// with a single-timeframe ZYX Zarr mask. Zarr never crashed (no T axis to over-index), but it +// was never asserted. The prescan must reuse the mask across timeframes and find the ROI. +TEST(TEST_NYXUS, TEST_OMEZARR_MULTITIMEFRAME_MASK_PRESCAN) { + fs::path ip = omezarr_data_path("dim5.ome.zarr"); // T=2, C=3, Z=4 + fs::path mp = omezarr_data_path("dim3_mask.ome.zarr"); // ZYX (T=1) label mask + ASSERT_TRUE(fs::exists(ip) && fs::exists(mp)); + Environment e; + SlideProps p (ip.string(), mp.string()); + bool ok = false; + ASSERT_NO_THROW(ok = Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + EXPECT_TRUE(ok); + EXPECT_EQ(p.max_roi_area, (size_t)(4 * 4 * 6)); +} + #endif // OMEZARR_SUPPORT @@ -2631,6 +2793,161 @@ TEST(TEST_NYXUS, TEST_OMETIFF_ALL_5D_PERMUTATIONS) { ASSERT_NO_THROW (test_ometiff_all_5d_permutations()); } +// Regression (found by the scale/stress harness): a MULTI-timeframe OME-TIFF paired with a +// single-timeframe (ZYX) mask. The 3D prescan (RawImageLoader::for_each_voxel) reuses the +// mask across every intensity timeframe, but read it at the intensity's timeframe -- so for +// t>0 the TIFF mask loader addressed an IFD past its last plane and TIFFSetDirectory threw, +// UNCAUGHT, crashing the process (0xC0000409). Zarr masks have no T axis to over-index, so +// only TIFF crashed. dim5 is T=2,C=3,Z=4; dim3_mask is its ZYX (T=1) segmentation of one ROI +// (label 1 over z=all, y in [1,5), x in [1,7) = 4*4*6 voxels). Pre-fix this threw. +TEST(TEST_NYXUS, TEST_OMETIFF_MULTITIMEFRAME_MASK_PRESCAN) { + fs::path ip = ometiff_data_path("dim5.ome.tif"); + fs::path mp = ometiff_data_path("dim3_mask.ome.tif"); + ASSERT_TRUE(fs::exists(ip)) << ip.string(); + ASSERT_TRUE(fs::exists(mp)) << mp.string(); + + Environment e; + SlideProps p (ip.string(), mp.string()); + bool ok = false; + ASSERT_NO_THROW(ok = Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + EXPECT_TRUE(ok); + EXPECT_EQ(p.max_roi_area, (size_t)(4 * 4 * 6)); // z=4 * y=4 * x=6 +} + +// P3 (positive): the crash's regression above covers the PRESCAN (for_each_voxel); this covers +// the FEATURIZE facade. ImageLoader::load_volume(c,t) forwards t as the mask timeframe, so with +// a T>1 intensity and a T=1 mask, load_volume(c,1) exercises the internal mask-timeframe clamp +// (image_loader.cpp). It must not throw, must reuse the same mask across timeframes, and must +// read different intensity per timeframe. dim5 is T=2; dim3_mask is its ZYX (T=1) mask. +TEST(TEST_NYXUS, TEST_OMETIFF_MULTITIMEFRAME_MASK_FACADE) { + fs::path ip = ometiff_data_path("dim5.ome.tif"); + fs::path mp = ometiff_data_path("dim3_mask.ome.tif"); + ASSERT_TRUE(fs::exists(ip) && fs::exists(mp)); + SlideProps p; p.fname_int = ip.string(); p.fname_seg = mp.string(); + FpImageOptions fp; ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ip.string(); + + ASSERT_TRUE(il.load_volume(0, 0)); + const std::vector seg_t0 = il.get_seg_volume_buffer(); + const std::vector int_t0 = il.get_int_volume_buffer(); + // timeframe 1 with a single-timeframe mask must NOT throw (clamp), reuse the mask, and + // deliver different intensity + ASSERT_TRUE(il.load_volume(0, 1)); + EXPECT_EQ(il.get_seg_volume_buffer(), seg_t0) << "mask changed across timeframes"; + EXPECT_NE(il.get_int_volume_buffer(), int_t0) << "intensity t=1 read t=0 data"; + il.close(); +} + +// P2 (positive): OME-TIFF physical calibration -> SlideProps (the TIFF twin of the OME-Zarr +// calibration test, and it additionally checks the scan_slide_props propagation the Zarr test +// omits). dim5_calibrated carries PhysicalSizeX/Y=0.5, Z=2.0 micrometer in its OME-XML. +TEST(TEST_NYXUS, TEST_OMETIFF_PHYSICAL_CALIBRATION) { + fs::path cal = ometiff_data_path("dim5_calibrated.ome.tif"); + ASSERT_TRUE(fs::exists(cal)) << cal.string(); + + Environment e; + SlideProps p (cal.string(), ""); + ASSERT_TRUE(Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + EXPECT_DOUBLE_EQ(p.phys_x, 0.5); + EXPECT_DOUBLE_EQ(p.phys_y, 0.5); + EXPECT_DOUBLE_EQ(p.phys_z, 2.0); + EXPECT_EQ(p.phys_unit, "micrometer"); + + // an uncalibrated OME-TIFF must default to 1.0 / no unit + SlideProps p2 (ometiff_data_path("dim5.ome.tif").string(), ""); + ASSERT_TRUE(Nyxus::scan_slide_props(p2, 3, e.anisoOptions, e.resultOptions.need_annotation())); + EXPECT_DOUBLE_EQ(p2.phys_x, 1.0); + EXPECT_DOUBLE_EQ(p2.phys_z, 1.0); + EXPECT_TRUE(p2.phys_unit.empty()); +} + +// N2 (negative): a block maps a plane to an IFD PAST the end of the file (an +// in-file overrun, distinct from a multi-file UUID). ifdForPlane returns 99, so the read must +// throw cleanly at TIFFSetDirectory -- not crash and not read a wrong plane. dim5_badifd has +// 4 z-planes; plane z3 claims IFD=99. +TEST(TEST_NYXUS, TEST_OMETIFF_BAD_IFD_THROWS) { + fs::path ds = ometiff_data_path("dim5_badifd.ome.tif"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + SlideProps p; p.fname_int = ds.string(); p.fname_seg = ""; + FpImageOptions fp; ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ds.string(); + // z0..z2 are fine; assembling the whole volume must hit z3's bad IFD and throw, not crash + EXPECT_ANY_THROW(il.load_volume(0, 0)); + il.close(); +} + +// N3 (negative): an all-background (all-zero) mask -> ZERO ROIs. The prescan must complete +// cleanly (no divide-by-zero, no garbage), reporting no ROI area, rather than crash. +TEST(TEST_NYXUS, TEST_OMETIFF_EMPTY_MASK_ZERO_ROIS) { + fs::path ip = ometiff_data_path("dim5.ome.tif"); + fs::path mp = ometiff_data_path("dim3_emptymask.ome.tif"); + ASSERT_TRUE(fs::exists(ip) && fs::exists(mp)); + Environment e; + SlideProps p (ip.string(), mp.string()); + bool ok = false; + ASSERT_NO_THROW(ok = Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + EXPECT_TRUE(ok); + EXPECT_EQ(p.max_roi_area, (size_t)0) << "empty mask should yield no ROI"; +} + +// N4 (edge): a mask with MORE channels than the intensity (C=2 mask, C=1-effective use). The +// featurize loop iterates the intensity's channels, so the extra mask channel must simply be +// ignored (mask channel clamped to what is asked), not crash or misread. dim3_zyx (C=1) paired +// with a C=2 label mask; the ROI is identical on both mask channels. +TEST(TEST_NYXUS, TEST_OMETIFF_MASK_MORE_CHANNELS_THAN_INTENSITY) { + fs::path ip = ometiff_data_path("dim3_zyx.ome.tif"); // C=1, Z=4 + fs::path mp = ometiff_data_path("dim4_mask_c2.ome.tif"); // C=2 label mask + ASSERT_TRUE(fs::exists(ip) && fs::exists(mp)); + Environment e; + SlideProps p (ip.string(), mp.string()); + bool ok = false; + ASSERT_NO_THROW(ok = Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + EXPECT_TRUE(ok); + EXPECT_EQ(p.max_roi_area, (size_t)(4 * 4 * 6)); // the one ROI, read from mask channel 0 +} + +// P1 (positive + negative): detect_input_format is the single dispatch point all three loader +// stacks use; it had no direct test. Extension classification + OME content-sniff. +TEST(TEST_NYXUS, TEST_DETECT_INPUT_FORMAT) { + using Nyxus::detect_input_format; + using Nyxus::ContainerKind; + // OME content present -> OmeTiff + is_ome + auto a = detect_input_format(ometiff_data_path("dim5.ome.tif").string()); + EXPECT_EQ(a.kind, ContainerKind::OmeTiff); + EXPECT_TRUE(a.is_ome); + // a plain multipage TIFF (no OME-XML) -> TiffPlain, not OME + auto b = detect_input_format(ometiff_data_path("dim3_plain.tif").string()); + EXPECT_EQ(b.kind, ContainerKind::TiffPlain); + EXPECT_FALSE(b.is_ome); + // extension-only kinds (no file needed / not opened) + EXPECT_EQ(detect_input_format("x.dcm").kind, ContainerKind::Dicom); + EXPECT_EQ(detect_input_format("x.nii.gz").kind, ContainerKind::Nifti); + // a .zarr path with no multiscales metadata -> OmeZarr kind but is_ome=false + auto z = detect_input_format("no_such_store.zarr"); + EXPECT_EQ(z.kind, ContainerKind::OmeZarr); + EXPECT_FALSE(z.is_ome); +} + +// Pyramidal OME-TIFF: every full-res plane's IFD carries downsampled levels as SubIFDs (tag +// 330), which live OUTSIDE the main IFD chain. This must not shift full-res plane addressing: +// TIFFNumberOfDirectories still returns Z (not Z*levels) and ifdForPlane -> main-chain IFD +// still lands on the full-res plane. nyxus reads level 0 only; the facade check (which also +// spans a 2x3 tile grid) asserts every full-res voxel is correct despite the SubIFDs. Z=6 +// z-stack (C=1,T=1), its own encoding. +TEST(TEST_NYXUS, TEST_OMETIFF_PYRAMID_SUBIFD_FULLRES) { + ASSERT_NO_THROW (test_ometiff_multitile_facade_volume("dim5_pyramid.ome.tif", 1, 1, 6, 32, 48)); +} + +// Non-canonical plane->IFD mapping: dim5_reordered stores its planes in REVERSED +// IFD order and declares the mapping via per-plane blocks. A reader that +// ignores TiffData and assumes contiguous-from-IFD-0 order reads the reversed plane's pixels; +// honoring the map reads correctly. load_volume loops Z through loadTileFromFile -> ifdForPlane +// (both loader stacks route here), so the facade check asserts the right plane per (z,c). +// T=1,C=2,Z=3 (its own encoding). This is the OME-TIFF counterpart to what bioformats emits. +TEST(TEST_NYXUS, TEST_OMETIFF_TIFFDATA_REORDERED) { + ASSERT_NO_THROW (test_ometiff_multitile_facade_volume("dim5_reordered.ome.tif", 1, 2, 3, 6, 8)); +} + // 4D (rank-4): time-only and channel-only. TEST(TEST_NYXUS, TEST_OMETIFF_4D_TZYX) { ASSERT_NO_THROW (test_ometiff_addressing("dim4_tzyx.ome.tif", 2, 1, 4)); @@ -2646,6 +2963,63 @@ TEST(TEST_NYXUS, TEST_OMETIFF_WHOLEVOLUME_CONSUMER) { ASSERT_NO_THROW (test_ometiff_wholevolume_consumer("dim3_zyx.ome.tif", 4)); } +// Wired consumer reads the correct plane for every (channel, timeframe), not just (0,0). +TEST(TEST_NYXUS, TEST_OMETIFF_WHOLEVOLUME_CONSUMER_CT) { + ASSERT_NO_THROW (test_ometiff_wholevolume_consumer_ct("dim5.ome.tif", 2, 3, 4)); +} + +// TILED multi-plane OME-TIFF: the tile loaders map (z,c,t)->IFD (distinct from strip loaders). +TEST(TEST_NYXUS, TEST_OMETIFF_TILED_ADDRESSING) { + ASSERT_NO_THROW (test_ometiff_tiled_addressing()); +} +// Facade whole-volume assembly over the TILED path (open() routes tiled TIFF -> tile loader). +TEST(TEST_NYXUS, TEST_OMETIFF_TILED_FACADE_VOLUME) { + ASSERT_NO_THROW (test_ometiff_facade_volume("dim5_tiled.ome.tif", 2, 3, 4)); +} +// Multi-TILE planes (2x3 grid of 16x16 tiles): dim5_tiled above has ONE tile per plane, so +// it passes even when only tile (0,0) is read. This is the OME-TIFF counterpart of +// TEST_OMEZARR_MULTICHUNK_FACADE_VOLUME. Covers ImageLoader::assemble_volume... +TEST(TEST_NYXUS, TEST_OMETIFF_MULTITILE_FACADE_VOLUME) { + ASSERT_NO_THROW (test_ometiff_multitile_facade_volume("dim5_multitile.ome.tif", 1, 2, 2, 32, 48)); +} +// ...and RawImageLoader::for_each_voxel (the prescan), which had the same single-tile bug. +// Encoded values run 1..6144 over all (c,t); whole-slide, so the ROI is the whole volume. +TEST(TEST_NYXUS, TEST_OMETIFF_MULTITILE_PRESCAN) { + fs::path ip = ometiff_data_path("dim5_multitile.ome.tif"); + ASSERT_TRUE(fs::exists(ip)) << ip.string(); + + Environment e; + SlideProps p (ip.string(), ""); // whole-slide: no mask + ASSERT_TRUE(Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + + EXPECT_DOUBLE_EQ(p.min_preroi_inten, 1.0); + EXPECT_DOUBLE_EQ(p.max_preroi_inten, 6144.0); + EXPECT_EQ(p.max_roi_area, (size_t)(48 * 32 * 2)); +} + +// PARTIAL edge tiles: 40x24 plane / 16 -> 3x2 tiles, last row-tile 8 tall, last col-tile 8 +// wide. Every multi-tile fixture above has plane dims that are exact multiples of the tile +// size, so the validH/validW seam clamp -- min(tileDim, fullDim-offset) -- had never run. +// Reading the exact value at every voxel checks the partial tiles are copied without garbage +// past the seam and without over-reading the tile buffer. Encoded 1..1920 over (c in 0,1). +TEST(TEST_NYXUS, TEST_OMETIFF_ODDTILE_FACADE_VOLUME) { + ASSERT_NO_THROW (test_ometiff_multitile_facade_volume("dim5_oddtile.ome.tif", 1, 2, 1, 40, 24)); +} +// The prescan (for_each_voxel) walks the same partial grid; its slide min/max and ROI area +// would be wrong if a partial tile were mis-clamped (a too-large validH double-counts voxels). +TEST(TEST_NYXUS, TEST_OMETIFF_ODDTILE_PRESCAN) { + fs::path ip = ometiff_data_path("dim5_oddtile.ome.tif"); + ASSERT_TRUE(fs::exists(ip)) << ip.string(); + + Environment e; + SlideProps p (ip.string(), ""); // whole-slide: no mask + ASSERT_TRUE(Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + + EXPECT_DOUBLE_EQ(p.min_preroi_inten, 1.0); + EXPECT_DOUBLE_EQ(p.max_preroi_inten, 1920.0); // 1 + (((0*2+1)*1+0)*40+39)*24+23 + EXPECT_EQ(p.max_roi_area, (size_t)(24 * 40 * 1)); +} + // Facade whole-volume assembly (load_volume loops Z into one X*Y*Z buffer). TEST(TEST_NYXUS, TEST_OMETIFF_FACADE_VOLUME_3D) { ASSERT_NO_THROW (test_ometiff_facade_volume("dim3_zyx.ome.tif", 1, 1, 4)); @@ -2673,6 +3047,26 @@ TEST(TEST_NYXUS, TEST_RAW_OMETIFF_PLAIN_MULTIPAGE_FALLBACK) { ASSERT_NO_THROW (test_raw_ometiff_addressing("dim3_plain.tif", 1, 1, 4)); } +// Negative: out-of-range channel/timeframe through the whole-volume facade must throw. +TEST(TEST_NYXUS, TEST_OMETIFF_LOAD_VOLUME_OUT_OF_RANGE) { + ASSERT_NO_THROW (test_ometiff_load_volume_out_of_range()); +} + +// Regression: a single-channel mask is reused across all intensity channels (not read OOB). +TEST(TEST_NYXUS, TEST_OMETIFF_MULTICHANNEL_MASK_PAIRING) { + ASSERT_NO_THROW (test_ometiff_multichannel_mask_pairing()); +} + +// Strip loaders advertise the OME C/T extents; the plain (non-OME) multi-page TIFF +// keeps C=T=1 (its pages are Z-slices, not channels/timeframes). +TEST(TEST_NYXUS, TEST_OMETIFF_CT_COUNTS) { + ASSERT_NO_THROW (test_ometiff_ct_counts("dim5.ome.tif", 2, 3, 4)); + ASSERT_NO_THROW (test_ometiff_ct_counts("dim4_tzyx.ome.tif", 2, 1, 4)); + ASSERT_NO_THROW (test_ometiff_ct_counts("dim4_czyx.ome.tif", 1, 3, 4)); + ASSERT_NO_THROW (test_ometiff_ct_counts("dim3_zyx.ome.tif", 1, 1, 4)); + ASSERT_NO_THROW (test_ometiff_ct_counts("dim3_plain.tif", 1, 1, 4)); +} + // Negative: out-of-range Z/C/T plane index must throw. TEST(TEST_NYXUS, TEST_OMETIFF_OUT_OF_RANGE_THROWS) { ASSERT_NO_THROW (test_ometiff_out_of_range_throws()); @@ -2684,6 +3078,203 @@ TEST(TEST_NYXUS, TEST_OMETIFF_MALFORMED_THROWS) { } +// Regression: the 3D prescan must scan the WHOLE volume of EVERY (channel, timeframe), +// not one Z-plane of (c0,t0). dim5.ome.tif is C=3,T=2,Z=4,Y=6,X=8 encoding values 1..1152, +// so the slide intensity range must be exactly [1, 1152]. Before the fix the prescan +// (a) did a single load_tile(0,0) and then indexed W*H*D voxels off that ONE-plane buffer, +// reading out of bounds (observed range 0..44,465 of garbage), and (b) covered only +// (c0,t0) -> a range of 1..192, which under-sized intensity-indexed buffers for c>0 and +// segfaulted. The area check guards that ROI geometry is taken from the first pass only +// (otherwise it would be multiplied by n_channels*n_timeframes). +TEST(TEST_NYXUS, TEST_3D_PRESCAN_SLIDE_RANGE) { + fs::path ip = ometiff_data_path("dim5.ome.tif"); + ASSERT_TRUE(fs::exists(ip)) << ip.string(); + + Environment e; + SlideProps p (ip.string(), ""); // whole-slide: no mask + ASSERT_TRUE(Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + + EXPECT_EQ(p.inten_channels, (size_t)3); + EXPECT_EQ(p.inten_time, (size_t)2); + EXPECT_EQ(p.volume_d, (size_t)4); + // the full encoded range across ALL (c,t) -- not garbage, and not just (c0,t0)'s 1..192 + EXPECT_DOUBLE_EQ(p.min_preroi_inten, 1.0); + EXPECT_DOUBLE_EQ(p.max_preroi_inten, 1152.0); + // geometry from the first pass only: one whole-slide ROI of exactly X*Y*Z voxels + EXPECT_EQ(p.max_roi_area, (size_t)(8 * 6 * 4)); +} + +// Regression: the 3D WHOLE-VOLUME reduce path. reduce_trivial_3d_wholevolume calls +// D3_VoxelIntensityFeatures::extract(), which used to invoke the 2-arg calculate() -- a stub +// that throws "illegal call" -- so EVERY 3D whole-volume featurization died before writing a +// row. The segmented path reduces via reduce(), which passes the Dataset, so nothing covered +// this. Mirrors featurize_wholevolume()'s vROI setup, then reduces. +TEST(TEST_NYXUS, TEST_3D_WHOLEVOLUME_REDUCE) { + fs::path ds = ometiff_data_path("dim3_zyx.ome.tif"); // 3D X8 Y6 Z4 + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + Environment e; + // enable the 3D intensity features so the reduce actually runs them + e.theFeatureSet.enableAll(false); + e.theFeatureSet.enableFeatures(D3_VoxelIntensityFeatures::featureset); + + // prescan the slide (whole-volume => no mask) + e.dataset.dataset_props.reserve(1); + SlideProps& sp = e.dataset.dataset_props.emplace_back(ds.string(), ""); + ASSERT_TRUE(Nyxus::scan_slide_props(sp, 3, e.anisoOptions, e.resultOptions.need_annotation())); + e.dataset.update_dataset_props_extrema(); + + // build the vROI exactly as featurize_wholevolume() does + FpImageOptions fp; + ImageLoader ilo; + ASSERT_TRUE(ilo.open(sp, fp)) << ds.string(); + LR vroi(1); + vroi.slide_idx = 0; + vroi.aux_area = sp.max_roi_area; + vroi.aabb.init_from_whd(sp.max_roi_w, sp.max_roi_h, sp.max_roi_d); + vroi.aux_min = (PixIntens)0; + vroi.aux_max = (PixIntens)(sp.max_preroi_inten - sp.min_preroi_inten); + ASSERT_NO_THROW(vroi.initialize_fvals()); + ASSERT_TRUE(Nyxus::scan_trivial_wholevolume(vroi, ds.string(), ilo, 0/*channel*/, 0/*timeframe*/)); + ASSERT_GT(vroi.raw_pixels_3D.size(), 0u); + vroi.aux_image_cube.allocate(vroi.aabb.get_width(), vroi.aabb.get_height(), vroi.aabb.get_z_depth()); + vroi.aux_image_cube.calculate_from_pixelcloud(vroi.raw_pixels_3D, vroi.aabb); + + // THE REGRESSION: this used to throw "illegal call of D3_VoxelIntensityFeatures::calculate" + ASSERT_NO_THROW(Nyxus::reduce_trivial_3d_wholevolume(e, vroi)); + + // and it must actually produce values (MAX >= MIN, both finite) + double vmin = vroi.get_fvals((int)Nyxus::Feature3D::MIN)[0]; + double vmax = vroi.get_fvals((int)Nyxus::Feature3D::MAX)[0]; + EXPECT_GE(vmax, vmin); + EXPECT_GT(vmax, 0.0); + ilo.close(); +} + +// Regression: separatecsv derives ONE output path per slide, but the CSV sinks are invoked +// once per (channel, timeframe) plane and used to open that path with mode "w" every time -- +// so each plane truncated the one before it and the file ended up holding only the LAST +// channel. Confirmed against the pre-fix build, where this file had 1 data row (c_index=1) +// instead of 2. The t_index/c_index columns exist precisely so the planes can coexist as rows. +TEST(TEST_NYXUS, TEST_CSV_MULTICHANNEL_NO_OVERWRITE) { + fs::path ds = ometiff_data_path("dim3_zyx.ome.tif"); // 3D X8 Y6 Z4 + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + fs::path outdir = fs::temp_directory_path() / "nyxus_csv_ct_test"; + fs::remove_all(outdir); + fs::create_directories(outdir); + + Environment e; + e.separateCsv = true; // the mode that overwrote (and the default) + e.output_dir = outdir.string(); + e.theFeatureSet.enableAll(false); + e.theFeatureSet.enableFeatures(D3_VoxelIntensityFeatures::featureset); + + e.dataset.dataset_props.reserve(1); + SlideProps& sp = e.dataset.dataset_props.emplace_back(ds.string(), ""); + ASSERT_TRUE(Nyxus::scan_slide_props(sp, 3, e.anisoOptions, e.resultOptions.need_annotation())); + e.dataset.update_dataset_props_extrema(); + + LR vroi(1); + vroi.slide_idx = 0; + vroi.aux_area = sp.max_roi_area; + vroi.aabb.init_from_whd(sp.max_roi_w, sp.max_roi_h, sp.max_roi_d); + ASSERT_NO_THROW(vroi.initialize_fvals()); + + // Two channel planes of the SAME slide, exactly as the whole-volume workflow emits them + ASSERT_TRUE(Nyxus::save_features_2_csv_wholeslide (e, vroi, ds.string(), "", outdir.string(), 0, 0)); + ASSERT_TRUE(Nyxus::save_features_2_csv_wholeslide (e, vroi, ds.string(), "", outdir.string(), 0, 1)); + + // Read the single file back + std::vector lines; + { + std::ifstream f (Nyxus::get_feature_output_fname (e, ds.string(), "")); + ASSERT_TRUE(f.good()); + std::string ln; + while (std::getline(f, ln)) + if (!ln.empty()) + lines.push_back(ln); + } + + ASSERT_EQ(lines.size(), (size_t)3) << "expected 1 header + one row per channel plane"; + EXPECT_NE(lines[0].find("\"c_index\""), std::string::npos) << "line 0 must be the header"; + // ...and the header must appear exactly once, not once per plane + EXPECT_EQ(lines[1].find("\"c_index\""), std::string::npos); + EXPECT_EQ(lines[2].find("\"c_index\""), std::string::npos); + + fs::remove_all(outdir); +} + +// Phase 6 physical-calibration logic (negative + positive). resolve_slide_anisotropy +// must NOT engage the anisotropic (resampling) path unless it's genuinely warranted: +// - flag off -> false, (1,1,1) even with anisotropic spacing +// - degenerate spacing (a 0 axis) -> false, (1,1,1) (guarded, no div-by-zero) +// - isotropic spacing (all equal) -> false, (1,1,1) (nothing to correct) +// - out-of-range slide index -> false, (1,1,1) (no OOB read) +// - real anisotropic spacing -> true, ratios normalized so min == 1 +// - explicit --aniso* -> true, the CLI values (win over physical) +TEST(TEST_NYXUS, TEST_RESOLVE_SLIDE_ANISOTROPY) { + Environment e; + e.use_physical_spacing_ = true; // opt-in on; anisoOptions stays un-customized + e.dataset.dataset_props.clear(); + SlideProps p; // ctor sets phys_x/y/z = 1.0 + e.dataset.dataset_props.push_back(p); + double ax = -1, ay = -1, az = -1; + + // degenerate: a zero-length axis must not divide-by-zero -> isotropic fallback + e.dataset.dataset_props[0].phys_x = 1.0; e.dataset.dataset_props[0].phys_y = 1.0; e.dataset.dataset_props[0].phys_z = 0.0; + EXPECT_FALSE(Nyxus::resolve_slide_anisotropy(e, 0, ax, ay, az)); + EXPECT_DOUBLE_EQ(ax, 1.0); EXPECT_DOUBLE_EQ(ay, 1.0); EXPECT_DOUBLE_EQ(az, 1.0); + + // isotropic but non-unit spacing -> normalized to (1,1,1) -> no anisotropic path + e.dataset.dataset_props[0].phys_x = 2.0; e.dataset.dataset_props[0].phys_y = 2.0; e.dataset.dataset_props[0].phys_z = 2.0; + EXPECT_FALSE(Nyxus::resolve_slide_anisotropy(e, 0, ax, ay, az)); + EXPECT_DOUBLE_EQ(az, 1.0); + + // out-of-range slide index -> safe (no dataset_props[99] read) + EXPECT_FALSE(Nyxus::resolve_slide_anisotropy(e, 99, ax, ay, az)); + EXPECT_DOUBLE_EQ(ax, 1.0); + + // genuinely anisotropic voxels (z 4x thicker) -> engage, ratio-normalized min == 1 + e.dataset.dataset_props[0].phys_x = 0.5; e.dataset.dataset_props[0].phys_y = 0.5; e.dataset.dataset_props[0].phys_z = 2.0; + EXPECT_TRUE(Nyxus::resolve_slide_anisotropy(e, 0, ax, ay, az)); + EXPECT_DOUBLE_EQ(ax, 1.0); EXPECT_DOUBLE_EQ(ay, 1.0); EXPECT_DOUBLE_EQ(az, 4.0); + + // flag OFF -> never engage, even with anisotropic spacing present + e.use_physical_spacing_ = false; + EXPECT_FALSE(Nyxus::resolve_slide_anisotropy(e, 0, ax, ay, az)); + EXPECT_DOUBLE_EQ(az, 1.0); +} + +// TEST_RESOLVE_SLIDE_ANISOTROPY covers the DECISION (physical spacing -> ratios). This covers +// that the resolved ratios actually RESCALE ROI geometry end-to-end: the 3D prescan's +// anisotropic branch (make_anisotropic_aabb 3-arg -> AABB::apply_anisotropy) was never +// exercised -- every other test uses make_nonanisotropic_aabb. A customized az=4 must scale the +// ROI's z-depth ~4x while leaving x/y (ax=ay=1) unchanged; without applying anisotropy the +// depth would be identical to the isotropic run. dim3_mask's ROI spans all Z (depth 4). +TEST(TEST_NYXUS, TEST_3D_ANISOTROPY_RESCALES_ROI_DEPTH) { + fs::path ip = ometiff_data_path("dim5.ome.tif"); + fs::path mp = ometiff_data_path("dim3_mask.ome.tif"); + ASSERT_TRUE(fs::exists(ip) && fs::exists(mp)); + Environment e; + + SlideProps iso (ip.string(), mp.string()); + AnisotropyOptions aniso_off; // un-customized -> isotropic AABB + ASSERT_FALSE(aniso_off.customized()); + ASSERT_TRUE(Nyxus::scan_slide_props(iso, 3, aniso_off, e.resultOptions.need_annotation())); + + SlideProps ani (ip.string(), mp.string()); + AnisotropyOptions aniso_z4; + aniso_z4.set_aniso_z(4.0); // z 4x thicker + ASSERT_TRUE(aniso_z4.customized()); + ASSERT_TRUE(Nyxus::scan_slide_props(ani, 3, aniso_z4, e.resultOptions.need_annotation())); + + EXPECT_GT(ani.max_roi_d, iso.max_roi_d) << "z-anisotropy did not rescale ROI depth"; + EXPECT_GE(ani.max_roi_d, iso.max_roi_d * 3) << "z-depth not scaled ~4x"; + EXPECT_EQ(ani.max_roi_w, iso.max_roi_w) << "x (ax=1) must be unchanged"; + EXPECT_EQ(ani.max_roi_h, iso.max_roi_h) << "y (ay=1) must be unchanged"; +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/tests/test_arrow_mechanics.h b/tests/test_arrow_mechanics.h index 9c5dc0f53..e5abacec5 100644 --- a/tests/test_arrow_mechanics.h +++ b/tests/test_arrow_mechanics.h @@ -64,42 +64,34 @@ std::shared_ptr create_features_table(const std::vector> fields; + // FIX (IO): 3 leading string columns (intensity, mask, phys_unit) + ROI_label int32 + float64 rest + const int num_str_cols = 3; fields.push_back(arrow::field(header[0], arrow::utf8())); fields.push_back(arrow::field(header[1], arrow::utf8())); - fields.push_back(arrow::field(header[2], arrow::int32())); + fields.push_back(arrow::field(header[2], arrow::utf8())); + fields.push_back(arrow::field(header[3], arrow::int32())); - for (int i = 3; i < header.size(); ++i) + for (int i = 4; i < header.size(); ++i) { fields.push_back(arrow::field(header[i], arrow::float64())); } auto schema = arrow::schema(fields); - arrow::StringBuilder string_builder_0; - - std::vector temp_string_vec1(string_columns.size()/2); - std::vector temp_string_vec2(string_columns.size()/2); - - for (int i = 0; i < string_columns.size(); i+=2) { - temp_string_vec1[i/2] = string_columns[i]; - temp_string_vec2[i/2] = string_columns[i+1]; - } - - PARQUET_THROW_NOT_OK(string_builder_0.AppendValues(temp_string_vec1)); - - arrow::StringBuilder string_builder_1; - - PARQUET_THROW_NOT_OK(string_builder_1.AppendValues(temp_string_vec2)); - - std::shared_ptr array_0, array_1; - - PARQUET_THROW_NOT_OK(string_builder_0.Finish(&array_0)); - PARQUET_THROW_NOT_OK(string_builder_1.Finish(&array_1)); - std::vector> arrays; - arrays.push_back(array_0); - arrays.push_back(array_1); + // de-interleave the flat string buffer (row-major, num_str_cols per row) into one array per column + for (int c = 0; c < num_str_cols; ++c) + { + std::vector col(number_of_rows); + for (int r = 0; r < number_of_rows; ++r) + col[r] = string_columns[r * num_str_cols + c]; + arrow::StringBuilder sb; + PARQUET_THROW_NOT_OK(sb.AppendValues(col)); + std::shared_ptr arr; + PARQUET_THROW_NOT_OK(sb.Finish(&arr)); + arrays.push_back(arr); + } // add labels arrow::Int32Builder labels_builder; @@ -224,8 +216,13 @@ void test_arrow() for (const auto& row : row_data) { string_columns.push_back(std::get<0>(row)[0]); string_columns.push_back(std::get<0>(row)[1]); + string_columns.push_back(std::get<0>(row)[2]); // phys_unit (FIX (IO): 3rd leading string) numeric_columns.push_back(std::get<1>(row)); // ROI label - numeric_columns.push_back(std::get<2>(row)); // time + numeric_columns.push_back(std::get(row)); // time + numeric_columns.push_back(std::get(row)); // channel (FIX (IO): c_index column) + numeric_columns.push_back(std::get(row)); // phys_x (FIX (IO)) + numeric_columns.push_back(std::get(row)); // phys_y + numeric_columns.push_back(std::get(row)); // phys_z for (const auto& data : std::get(row)) { numeric_columns.push_back(data); } @@ -295,8 +292,13 @@ void test_parquet() { for (const auto& row : row_data) { string_columns.push_back(std::get<0>(row)[0]); string_columns.push_back(std::get<0>(row)[1]); + string_columns.push_back(std::get<0>(row)[2]); // phys_unit (FIX (IO): 3rd leading string) numeric_columns.push_back(std::get<1>(row)); // ROI label - numeric_columns.push_back(std::get<2>(row)); // time + numeric_columns.push_back(std::get(row)); // time + numeric_columns.push_back(std::get(row)); // channel (FIX (IO): c_index column) + numeric_columns.push_back(std::get(row)); // phys_x (FIX (IO)) + numeric_columns.push_back(std::get(row)); // phys_y + numeric_columns.push_back(std::get(row)); // phys_z for (const auto& data : std::get(row)) { numeric_columns.push_back(data); } diff --git a/tests/test_data.h b/tests/test_data.h index cfaf35264..3954e3bed 100644 --- a/tests/test_data.h +++ b/tests/test_data.h @@ -689,7 +689,7 @@ const static NyxusPixel roiDataForPerimeterTest[] = { }; const static std::tuple, std::vector> features888{ - std::make_tuple(std::vector{"intensity_image", "mask_image", "ROI_label", "time", "COV", "COVERED_IMAGE_INTENSITY_RANGE", "ENERGY", + std::make_tuple(std::vector{"intensity_image", "mask_image", "phys_unit", "ROI_label", "time", "c_index", "phys_x", "phys_y", "phys_z", "COV", "COVERED_IMAGE_INTENSITY_RANGE", "ENERGY", "ENTROPY", "EXCESS_KURTOSIS", "HYPERFLATNESS", "HYPERSKEWNESS", "INTEGRATED_INTENSITY", "INTERQUARTILE_RANGE", "KURTOSIS", "MAX", "MEAN", "MEAN_ABSOLUTE_DEVIATION", "MEDIAN", "MEDIAN_ABSOLUTE_DEVIATION", "MIN", "MODE", "P01", "P10", "P25", "P75", "P90", "P99", "QCOD", "RANGE", "ROBUST_MEAN", "ROBUST_MEAN_ABSOLUTE_DEVIATION", "ROOT_MEAN_SQUARED", "SKEWNESS", "STANDARD_DEVIATION", @@ -697,36 +697,44 @@ const static NyxusPixel roiDataForPerimeterTest[] = { std::vector{ FTABLE_RECORD { std::make_tuple( - std::vector{"Segmentation0", "Intensity0"}, + std::vector{"Segmentation0", "Intensity0", ""}, 1, DEFAULT_T_INDEX, + DEFAULT_C_INDEX, + 1.0, 1.0, 1.0, std::vector {0.6550544230766455, 0.8333333333333334, 245.0, 1.4577174691301484, -1.4129028124799794, 2.8044001823929605, 1.0173315649829293, 59.0, 3.009722222222222, 1.5870971875200206, 6.0, 2.95, 1.7550000000000001, 4.0, 1.65, 1.0, 1.0, 1.001111111111111, 1.011111111111111, 1.0277777777777777, 4.0375, 5.966666666666667, 5.996666666666667, 0.5941870030161777, 5.0, 0.0, 1.2, 3.5, 0.20687260936809726, 1.9324105480761042, 1.8834808201837363, 0.43210013459357943, 3.7342105263157896, 3.5475000000000003, 154.0, 28.57142857142857} ) }, FTABLE_RECORD { std::make_tuple( - std::vector{"Segmentation1", "Intensity1"}, + std::vector{"Segmentation1", "Intensity1", ""}, 2, DEFAULT_T_INDEX, + DEFAULT_C_INDEX, + 1.0, 1.0, 1.0, std::vector {0.8045973903258694, 0.8333333333333334, 157.0, 1.457424881409171, -0.5612338213749162, 8.091332781325145, 3.896507376634866, 43.0, 2.9958333333333336, 2.438766178625084, 6.0, 2.263157894736842, 1.5955678670360112, 1.0, 1.263157894736842, 1.0, 1.0, 1.0007916666666667, 1.0079166666666666, 1.0197916666666667, 4.015625, 5.952500000000001, 5.99525, 0.5949524203558131, 5.0, 0.0, 1.2, 2.8745709061939735, 0.9598465178383306, 1.8209309360006518, 1.772364025313972, 0.41775020701019844, 3.3157894736842097, 3.141274238227146, 165.0, 28.57142857142857} ) }, FTABLE_RECORD { std::make_tuple( - std::vector{"Segmentation2", "Intensity2"}, + std::vector{"Segmentation2", "Intensity2", ""}, 3, DEFAULT_T_INDEX, + DEFAULT_C_INDEX, + 1.0, 1.0, 1.0, std::vector {0.9067141318473291, 0.8333333333333334, 83.0, 0.8030717273241024, 2.994153500512417, 42.98771025542074, 14.88820850639021, 29.0, 0.030000000000000027, 5.994153500512417, 6.0, 1.6111111111111112, 1.0185185185185182, 1.0, 0.6111111111111112, 1.0, 1.0, 1.0006, 1.006, 1.015, 1.045, 4.03, 5.991, 0.014563106796116521, 5.0, 0.0, 1.2, 2.1473497877875207, 2.10443379958053, 1.4608172124206968, 1.4196591487978487, 0.3443179189922347, 2.1339869281045756, 2.0154320987654324, 230.0, 28.57142857142857} ) }, FTABLE_RECORD { std::make_tuple( - std::vector{"Segmentation0", "Intensity0"}, + std::vector{"Segmentation0", "Intensity0", ""}, 4, DEFAULT_T_INDEX, + DEFAULT_C_INDEX, + 1.0, 1.0, 1.0, std::vector {.9067141318473291, 0.8333333333333334, 83.0, 0.8030717273241024, 2.994153500512417, 42.98771025542074, 14.88820850639021, 29.0, 0.030000000000000027, 5.994153500512417, 6.0, 1.6111111111111112, 1.0185185185185182, 1.0, 0.6111111111111112, 1.0, 1.0, 1.0006, 1.006, 1.015, 1.045, 4.03, 5.991, 0.014563106796116521, 5.0, 0.0, 1.2, 2.1473497877875207, 2.10443379958053, 1.4608172124206968, 1.4196591487978487, 0.3443179189922347, 2.1339869281045756, 2.0154320987654324, 230.0, 28.57142857142857} ) } diff --git a/tests/test_ome_meta.h b/tests/test_ome_meta.h index 62bf687bc..88e827ffb 100644 --- a/tests/test_ome_meta.h +++ b/tests/test_ome_meta.h @@ -58,6 +58,78 @@ TEST(OmeTiffMeta, ParsesNonDefault5D) EXPECT_EQ(a.storageIndexOf('X'), 4); } +// plane->IFD mapping. Base XML: XYZCT, Z=2 C=2 T=1 -> 4 planes, canonical +// ordinal ord = z + c*2. +static std::string tiffdata_xml(const std::string& blocks) +{ + return "" + blocks + + ""; +} + +// A single canonical block (tifffile's form) means contiguous-from-IFD-0 == the default, so +// the map is left empty and ifdForPlane stays canonical. +TEST(OmeTiffMetaTiffData, CanonicalSingleBlockStaysIdentity) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(tiffdata_xml("")); + ASSERT_TRUE(a.valid); + EXPECT_TRUE(a.planeToIfd.empty()); + EXPECT_FALSE(a.multiFileTiff); + EXPECT_EQ(a.ifdForPlane(0, 0, 0), 0u); + EXPECT_EQ(a.ifdForPlane(1, 1, 0), 3u); // ord = 1 + 1*2 +} + +// Per-plane blocks that reverse the IFDs: ifdForPlane must return the declared IFD. +TEST(OmeTiffMetaTiffData, ReversedPerPlaneMapping) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(tiffdata_xml( + "" + "" + "" + "")); + ASSERT_TRUE(a.valid); + ASSERT_EQ(a.planeToIfd.size(), 4u); + EXPECT_EQ(a.ifdForPlane(0, 0, 0), 3u); + EXPECT_EQ(a.ifdForPlane(1, 0, 0), 2u); + EXPECT_EQ(a.ifdForPlane(0, 1, 0), 1u); + EXPECT_EQ(a.ifdForPlane(1, 1, 0), 0u); +} + +// P5: PlaneCount omitted -> defaults to "the remaining planes from the start plane" (OME +// spec). A single block with only IFD given must map every plane from that offset. +TEST(OmeTiffMetaTiffData, OmittedPlaneCountCoversRemaining) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(tiffdata_xml("")); + ASSERT_TRUE(a.valid); + ASSERT_EQ(a.planeToIfd.size(), 4u); // 4 planes (Z=2,C=2) + EXPECT_EQ(a.ifdForPlane(0, 0, 0), 5u); // start at IFD 5 + EXPECT_EQ(a.ifdForPlane(1, 0, 0), 6u); + EXPECT_EQ(a.ifdForPlane(0, 1, 0), 7u); + EXPECT_EQ(a.ifdForPlane(1, 1, 0), 8u); // all four planes mapped, consecutively +} + +// A non-zero starting IFD (planes contiguous but offset, e.g. a multi-image container). +TEST(OmeTiffMetaTiffData, NonZeroStartOffsetsAllPlanes) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(tiffdata_xml("")); + ASSERT_TRUE(a.valid); + ASSERT_EQ(a.planeToIfd.size(), 4u); + EXPECT_EQ(a.ifdForPlane(0, 0, 0), 10u); + EXPECT_EQ(a.ifdForPlane(1, 1, 0), 13u); +} + +// A naming another file ( child) is multi-file: unsupported, flagged, and +// that plane is left at the canonical fallback rather than pointed at a wrong local IFD. +TEST(OmeTiffMetaTiffData, MultiFileUuidBlockFlaggedAndSkipped) +{ + Nyxus::OmeAxes a = Nyxus::parse_ome_xml(tiffdata_xml( + "" + "urn:uuid:abc")); + ASSERT_TRUE(a.valid); + EXPECT_TRUE(a.multiFileTiff); + EXPECT_EQ(a.ifdForPlane(0, 0, 0), 0u); // canonical fallback (not mapped) +} + TEST(OmeTiffMeta, Parses2DSingletonAxesCollapse) { Nyxus::OmeAxes a = Nyxus::parse_ome_xml(kOmeXml_2d); diff --git a/tests/test_ometiff_mechanics.h b/tests/test_ometiff_mechanics.h index 88da399a6..ba75844a4 100644 --- a/tests/test_ometiff_mechanics.h +++ b/tests/test_ometiff_mechanics.h @@ -103,7 +103,7 @@ void test_ometiff_wholevolume_consumer(const char* store, int Z) ASSERT_TRUE(ilo.open(p, fp)) << ds.string(); LR vroi; - ASSERT_TRUE(Nyxus::scan_trivial_wholevolume(vroi, ds.string(), ilo)); + ASSERT_TRUE(Nyxus::scan_trivial_wholevolume(vroi, ds.string(), ilo, 0/*channel*/, 0/*timeframe*/)); // every voxel of the X*Y*Z volume must be captured (not just plane z=0 -> 48) ASSERT_EQ(vroi.raw_pixels_3D.size(), (size_t)X * Y * Z); @@ -113,6 +113,35 @@ void test_ometiff_wholevolume_consumer(const char* store, int Z) ilo.close(); } +// End-to-end through the wired volumetric consumer for EVERY (channel,timeframe): +// scan_trivial_wholevolume(vroi, .., c, t) must fill the voxel cloud with THAT c/t +// plane's encoded intensity (before the wiring it always read c=0,t=0). +void test_ometiff_wholevolume_consumer_ct(const char* store, int T, int C, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = ometiff_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader ilo; + ASSERT_TRUE(ilo.open(p, fp)) << ds.string(); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + { + LR vroi; + ASSERT_TRUE(Nyxus::scan_trivial_wholevolume(vroi, ds.string(), ilo, c, t)) << store; + ASSERT_EQ(vroi.raw_pixels_3D.size(), (size_t)X * Y * Z) << store; + for (const Pixel3& px : vroi.raw_pixels_3D) + ASSERT_EQ((uint32_t)px.inten, ometiff_enc((int)px.x, (int)px.y, (int)px.z, c, t)) + << store << " (c" << c << " t" << t << ") voxel (" << px.x << "," << px.y << "," << px.z << ")"; + } + ilo.close(); +} + // Whole-volume assembly through the ImageLoader facade: load_volume() stacks all // Z-planes (per (channel,timeframe)) into one X*Y*Z buffer -- the foundation that // lets the volumetric pipeline consume a multi-page OME-TIFF. @@ -147,6 +176,49 @@ void test_ometiff_facade_volume(const char* store, int T, int C, int Z) il.close(); } +// The multitile fixtures carry their own (larger Y/X) shape because a TIFF tile size must be +// a multiple of 16: only a plane bigger than 16x16 yields a real tile grid. Their coordinate +// encoding is encoded_tczyx with each fixture's own C/Z/Y/X (see gen_ome_tiff.write_multitile). +inline uint32_t ometiff_enc_dims(int x, int y, int z, int c, int t, int C, int Z, int Y, int X) +{ + return (uint32_t)(1 + ((((t * C + c) * Z + z) * Y + y) * X + x)); +} + +// Multi-TILE plane: each plane spans a grid of 16x16 tiles, unlike every other fixture (one +// tile/chunk per plane), so assembling a volume from tile (0,0) alone silently returns wrong +// data outside the first 16x16 corner -- and over-reads that tile's buffer. With Y or X not a +// multiple of 16 the last tile of the row/column is PARTIAL, exercising the validH/validW seam +// clamp. Asserting the exact value at every voxel IS the seam check. +void test_ometiff_multitile_facade_volume(const char* store, int T, int C, int Z, int Y, int X) +{ + fs::path ds = ometiff_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ds.string(); + ASSERT_EQ(il.get_full_width(), (size_t)X); + ASSERT_EQ(il.get_full_height(), (size_t)Y); + ASSERT_EQ(il.get_full_depth(), (size_t)Z); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + { + ASSERT_TRUE(il.load_volume(c, t)); + const std::vector& vol = il.get_int_volume_buffer(); + ASSERT_EQ(vol.size(), (size_t)X * Y * Z); + for (int z = 0; z < Z; ++z) + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(vol[(size_t)z * X * Y + (size_t)y * X + x], ometiff_enc_dims(x, y, z, c, t, C, Z, Y, X)) + << store << " vol (x" << x << " y" << y << " z" << z << " c" << c << " t" << t << ")"; + } + il.close(); +} + // Every one of the 6 legal OME DimensionOrder values (all orderings of {T,C,Z} // before Y,X) must read correctly -- proves ifdForPlane honors DimensionOrder. void test_ometiff_all_5d_permutations() @@ -160,6 +232,134 @@ void test_ometiff_all_5d_permutations() } } +// TILED multi-plane OME-TIFF: the TILE loaders (not just the strip loaders) must map +// (z,c,t) -> IFD. dim5_tiled.ome.tif is 5D TCZYX with one 16x16 tile per 6x8 plane. +void test_ometiff_tiled_addressing() +{ + const int T = 2, C = 3, Z = 4, Y = 6, X = 8; + fs::path ds = ometiff_data_path("dim5_tiled.ome.tif"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + // AbstractTileLoader stack + auto ldr = NyxusGrayscaleTiffTileLoader(1, ds.string(), true, 0.0f, 1.0f, 1e4f, false); + ASSERT_EQ(ldr.fullWidth(0), (size_t)X); + ASSERT_EQ(ldr.fullHeight(0), (size_t)Y); + ASSERT_EQ(ldr.fullDepth(0), (size_t)Z); // SizeZ, not the 24 IFDs + ASSERT_EQ(ldr.numberChannels(), (size_t)C); + ASSERT_EQ(ldr.fullTimestamps(0), (size_t)T); + const size_t tw = ldr.tileWidth(0); + auto tile = std::make_shared>(ldr.tileHeight(0) * tw, 0u); + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + for (int z = 0; z < Z; ++z) + { + std::fill(tile->begin(), tile->end(), 0u); + ASSERT_NO_THROW(ldr.loadTileFromFile(tile, 0, 0, z, c, t, 0)); + const std::vector& buf = *tile; + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(buf[y * tw + x], ometiff_enc(x, y, z, c, t)) + << "tile plane (z" << z << " c" << c << " t" << t << ") at (" << x << "," << y << ")"; + } + EXPECT_ANY_THROW(ldr.loadTileFromFile(tile, 0, 0, 0, 99, 0, 0)); // channel out of range + EXPECT_ANY_THROW(ldr.loadTileFromFile(tile, 0, 0, 99, 0, 0, 0)); // z out of range + + // RawFormatLoader stack + auto raw = RawTiffTileLoader(ds.string()); + ASSERT_EQ(raw.fullDepth(0), (size_t)Z); + ASSERT_EQ(raw.numberChannels(), (size_t)C); + ASSERT_EQ(raw.fullTimestamps(0), (size_t)T); + const size_t rw = raw.tileWidth(0); + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + for (int z = 0; z < Z; ++z) + { + ASSERT_NO_THROW(raw.loadTileFromFile(0, 0, z, c, t, 0)); + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(raw.get_uint32_pixel(y * rw + x), ometiff_enc(x, y, z, c, t)) + << "raw tile plane (z" << z << " c" << c << " t" << t << ") at (" << x << "," << y << ")"; + raw.free_tile(); + } + EXPECT_ANY_THROW(raw.loadTileFromFile(0, 0, 0, 0, 99, 0)); // timeframe out of range + EXPECT_ANY_THROW(raw.loadTileFromFile(0, 0, 99, 0, 0, 0)); // z out of range +} + +// Regression: a single-channel mask paired with a multi-channel intensity. The mask is +// channel-agnostic and must be REUSED for every intensity channel (not indexed at the +// intensity's channel, which would read it out of range and drop the ROI for c>0). +// End-to-end this bug produced only the c=0 rows; here we assert at the facade. +void test_ometiff_multichannel_mask_pairing() +{ + fs::path ipath = ometiff_data_path("dim5.ome.tif"); // intensity C=3, T=2, Z=4 + fs::path mpath = ometiff_data_path("dim3_mask.ome.tif"); // single-channel ZYX label mask + ASSERT_TRUE(fs::exists(ipath)) << ipath.string(); + ASSERT_TRUE(fs::exists(mpath)) << mpath.string(); + + SlideProps p; + p.fname_int = ipath.string(); + p.fname_seg = mpath.string(); + FpImageOptions fp; + ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ipath.string(); + + ASSERT_TRUE(il.load_volume(0, 0)); + const std::vector seg0 = il.get_seg_volume_buffer(); + const std::vector int0 = il.get_int_volume_buffer(); + size_t nz = 0; for (auto v : seg0) if (v) ++nz; + ASSERT_GT(nz, 0u) << "mask has no ROI voxels"; + + for (int c = 0; c < 3; ++c) + { + ASSERT_TRUE(il.load_volume(c, 0)) << "channel " << c; // must not throw/fail for c>0 + ASSERT_EQ(il.get_seg_volume_buffer(), seg0) << "mask changed for channel " << c; // reused identically + if (c > 0) + ASSERT_NE(il.get_int_volume_buffer(), int0) << "intensity channel " << c << " read c=0 data"; + } + il.close(); +} + +// Phase 5 negative: the whole-volume facade read must propagate an out-of-range channel +// or timeframe as a throw (the (z,c,t)->IFD map range-guards). dim5 has C=3, T=2. +void test_ometiff_load_volume_out_of_range() +{ + fs::path ds = ometiff_data_path("dim5.ome.tif"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ds.string(); + + EXPECT_ANY_THROW(il.load_volume(99, 0)); // channel out of range (C=3) + EXPECT_ANY_THROW(il.load_volume(0, 99)); // timeframe out of range (T=2) + // in-range still works + EXPECT_TRUE(il.load_volume(2, 1)); + il.close(); +} + +// The strip loaders must ADVERTISE the OME C/T extents via numberChannels() / +// fullTimestamps() -- what the volumetric pipeline keys off to iterate channels +// and timeframes. A plain (non-OME) multi-page TIFF has no OME-XML, so it must keep +// the single-plane default of 1 for both (its pages are all Z-slices). +void test_ometiff_ct_counts(const char* store, int T, int C, int Z) +{ + fs::path ds = ometiff_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto ldr = NyxusGrayscaleTiffStripLoader(1, ds.string()); + ASSERT_EQ(ldr.numberChannels(), (size_t)C) << store; + ASSERT_EQ(ldr.fullTimestamps(0), (size_t)T) << store; + ASSERT_EQ(ldr.fullDepth(0), (size_t)Z) << store; + + auto raw = RawTiffStripLoader(1, ds.string()); + ASSERT_EQ(raw.numberChannels(), (size_t)C) << store; + ASSERT_EQ(raw.fullTimestamps(0), (size_t)T) << store; + ASSERT_EQ(raw.fullDepth(0), (size_t)Z) << store; +} + // Negative: an out-of-range Z/C/T plane maps to a non-existent IFD and must throw. // dim5.ome.tif has T=2, C=3, Z=4 (24 IFDs). void test_ometiff_out_of_range_throws() diff --git a/tests/test_omezarr_mechanics.h b/tests/test_omezarr_mechanics.h index b20f1ee69..6510584aa 100644 --- a/tests/test_omezarr_mechanics.h +++ b/tests/test_omezarr_mechanics.h @@ -314,7 +314,7 @@ void test_omezarr_wholevolume_consumer(const char* store, int Z) ASSERT_TRUE(ilo.open(p, fp)) << ds.string(); LR vroi; - ASSERT_TRUE(Nyxus::scan_trivial_wholevolume(vroi, ds.string(), ilo)); + ASSERT_TRUE(Nyxus::scan_trivial_wholevolume(vroi, ds.string(), ilo, 0/*channel*/, 0/*timeframe*/)); ASSERT_EQ(vroi.raw_pixels_3D.size(), (size_t)X * Y * Z); // all voxels, not just plane 0 (48) for (const Pixel3& px : vroi.raw_pixels_3D) @@ -323,6 +323,36 @@ void test_omezarr_wholevolume_consumer(const char* store, int Z) ilo.close(); } +// End-to-end through the wired volumetric consumer, but for EVERY (channel,timeframe): +// scan_trivial_wholevolume(vroi, .., c, t) must fill the ROI's voxel cloud with the +// encoded intensity of THAT c/t plane. Before the channel/timeframe wiring the consumer +// always read (c=0,t=0), so any c>0 / t>0 plane would carry the wrong values. +void test_omezarr_wholevolume_consumer_ct(const char* store, int T, int C, int Z) +{ + const int Y = 6, X = 8; + fs::path ds = omezarr_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader ilo; + ASSERT_TRUE(ilo.open(p, fp)) << ds.string(); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + { + LR vroi; + ASSERT_TRUE(Nyxus::scan_trivial_wholevolume(vroi, ds.string(), ilo, c, t)) << store; + ASSERT_EQ(vroi.raw_pixels_3D.size(), (size_t)X * Y * Z) << store; + for (const Pixel3& px : vroi.raw_pixels_3D) + ASSERT_EQ((uint32_t)px.inten, dim5_enc((int)px.x, (int)px.y, (int)px.z, c, t)) + << store << " (c" << c << " t" << t << ") voxel (" << px.x << "," << px.y << "," << px.z << ")"; + } + ilo.close(); +} + // Whole-volume assembly through the ImageLoader facade: load_volume() must stack // all Z-planes (per (channel,timeframe)) into one X*Y*Z buffer. This is the // foundation that lets the volumetric pipeline consume plane-by-plane OME-Zarr. @@ -389,6 +419,76 @@ void test_omezarr_out_of_range_throws() EXPECT_ANY_THROW(raw.loadTileFromFile(0, 0, 99, 0, 0, 0)); } +// The loaders must ADVERTISE the real C/T extents via numberChannels() / +// fullTimestamps(). This is what the volumetric pipeline keys off to iterate +// channels and timeframes; before this both fell back to the base-class default +// of 1, which pinned the pipeline to plane (c=0, t=0) regardless of the store. +// Covers the parsed-axes path and (via dim5_noaxes) the positional fallback. +void test_omezarr_ct_counts(const char* store, int T, int C, int Z) +{ + fs::path ds = omezarr_data_path(store); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto ldr = NyxusOmeZarrLoader(1, ds.string()); + ASSERT_EQ(ldr.numberChannels(), (size_t)C) << store; + ASSERT_EQ(ldr.fullTimestamps(0), (size_t)T) << store; + ASSERT_EQ(ldr.fullDepth(0), (size_t)Z) << store; + + auto raw = RawOmezarrLoader(ds.string()); + ASSERT_EQ(raw.numberChannels(), (size_t)C) << store; + ASSERT_EQ(raw.fullTimestamps(0), (size_t)T) << store; + ASSERT_EQ(raw.fullDepth(0), (size_t)Z) << store; +} + +// Phase 5 negative: the whole-volume facade read must propagate an out-of-range channel +// or timeframe as a throw (not silently read plane 0 or OOB memory). dim5 has C=3, T=2. +void test_omezarr_load_volume_out_of_range() +{ + fs::path ds = omezarr_data_path("dim5.ome.zarr"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ds.string(); + + EXPECT_ANY_THROW(il.load_volume(99, 0)); // channel out of range (C=3) + EXPECT_ANY_THROW(il.load_volume(0, 99)); // timeframe out of range (T=2) + // in-range still works + EXPECT_TRUE(il.load_volume(2, 1)); + il.close(); +} + +// Physical calibration: the loaders must surface the OME-Zarr coordinateTransformations +// 'scale' as physicalSizeX/Y/Z and the space-axis 'unit'. dim5_calibrated encodes +// scale (t,c,z,y,x)=(1,1,2,0.5,0.5) micrometer -> physX=physY=0.5, physZ=2.0. An +// uncalibrated store (scale all 1) must report 1.0 and no unit. +void test_omezarr_physical_calibration() +{ + fs::path cal = omezarr_data_path("dim5_calibrated.ome.zarr"); + ASSERT_TRUE(fs::exists(cal)) << cal.string(); + + auto ldr = NyxusOmeZarrLoader(1, cal.string()); + ASSERT_DOUBLE_EQ(ldr.physicalSizeX(), 0.5); + ASSERT_DOUBLE_EQ(ldr.physicalSizeY(), 0.5); + ASSERT_DOUBLE_EQ(ldr.physicalSizeZ(), 2.0); + ASSERT_EQ(ldr.physicalSizeUnit(), "micrometer"); + + auto raw = RawOmezarrLoader(cal.string()); + ASSERT_DOUBLE_EQ(raw.physicalSizeX(), 0.5); + ASSERT_DOUBLE_EQ(raw.physicalSizeY(), 0.5); + ASSERT_DOUBLE_EQ(raw.physicalSizeZ(), 2.0); + ASSERT_EQ(raw.physicalSizeUnit(), "micrometer"); + + // uncalibrated store: scale all 1.0 -> physical sizes default to 1.0 + fs::path plain = omezarr_data_path("dim5.ome.zarr"); + auto ldr2 = NyxusOmeZarrLoader(1, plain.string()); + ASSERT_DOUBLE_EQ(ldr2.physicalSizeX(), 1.0); + ASSERT_DOUBLE_EQ(ldr2.physicalSizeZ(), 1.0); +} + // Illegal / adversarial: self-inconsistent metadata must be rejected cleanly // (throw), not crash. bad_axes_count declares 5 axes for a 3D array (indexing the // shape by axis role would read OOB); bad_no_xy has axes but none labeled x/y. From d4f153b3970308d03302791a6f3ee96ad71a4ad2 Mon Sep 17 00:00:00 2001 From: Demian Vladi Date: Mon, 20 Jul 2026 17:15:42 -0700 Subject: [PATCH 4/4] feat(io): stream oversized 3D volumetric ROIs out-of-core across all feature families Step 3 of the IO project: the out-of-core streaming capability. Volumetric ROIs too large to hold in RAM are now streamed rather than rejected, across every 3D feature family -- whole-volume assembly, intensity/histogram, surface, GLCM/GLDM/NGLDM/NGTDM, GLRLM, and GLSZM/GLDZM (via streaming connected-component labeling) -- plus a fix to the 2D out-of-core intensity dispatch that predates the 3D work. Includes the negative-path test coverage that caught a degenerate-ROI bug, Zarr v3 sharding validated at multi-shard-file scale, and a chunk-depth read fix. Absorbed commits (oldest to newest): 80614c4 fix(3d): oversized whole volume fails loudly instead of emitting zeros 4222cdc fix(3d): whole-volume oversized check must use the 3D footprint estimator 6bef8bd fix(io): correct 2D out-of-core intensity features (dispatch + 3 latent bugs) 696061f feat(io): stream 3D intensity/histogram out-of-core for oversized volumetric ROIs 025a658 feat(io): stream 3D surface features out-of-core for oversized volumetric ROIs 8c848aa feat(io): stream 3D GLCM/GLDM/NGLDM/NGTDM out-of-core for oversized volumetric ROIs d003cc0 feat(io): stream 3D GLRLM out-of-core; fix latent grey-LUT gaps in GLCM/GLDM 67adfff feat(io): stream 3D GLSZM/GLDZM out-of-core via streaming connected-component labeling d469e0e test(io): add negative-path tests for the 3D out-of-core stream; fix a degenerate-ROI bug they caught a141d98 feat(io): stream oversized 3D whole-volume ROIs out-of-core 12adfe5 docs(vetting): register test_ooc_mechanics.py on the 3D features it covers 0fbe34b test(omezarr): validate Zarr v3 sharding at multi-shard-FILE scale 6da2482 fix(omezarr): read the full chunk depth instead of one Z-plane --- CMakeLists.txt | 2 + src/nyx/feature_method.cpp | 7 + src/nyx/feature_method.h | 6 + src/nyx/features/3d_glcm.cpp | 7 + src/nyx/features/3d_glcm.h | 2 + src/nyx/features/3d_glcm_nontriv.cpp | 213 ++++++++- src/nyx/features/3d_gldm.cpp | 143 +++++- src/nyx/features/3d_gldzm.cpp | 187 +++++++- src/nyx/features/3d_glrlm_nontriv.cpp | 253 ++++++++++- src/nyx/features/3d_glszm.cpp | 192 +++++++- src/nyx/features/3d_intensity.cpp | 62 ++- src/nyx/features/3d_intensity.h | 3 + src/nyx/features/3d_ngldm.cpp | 101 ++++- src/nyx/features/3d_ngtdm.cpp | 225 +++++---- src/nyx/features/3d_surface.cpp | 190 +++++++- src/nyx/features/3d_surface.h | 20 +- src/nyx/features/histogram.h | 102 ++++- src/nyx/features/intensity.cpp | 32 +- src/nyx/features/intensity.h | 4 + src/nyx/features/intensity_histogram.h | 4 + src/nyx/features/streaming_ccl.h | 67 +++ src/nyx/features/voxel_cloud_nontriv.cpp | 180 ++++++++ src/nyx/features/voxel_cloud_nontriv.h | 80 ++++ src/nyx/globals.h | 15 + src/nyx/image_loader.cpp | 72 +++ src/nyx/image_loader.h | 16 + src/nyx/omezarr.h | 32 +- src/nyx/phase3.cpp | 4 +- src/nyx/phase3_3d.cpp | 187 ++++++++ src/nyx/raw_omezarr.h | 37 +- src/nyx/roi_cache.h | 4 + src/nyx/workflow_3d_segmented.cpp | 4 +- src/nyx/workflow_3d_whole.cpp | 92 +++- src/nyx/workflow_pythonapi.cpp | 5 +- .../dim5_v3_multishard.ome.zarr/0/c/0/0/0/0/0 | Bin 0 -> 3588 bytes .../dim5_v3_multishard.ome.zarr/0/c/0/0/0/0/1 | Bin 0 -> 3588 bytes .../dim5_v3_multishard.ome.zarr/0/c/0/0/0/1/0 | Bin 0 -> 3588 bytes .../dim5_v3_multishard.ome.zarr/0/c/0/0/0/1/1 | Bin 0 -> 3588 bytes .../dim5_v3_multishard.ome.zarr/0/c/0/1/0/0/0 | Bin 0 -> 3588 bytes .../dim5_v3_multishard.ome.zarr/0/c/0/1/0/0/1 | Bin 0 -> 3588 bytes .../dim5_v3_multishard.ome.zarr/0/c/0/1/0/1/0 | Bin 0 -> 3588 bytes .../dim5_v3_multishard.ome.zarr/0/c/0/1/0/1/1 | Bin 0 -> 3588 bytes .../dim5_v3_multishard.ome.zarr/0/zarr.json | 67 +++ .../dim5_v3_multishard.ome.zarr/zarr.json | 52 +++ .../dim5_v3_zchunked.ome.zarr/0/c/0/0/0/0/0 | Bin 0 -> 288 bytes .../dim5_v3_zchunked.ome.zarr/0/c/0/0/1/0/0 | Bin 0 -> 288 bytes .../dim5_v3_zchunked.ome.zarr/0/c/0/0/2/0/0 | Bin 0 -> 288 bytes .../dim5_v3_zchunked.ome.zarr/0/c/0/1/0/0/0 | 1 + .../dim5_v3_zchunked.ome.zarr/0/c/0/1/1/0/0 | Bin 0 -> 288 bytes .../dim5_v3_zchunked.ome.zarr/0/c/0/1/2/0/0 | Bin 0 -> 288 bytes .../dim5_v3_zchunked.ome.zarr/0/zarr.json | 41 ++ .../dim5_v3_zchunked.ome.zarr/zarr.json | 52 +++ tests/data/omezarr/gen_dim5.py | 82 ++++ tests/python/test_ooc_mechanics.py | 316 +++++++++++++ tests/test_all.cc | 289 ++++++++++++ tests/vetting/oracle_coverage.csv | 426 +++++++++--------- 56 files changed, 3428 insertions(+), 448 deletions(-) create mode 100644 src/nyx/features/streaming_ccl.h create mode 100644 src/nyx/features/voxel_cloud_nontriv.cpp create mode 100644 src/nyx/features/voxel_cloud_nontriv.h create mode 100644 src/nyx/phase3_3d.cpp create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/0/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/0/0/0/1 create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/0/0/1/0 create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/0/0/1/1 create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/1/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/1/0/0/1 create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/1/0/1/0 create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/1/0/1/1 create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_multishard.ome.zarr/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/0/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/0/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/0/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/0/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/1/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/2/0/0 create mode 100644 tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/zarr.json create mode 100644 tests/data/omezarr/dim5_v3_zchunked.ome.zarr/zarr.json create mode 100644 tests/python/test_ooc_mechanics.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 05cdd1182..6249c9ad2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,7 @@ set(SOURCE src/nyx/features/hexagonality_polygonality.cpp src/nyx/features/image_matrix.cpp src/nyx/features/image_matrix_nontriv.cpp + src/nyx/features/voxel_cloud_nontriv.cpp src/nyx/features/2d_geomoments.cpp src/nyx/features/2d_geomoments_basic.cpp src/nyx/features/2d_geomoments_basic_nt.cpp @@ -227,6 +228,7 @@ set(SOURCE src/nyx/phase2_25d.cpp src/nyx/phase2_3d.cpp src/nyx/phase3.cpp + src/nyx/phase3_3d.cpp src/nyx/pixel_feed.cpp src/nyx/raw_image_loader.cpp src/nyx/reduce_trivial_rois.cpp diff --git a/src/nyx/feature_method.cpp b/src/nyx/feature_method.cpp index c5740309f..2d53fe213 100644 --- a/src/nyx/feature_method.cpp +++ b/src/nyx/feature_method.cpp @@ -49,6 +49,13 @@ void FeatureMethod::osized_scan_whole_image (LR& r, const Fsettings& s, ImageLoa this->save_value (r.fvals); } +// The Dataset-aware overload defaults to the Dataset-less path, so features that don't need +// slide props are unaffected; only features that override it consume the Dataset. +void FeatureMethod::osized_scan_whole_image (LR& r, const Fsettings& s, const Dataset& ds, ImageLoader& ldr) +{ + this->osized_scan_whole_image (r, s, ldr); +} + bool FeatureMethod::provides (int fcode) const { return std::find(provided_features.begin(), provided_features.end(), (int)fcode) != provided_features.end(); diff --git a/src/nyx/feature_method.h b/src/nyx/feature_method.h index 16aac5b9b..d3c7df9bd 100644 --- a/src/nyx/feature_method.h +++ b/src/nyx/feature_method.h @@ -5,6 +5,8 @@ #include "image_loader.h" #include "features/image_matrix_nontriv.h" +class Dataset; // forward decl for the Dataset-aware osized_scan_whole_image overload + /// @brief Abstract class encapsulating basic feature functionality e.g. dependency on other features, dependency on helper objects, state (calculated or pending), etc. class FeatureMethod { @@ -26,6 +28,10 @@ class FeatureMethod //=== Oversized ROI virtual void osized_scan_whole_image (LR& roi, const Fsettings& s, ImageLoader& ldr); + // Dataset-aware out-of-core dispatch. Features that need slide props (intensity, + // intensity-histogram) override this to reach their Dataset-taking osized_calculate; the + // default forwards to the Dataset-less overload so every other feature is unaffected. + virtual void osized_scan_whole_image (LR& roi, const Fsettings& s, const Dataset& ds, ImageLoader& ldr); virtual void osized_add_online_pixel (size_t x, size_t y, uint32_t intensity) = 0; // Called each time the ROI pixel is being scanned in the raster order virtual void osized_reduce() final {}; // Get rid of this method in all derived diff --git a/src/nyx/features/3d_glcm.cpp b/src/nyx/features/3d_glcm.cpp index 08ebb27fa..b8f8912b0 100644 --- a/src/nyx/features/3d_glcm.cpp +++ b/src/nyx/features/3d_glcm.cpp @@ -179,7 +179,14 @@ void D3_GLCM_feature::save_value(std::vector>& fvals) void D3_GLCM_feature::extract_texture_features_at_angle(int dx, int dy, int dz, const SimpleCube& binned_greys, PixIntens min_val, PixIntens max_val, int n_greys, bool ibsi, double soft_nan) { calculateCoocMatAtAngle (P_matrix, dx, dy, dz, binned_greys, min_val, max_val, n_greys, ibsi); + finalize_angle (soft_nan); +} +// Computes this angle's GLCM feature values from the already-built P_matrix + sum_p + I. Split out +// of extract_texture_features_at_angle so the out-of-core path (which builds P_matrix by streaming a +// 2-plane window instead of scanning the in-RAM cube) reuses the exact same feature math. +void D3_GLCM_feature::finalize_angle (double soft_nan) +{ // Blank cooc-matrix? -- no point to use it, assign each feature value '0' and return. if (sum_p == 0) { diff --git a/src/nyx/features/3d_glcm.h b/src/nyx/features/3d_glcm.h index 82c7290ea..980b938d3 100644 --- a/src/nyx/features/3d_glcm.h +++ b/src/nyx/features/3d_glcm.h @@ -145,6 +145,8 @@ class D3_GLCM_feature : public FeatureMethod, public TextureFeature bool normalize); void extract_texture_features_at_angle (int dx, int dy, int dz, const SimpleCube & grays, PixIntens min_val, PixIntens max_val, int n_greys, bool ibsi, double soft_nan); + // Compute this angle's features from the already-built P_matrix (shared by in-core + out-of-core) + void finalize_angle (double soft_nan); void calculateCoocMatAtAngle( // out diff --git a/src/nyx/features/3d_glcm_nontriv.cpp b/src/nyx/features/3d_glcm_nontriv.cpp index a0fb45fbf..ded9857a2 100644 --- a/src/nyx/features/3d_glcm_nontriv.cpp +++ b/src/nyx/features/3d_glcm_nontriv.cpp @@ -1,11 +1,202 @@ -#include "../environment.h" -#include "3d_glcm.h" -#include "image_matrix_nontriv.h" - -using namespace Nyxus; - -void D3_GLCM_feature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&) -{ - calculate (r, s); -} - +#include +#include +#include +#include "../environment.h" +#include "3d_glcm.h" +#include "image_matrix_nontriv.h" + +using namespace Nyxus; + +// The 13 direction shifts, identical to the in-core path (3d_glcm.cpp) +namespace { + struct Sh { int dx, dy, dz; }; + const Sh SHIFTS[13] = { + {1, 1, 1}, {1, 1, 0}, {1, 1, -1}, {1, 0, 1}, {1, 0, 0}, {1, 0, -1}, + {1, -1, 1}, {1, -1, 0}, {1, -1, -1}, {0, 1, 1}, {0, 1, 0}, {0, 1, -1}, {0, 0, 1} + }; +} + +// Out-of-core 3D GLCM. Instead of scanning the in-RAM grey-binned cube, this builds the 13 +// co-occurrence matrices by streaming the disk-backed voxel cloud one Z-plane at a time through a +// sliding window of (offset+1) dense grey-binned planes (co-occurrence counts are additive and the +// 13 directions only reach +/- offset in Z). The per-direction feature math is then the shared +// finalize_angle(), so the values are identical to calculate(). +void D3_GLCM_feature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&) +{ + clear_result_buffers(); + + const size_t nvox = r.raw_voxels_NT.size(); + + // grey-binning mode, mirroring calculate(): IBSI forces the "no binning" (==0) mode + int greyInfo = STNGS_IBSI(s) ? 0 : STNGS_GLCM_GREYDEPTH(s); + bool ibsi = STNGS_IBSI(s); + double soft_nan = STNGS_NAN(s); + PixIntens mn = r.aux_min, mx = r.aux_max; + + const int W = (int) r.aabb.get_width(), + H = (int) r.aabb.get_height(), + Dz = (int) r.aabb.get_z_depth(); + const StatsInt xmin = r.aabb.get_xmin(), + ymin = r.aabb.get_ymin(), + zmin = r.aabb.get_zmin(); + + // Background cells of the in-core cube are grey-binned too (0 -> 1 for matlab, 0 otherwise); + // reproduce that so a dense plane matches a Z-slice of the binned cube exactly. + const PixIntens bg = TextureFeature::bin_pixel (0, mn, mx, greyInfo); + + // --- pass 1: the global set of grey levels (I) + matrix dimension, as in calculateCoocMatAtAngle, + // which builds I from the unique values of the WHOLE binned cube (mask + background) for the + // radiomics branch -- so a background bin (e.g. matlab binning maps raw-0 to bin 1) must be + // included too when the ROI bbox actually has background voxels. + const bool hasBackground = nvox < (size_t) r.aabb.get_width() * r.aabb.get_height() * r.aabb.get_z_depth(); + std::set uniq; + PixIntens maxbin = 0; + if (hasBackground && bg != 0) + { + uniq.insert (bg); + maxbin = bg; + } + for (size_t i = 0; i < nvox; i++) + { + Pixel3 v = r.raw_voxels_NT[i]; + PixIntens b = TextureFeature::bin_pixel (v.inten, mn, mx, greyInfo); + if (b > maxbin) maxbin = b; + if (b != 0) uniq.insert (b); + } + + I.clear(); + if (radiomics_grey_binning(greyInfo)) + I.assign (uniq.begin(), uniq.end()); // std::set is already sorted ascending + else if (matlab_grey_binning(greyInfo)) + { + int n = greyInfo; + I.resize (n); + for (int i = 0; i < n; i++) I[i] = i + 1; + } + else + { + int n = (int) maxbin; // IBSI: levels 1..max + I.resize (n); + for (int i = 0; i < n; i++) I[i] = i + 1; + } + const int Ng = (int) I.size(); + + // Nothing to featurize (empty/blank): emit 13 blank angles, matching the in-core path's + // per-direction soft-NaN pushes so the angled vectors and their averages line up. + if (Ng == 0 || nvox == 0) + { + P_matrix.allocate (1, 1); + std::fill (P_matrix.begin(), P_matrix.end(), 0.0); + for (int k = 0; k < 13; k++) + { + sum_p = 0; + finalize_angle (soft_nan); + } + return; + } + + // --- pass 2: accumulate the 13 co-occurrence matrices over a sliding Z window + std::vector> mats (13); + for (int k = 0; k < 13; k++) + { + mats[k].allocate (Ng, Ng); + std::fill (mats[k].begin(), mats[k].end(), 0.0); + } + + const int off = D3_GLCM_feature::offset; + const bool sym = D3_GLCM_feature::symmetric_glcm; + + // Count one (base, neighbor) grey-level pair into matrix M, mirroring calculateCoocMatAtAngle: + // GLCM.xy(idx(neighbor), idx(base)), plus the symmetric transpose for radiomics/ibsi/symmetric. + auto add_pair = [&](SimpleMatrix& M, PixIntens lvl_base, PixIntens lvl_nbr) + { + if (ibsi_grey_binning(greyInfo)) + if (lvl_nbr == 0 || lvl_base == 0) + return; + int a = (int) lvl_nbr, b = (int) lvl_base; + if (radiomics_grey_binning(greyInfo)) + { + if (a == 0 || b == 0) + return; + a = (int) (std::lower_bound (I.begin(), I.end(), (PixIntens) a) - I.begin()); + b = (int) (std::lower_bound (I.begin(), I.end(), (PixIntens) b) - I.begin()); + } + else { a -= 1; b -= 1; } + M.xy (a, b) += 1.0; + if (sym || radiomics_grey_binning(greyInfo) || ibsi_grey_binning(greyInfo)) + M.xy (b, a) += 1.0; + }; + + // Ring of (off+1) dense grey-binned planes, indexed by local z modulo (off+1) + const int ring = off + 1; + std::vector> planes (ring); + std::vector slab; + + for (int lz = 0; lz < Dz; lz++) + { + std::vector& cur = planes[lz % ring]; + cur.assign ((size_t) W * H, bg); + r.raw_voxels_NT.read_slab ((size_t)(zmin + lz), slab); + for (const auto& v : slab) + { + int lx = (int) v.x - (int) xmin, ly = (int) v.y - (int) ymin; + if (lx >= 0 && lx < W && ly >= 0 && ly < H) + cur[(size_t) ly * W + lx] = TextureFeature::bin_pixel (v.inten, mn, mx, greyInfo); + } + + const std::vector* farp = (lz >= off) ? &planes[(lz - off) % ring] : nullptr; + + for (int k = 0; k < 13; k++) + { + const int dx = SHIFTS[k].dx * off, dy = SHIFTS[k].dy * off, dz = SHIFTS[k].dz * off; + SimpleMatrix& M = mats[k]; + + if (dz == 0) + { + // base and neighbor both on the current plane + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + int nx = x + dx, ny = y + dy; + if (nx >= 0 && nx < W && ny >= 0 && ny < H) + add_pair (M, cur[(size_t) y * W + x], cur[(size_t) ny * W + nx]); + } + } + else if (dz < 0) + { + // base on current plane, neighbor 'off' planes below (the far plane in the ring) + if (!farp) continue; + const std::vector& far = *farp; + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + int nx = x + dx, ny = y + dy; + if (nx >= 0 && nx < W && ny >= 0 && ny < H) + add_pair (M, cur[(size_t) y * W + x], far[(size_t) ny * W + nx]); + } + } + else + { + // dz>0: base on the far plane (local z-off), neighbor on the current plane + if (!farp) continue; + const std::vector& far = *farp; + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + int nx = x + dx, ny = y + dy; + if (nx >= 0 && nx < W && ny >= 0 && ny < H) + add_pair (M, far[(size_t) y * W + x], cur[(size_t) ny * W + nx]); + } + } + } + } + + // --- per-direction feature values from the accumulated matrices (shared finalize) + for (int k = 0; k < 13; k++) + { + P_matrix = mats[k]; + sum_p = 0; + for (double a : P_matrix) sum_p += a; + finalize_angle (soft_nan); + } +} diff --git a/src/nyx/features/3d_gldm.cpp b/src/nyx/features/3d_gldm.cpp index 663b2d4fb..e8081ba8d 100644 --- a/src/nyx/features/3d_gldm.cpp +++ b/src/nyx/features/3d_gldm.cpp @@ -1,8 +1,10 @@ #include #include #include +#include #include #include +#include #include "3d_gldm.h" #include "../environment.h" @@ -246,7 +248,146 @@ void D3_GLDM_feature::osized_add_online_pixel(size_t x, size_t y, uint32_t inten void D3_GLDM_feature::osized_calculate(LR& r, const Fsettings& s, ImageLoader&) { - calculate (r, s); + // Out-of-core GLDM. Streams the disk-backed voxel cloud through a 3-plane sliding window of + // dense grey-binned planes (the 26-neighbourhood spans z-1..z+1) and fills the dependence + // matrix P directly per voxel (no O(volume) zone list). Feature math (calc_*) is shared with + // the in-core path, so values are identical. + clear_buffers(); + + // intercept blank ROIs (same guard as calculate()) + if (r.aux_min == r.aux_max) + { + fv_SDE = fv_LDE = fv_GLN = fv_DN = fv_DNN = fv_GLV = fv_DV = fv_DE = + fv_LGLE = fv_HGLE = fv_SDLGLE = fv_SDHGLE = fv_LDLGLE = fv_LDHGLE = STNGS_NAN(s); + return; + } + + int greyInfo = STNGS_IBSI(s) ? 0 : STNGS_GLDM_GREYDEPTH(s); + PixIntens mn = r.aux_min, mx = r.aux_max; + const PixIntens bg = TextureFeature::bin_pixel (0, mn, mx, greyInfo); + const PixIntens zeroI = matlab_grey_binning(greyInfo) ? 1 : 0; + + const int W = (int) r.aabb.get_width(), + H = (int) r.aabb.get_height(), + Dz = (int) r.aabb.get_z_depth(); + const StatsInt xmin = r.aabb.get_xmin(), ymin = r.aabb.get_ymin(), zmin = r.aabb.get_zmin(); + + // --- grey levels I + matrix dimension (mirrors calculate(), which builds I from the WHOLE + // binned cube incl. background -- e.g. matlab binning maps raw-0 background to bin 1, and that + // bin must appear in I too, matching D3_GLDM_feature::calculate()'s unordered_set(D.begin(),D.end())). + std::set uniq; + PixIntens maxbin = 0; + const size_t nvox = r.raw_voxels_NT.size(); + const bool hasBackground = nvox < (size_t) W * H * Dz; + if (hasBackground && bg != 0) + { + uniq.insert (bg); + maxbin = bg; + } + for (size_t i = 0; i < nvox; i++) + { + PixIntens b = TextureFeature::bin_pixel (r.raw_voxels_NT[i].inten, mn, mx, greyInfo); + if (b > maxbin) maxbin = b; + if (b != 0) uniq.insert (b); + } + I.clear(); + if (ibsi_grey_binning(greyInfo)) + { + int n = (int) maxbin; + I.resize (n); + for (int i = 0; i < n; i++) I[i] = i + 1; + } + else + I.assign (uniq.begin(), uniq.end()); // std::set already sorted + Ng = (greyInfo == 0) ? (I.empty() ? 0 : (int) *std::max_element(I.begin(), I.end())) : (int) I.size(); + + Nd = nsh + 1; + P.allocate (Nd + 1, Ng + 1); + std::fill (P.begin(), P.end(), 0); + int max_Nd = 0; + + // --- 3-plane sliding window of dense grey-binned planes (bg-initialised so background matches + // the in-core cube), indexed by local z modulo 3 + std::vector> ring (3); + int ringZ[3] = { -1, -1, -1 }; + std::vector slab; + auto load = [&](int lz) + { + if (lz < 0 || lz >= Dz) return; + int slot = lz % 3; + if (ringZ[slot] == lz) return; + std::vector& pl = ring[slot]; + pl.assign ((size_t) W * H, bg); + r.raw_voxels_NT.read_slab ((size_t)(zmin + lz), slab); + for (const auto& v : slab) + { + int lx = (int) v.x - (int) xmin, ly = (int) v.y - (int) ymin; + if (lx >= 0 && lx < W && ly >= 0 && ly < H) + pl[(size_t) ly * W + lx] = TextureFeature::bin_pixel (v.inten, mn, mx, greyInfo); + } + ringZ[slot] = lz; + }; + + const bool ibsi = STNGS_IBSI(s); + for (int c = 0; c < Dz; c++) + { + load (c - 1); load (c); load (c + 1); + const std::vector& cur = ring[c % 3]; + + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + PixIntens pi = cur[(size_t) y * W + x]; + if (pi == zeroI) + continue; + + int nd = 1; + for (int i = 0; i < nsh; i++) + { + int nz = c + shifts[i].dz, ny = y + shifts[i].dy, nx = x + shifts[i].dx; + if (nz < 0 || nz >= Dz || ny < 0 || ny >= H || nx < 0 || nx >= W) + continue; + if (pi == ring[nz % 3][(size_t) ny * W + nx]) + nd++; + } + + int row = ibsi ? (int) pi - 1 + : (int)(std::lower_bound (I.begin(), I.end(), pi) - I.begin()); + int col = nd - 1; + P.xy (col, row)++; + max_Nd = (std::max) (max_Nd, nd); + } + } + + if (greyInfo) + Nd = max_Nd; + + Nz = 0; + for (auto p : P) + Nz += p; + + if (Nz == 0) + { + fv_SDE = fv_LDE = fv_GLN = fv_DN = fv_DNN = fv_GLV = fv_DV = fv_DE = + fv_LGLE = fv_HGLE = fv_SDLGLE = fv_SDHGLE = fv_LDLGLE = fv_LDHGLE = STNGS_NAN(s); + } + else + { + fv_SDE = calc_SDE(); + fv_LDE = calc_LDE(); + fv_GLN = calc_GLN(); + fv_DN = calc_DN(); + fv_DNN = calc_DNN(); + fv_GLV = calc_GLV(); + fv_DV = calc_DV(); + fv_DE = calc_DE(); + fv_LGLE = calc_LGLE(); + fv_HGLE = calc_HGLE(); + fv_SDLGLE = calc_SDLGLE(); + fv_SDHGLE = calc_SDHGLE(); + fv_LDLGLE = calc_LDLGLE(); + fv_LDHGLE = calc_LDHGLE(); + } } void D3_GLDM_feature::save_value(std::vector>& fvals) diff --git a/src/nyx/features/3d_gldzm.cpp b/src/nyx/features/3d_gldzm.cpp index 6fda05530..34b092a40 100644 --- a/src/nyx/features/3d_gldzm.cpp +++ b/src/nyx/features/3d_gldzm.cpp @@ -1,8 +1,13 @@ +#include #include +#include #include +#include +#include #include "../environment.h" #include "3d_gldzm.h" #include "image_cube.h" +#include "streaming_ccl.h" int D3_GLDZM_feature::n_levels = 0; @@ -571,7 +576,187 @@ void D3_GLDZM_feature::save_value (std::vector> & fvals) void D3_GLDZM_feature::osized_add_online_pixel (size_t x, size_t y, uint32_t intensity) {} +// Out-of-core 3D GLDZM. prepare_GLDZM_matrix_kit()'s flood fill is 6-connectivity (face-adjacent +// only -- East/West/North/South/Up/Down, no diagonals) tracking each zone's MINIMUM per-voxel +// distance to the ROI border (zone SIZE is also tracked in-core but never actually consumed by +// calc_gldzm_matrix, so this streaming version doesn't bother). Same streaming two-pass CCL +// approach as GLSZM (docs/13-3d-ooc-glszm-gldzm-design.md), but with only 3 causal neighbours +// (West, North, straight-down-Z) since 6-connectivity halves to 3, and a MIN-combining +// LabelUnionFind instead of a size-summing one. +// +// dist2border() is 2D-only (scans left/right/up/down within the CURRENT Z-plane only to the +// nearest literal-0 voxel or the plane edge) -- a limitation/convention of this implementation, +// replicated exactly, not "fixed". It needs only the current plane's dense buffer, already +// resident in the 2-plane window, so no extra memory cost versus GLSZM. +// +// Background-skip convention is GLDZM's OWN (a THIRD distinct rule, different from both GLRLM's +// unconditional zeroI=0 and GLSZM's matlab-aware zeroI): skip a voxel ONLY when +// ibsi_grey_binning(greyInfo) is true AND its binned value is literal 0 -- for radiomics/matlab +// binning, background is never skipped and forms its own zone(s), exactly mirroring +// prepare_GLDZM_matrix_kit's `if (ibsi_grey_binning(greyInfo)) if (inten==0) continue;`. void D3_GLDZM_feature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&) { - calculate (r, s); + clear_buffers(); + + if (r.aux_min == r.aux_max) + { + f_SDE = f_LDE = f_LGLZE = f_HGLZE = f_SDLGLE = f_SDHGLE = f_LDLGLE = f_LDHGLE = + f_GLNU = f_GLNUN = f_ZDNU = f_ZDNUN = f_ZP = f_GLM = f_GLV = f_ZDM = f_ZDV = f_ZDE = f_GLE = STNGS_NAN(s); + return; + } + + // grey-binning selection mirrors prepare_GLDZM_matrix_kit exactly: no GLDZM-specific greydepth + // setting exists, so it starts from the general STNGS_NGREYS(s), can be overridden by the static + // D3_GLDZM_feature::n_levels, then forced to 0 (IBSI/no-rescale) when STNGS_IBSI(s) is set. + auto greyInfo = STNGS_NGREYS(s); + auto greyInfo_localFeature = D3_GLDZM_feature::n_levels; + if (greyInfo_localFeature != 0 && greyInfo != greyInfo_localFeature) + greyInfo = greyInfo_localFeature; + if (STNGS_IBSI(s)) + greyInfo = 0; + + PixIntens mn = r.aux_min, mx = r.aux_max; + const PixIntens bg = TextureFeature::bin_pixel (0, mn, mx, greyInfo); + const bool skipZero = ibsi_grey_binning (greyInfo); // GLDZM's own rule: only skip in ibsi mode + + const int W = (int) r.aabb.get_width(), + H = (int) r.aabb.get_height(), + Dz = (int) r.aabb.get_z_depth(); + const StatsInt xmin = r.aabb.get_xmin(), ymin = r.aabb.get_ymin(), zmin = r.aabb.get_zmin(); + const size_t nvox = r.raw_voxels_NT.size(); + const bool hasBackground = nvox < (size_t) W * H * Dz; + + // --- grey levels (mirrors prepare_GLDZM_matrix_kit: unique values of the WHOLE binned cube, + // incl. background, minus literal 0 -- or a dense 1..max linspace for ibsi_grey_binning) + std::vector I; + if (ibsi_grey_binning (greyInfo)) + { + PixIntens maxbin = 0; + for (size_t i = 0; i < nvox; i++) + { + PixIntens b = TextureFeature::bin_pixel (r.raw_voxels_NT[i].inten, mn, mx, greyInfo); + if (b > maxbin) maxbin = b; + } + I.resize (maxbin); + for (int i = 0; i < (int) maxbin; i++) I[i] = i + 1; + } + else + { + std::set uniq; + if (hasBackground && bg != 0) + uniq.insert (bg); + for (size_t i = 0; i < nvox; i++) + { + PixIntens b = TextureFeature::bin_pixel (r.raw_voxels_NT[i].inten, mn, mx, greyInfo); + if (b != 0) uniq.insert (b); + } + I.assign (uniq.begin(), uniq.end()); + } + + // --- streaming 6-connectivity CCL over a 2-plane window, tracking min distance-to-border + LabelUnionFind uf; + std::vector prevPlane, curPlane; + std::vector prevLabel, curLabel; + std::vector slab; + + // 2D-only distance to the nearest literal-0 voxel or the plane edge, within 'plane' -- matches + // dist2border() exactly (1-based, min of 4 directions, clamped to >=1). + auto dist2border_plane = [&](const std::vector& plane, int x, int y) -> long long + { + int dist2l = 0; + for (int x0 = x - 1; x0 >= 0; x0--) + if (plane[(size_t) y * W + x0] == 0 || x0 == 0) { dist2l = x - x0; break; } + int dist2r = 0; + for (int x0 = x + 1; x0 < W; x0++) + if (plane[(size_t) y * W + x0] == 0 || x0 == W - 1) { dist2r = x0 - x; break; } + int dist2t = 0; + for (int y0 = y - 1; y0 >= 0; y0--) + if (plane[(size_t) y0 * W + x] == 0 || y0 == 0) { dist2t = y - y0; break; } + int dist2b = 0; + for (int y0 = y + 1; y0 < H; y0++) + if (plane[(size_t) y0 * W + x] == 0 || y0 == H - 1) { dist2b = y0 - y; break; } + dist2l++; dist2r++; dist2t++; dist2b++; + long long d = (std::min) ((std::min) (dist2l, dist2r), (std::min) (dist2t, dist2b)); + if (d == 0) d = 1; + return d; + }; + + for (int z = 0; z < Dz; z++) + { + curPlane.assign ((size_t) W * H, bg); + r.raw_voxels_NT.read_slab ((size_t)(zmin + z), slab); + for (const auto& v : slab) + { + int lx = (int) v.x - (int) xmin, ly = (int) v.y - (int) ymin; + if (lx >= 0 && lx < W && ly >= 0 && ly < H) + curPlane[(size_t) ly * W + lx] = TextureFeature::bin_pixel (v.inten, mn, mx, greyInfo); + } + curLabel.assign ((size_t) W * H, -1); + + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + PixIntens pi = curPlane[(size_t) y * W + x]; + if (skipZero && pi == 0) + continue; + + long long d = dist2border_plane (curPlane, x, y); + + int found = -1; + if (x > 0 && curPlane[(size_t) y * W + (x - 1)] == pi) + { + int lbl = curLabel[(size_t) y * W + (x - 1)]; + if (lbl >= 0) found = lbl; + } + if (y > 0 && curPlane[(size_t) (y - 1) * W + x] == pi) + { + int lbl = curLabel[(size_t) (y - 1) * W + x]; + if (lbl >= 0) found = (found < 0) ? lbl : uf.union_min (found, lbl); + } + if (z > 0 && prevPlane[(size_t) y * W + x] == pi) + { + int lbl = prevLabel[(size_t) y * W + x]; + if (lbl >= 0) found = (found < 0) ? lbl : uf.union_min (found, lbl); + } + + if (found < 0) + found = uf.make_label (pi, d); + else + uf.update_min (found, d); + curLabel[(size_t) y * W + x] = found; + } + + prevPlane.swap (curPlane); + prevLabel.swap (curLabel); + } + + // --- one zone per distinct root: (intensity, min distance) + std::vector> Zones; + std::vector seenRoot (uf.num_labels(), 0); + for (size_t i = 0; i < uf.num_labels(); i++) + { + int rt = uf.find ((int) i); + if (seenRoot[rt]) continue; + seenRoot[rt] = 1; + Zones.push_back ({ uf.intensity_of (rt), (int) uf.metric (rt) }); + } + + int Ng = (int) I.size(); + int Nd = 0; + for (auto& zo : Zones) + Nd = (std::max) (Nd, zo.second); + + SimpleMatrix GLDZM; + GLDZM.allocate (Nd, Ng); // Ng rows, Nd columns -- matches calc_gldzm_matrix's layout + GLDZM.fill (0); + for (auto& zo : Zones) + { + int row = (int)(std::lower_bound (I.begin(), I.end(), zo.first) - I.begin()); + int col = zo.second - 1; + GLDZM.yx (row, col)++; + } + + std::vector Mx, Md; + calc_row_and_column_sum_vectors (Mx, Md, GLDZM, Ng, Nd, I); + calc_features (Mx, Md, GLDZM, I, r.aux_area); } diff --git a/src/nyx/features/3d_glrlm_nontriv.cpp b/src/nyx/features/3d_glrlm_nontriv.cpp index b4191d053..bc5e48a00 100644 --- a/src/nyx/features/3d_glrlm_nontriv.cpp +++ b/src/nyx/features/3d_glrlm_nontriv.cpp @@ -1,11 +1,242 @@ -#include "../environment.h" -#include "3d_glrlm.h" -#include "image_matrix_nontriv.h" - -using namespace Nyxus; - -void D3_GLRLM_feature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&) -{ - calculate (r, s); -} - +#include +#include +#include +#include +#include "../environment.h" +#include "3d_glrlm.h" +#include "image_matrix_nontriv.h" + +using namespace Nyxus; + +// Same 13 canonical direction shifts as 3d_glrlm.cpp (dz in {0,1} only: half of the 26-connected +// directions, avoiding double-counting an axis and its opposite). AngleShift order is {dz,dy,dx}. +static const AngleShift SHIFTS13[] = +{ + {1, 1, 1}, {1, 1, 0}, {1, 1, -1}, {1, 0, 1}, {1, 0, 0}, {1, 0, -1}, + {1, -1, 1}, {1, -1, 0}, {1, -1, -1}, {0, 1, 1}, {0, 1, 0}, {0, 1, -1}, {0, 0, 1} +}; + +// Out-of-core 3D GLRLM. gather_rl_zones() greedily walks each run forward along its direction, +// consuming voxels via an in-RAM VISITED marker -- workable in-core (whole cube available) but not +// as-is out-of-core. Two cases, driven by the fact all 13 canonical directions have dz in {0,1}: +// +// - dz==0 (3 directions): the run never leaves its Z-plane, so gather_rl_zones() is called +// UNCHANGED on a depth-1 SimpleCube built from one streamed dense plane -- byte-identical to the +// in-core algorithm, just fed one plane at a time. +// - dz==1 (10 directions): a run advances exactly one (dz,dy,dx) step per Z-plane, so it visits at +// most one voxel per plane. This lets a "carry" array (indexed by the CURRENT plane's (y,x)) +// track each in-progress run's length across a 2-plane window, finalizing (recording into the +// grey/length histogram) a run the moment its continuation check fails, instead of needing the +// whole volume. Peak memory is O(plane area), not O(volume). +// +// Grey-level LUT and histogram-fill both reuse calc_SRE()/etc unchanged once a full SimpleMatrix +// is assembled per direction, so values are identical to calculate(). +void D3_GLRLM_feature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&) +{ + n_angles_ = (int) (sizeof(SHIFTS13) / sizeof(AngleShift)); + clear_buffers(); + + PixIntens minI = r.aux_min, maxI = r.aux_max; + if (minI == maxI) + { + double w = STNGS_NAN(s); + angled_SRE.assign (n_angles_, w); angled_LRE.assign (n_angles_, w); + angled_GLN.assign (n_angles_, w); angled_GLNN.assign (n_angles_, w); + angled_RLN.assign (n_angles_, w); angled_RLNN.assign (n_angles_, w); + angled_RP.assign (n_angles_, w); angled_GLV.assign (n_angles_, w); + angled_RV.assign (n_angles_, w); angled_RE.assign (n_angles_, w); + angled_LGLRE.assign (n_angles_, w); angled_HGLRE.assign (n_angles_, w); + angled_SRLGLE.assign (n_angles_, w); angled_SRHGLE.assign (n_angles_, w); + angled_LRLGLE.assign (n_angles_, w); angled_LRHGLE.assign (n_angles_, w); + return; + } + + int greyInfo = STNGS_IBSI(s) ? 0 : STNGS_GLRLM_GREYDEPTH(s); + const bool ibsi = STNGS_IBSI(s); + const PixIntens bg = TextureFeature::bin_pixel (0, minI, maxI, greyInfo); + + const int W = (int) r.aabb.get_width(), + H = (int) r.aabb.get_height(), + Dz = (int) r.aabb.get_z_depth(); + const StatsInt xmin = r.aabb.get_xmin(), ymin = r.aabb.get_ymin(), zmin = r.aabb.get_zmin(); + const size_t nvox = r.raw_voxels_NT.size(); + const bool hasBackground = nvox < (size_t) W * H * Dz; + + // --- grey levels I (mirrors calculate(): unique values of the WHOLE binned cube, incl. + // background, minus 0 -- for IBSI a 1..max linspace instead). Ng == I.size() in both cases. + // NOTE: the in-core path's I-construction branches on ibsi_grey_binning(greyInfo) (the numeric + // "no rescale" binning mode), NOT the STNGS_IBSI(s) compliance flag used below for row lookup -- + // under default settings greyInfo==0 (ibsi_grey_binning true) while STNGS_IBSI(s) is false, so + // conflating the two picks the wrong (sparse unique-set) branch here. + std::vector I; + if (ibsi_grey_binning (greyInfo)) + { + PixIntens maxbin = 0; + for (size_t i = 0; i < nvox; i++) + { + PixIntens b = TextureFeature::bin_pixel (r.raw_voxels_NT[i].inten, minI, maxI, greyInfo); + if (b > maxbin) maxbin = b; + } + I.resize (maxbin); + for (int i = 0; i < (int) maxbin; i++) I[i] = i + 1; + } + else + { + std::set uniq; + if (hasBackground && bg != 0) + uniq.insert (bg); + for (size_t i = 0; i < nvox; i++) + { + PixIntens b = TextureFeature::bin_pixel (r.raw_voxels_NT[i].inten, minI, maxI, greyInfo); + if (b != 0) uniq.insert (b); + } + I.assign (uniq.begin(), uniq.end()); + } + const int Ng = (int) I.size(); + const size_t Np = nvox; // matches r.raw_pixels_3D.size() in the in-core path + + auto row_of = [&](PixIntens pi) -> int + { + return ibsi ? (int) pi - 1 : (int)(std::lower_bound (I.begin(), I.end(), pi) - I.begin()); + }; + + // --- stream one direction at a time + for (const AngleShift& ash : SHIFTS13) + { + std::vector> counts (Ng > 0 ? Ng : 1); // counts[row][length-1] = run count + + auto finalize = [&](PixIntens pi, int length) + { + if (length <= 0 || Ng == 0) return; + int row = row_of (pi); + if (row < 0 || row >= Ng) return; + if ((int) counts[row].size() < length) + counts[row].resize (length, 0); + counts[row][length - 1]++; + }; + + if (ash.dz == 0) + { + // In-plane run: reuse the in-core algorithm verbatim on a depth-1 sub-cube per plane. + std::vector plane; + std::vector slab; + for (int z = 0; z < Dz; z++) + { + plane.assign ((size_t) W * H, bg); + r.raw_voxels_NT.read_slab ((size_t)(zmin + z), slab); + for (const auto& v : slab) + { + int lx = (int) v.x - (int) xmin, ly = (int) v.y - (int) ymin; + if (lx >= 0 && lx < W && ly >= 0 && ly < H) + plane[(size_t) ly * W + lx] = TextureFeature::bin_pixel (v.inten, minI, maxI, greyInfo); + } + + SimpleCube D1 (plane, W, H, 1); + std::vector> zones; + D3_GLRLM_feature::gather_rl_zones (zones, ash, D1, /*zeroI=*/ 0); + for (auto& zo : zones) + finalize (zo.first, zo.second); + } + } + else + { + // Cross-plane run (dz==1): carry each in-progress run's length across a 2-plane window, + // indexed by the CURRENT plane's (y,x). A run at (y,x) in plane z has its predecessor at + // (y-dy, x-dx) in plane z-1; if intensities match, the run continues (length+1), else the + // PREDECESSOR's run just ended and is finalized. Whatever remains unconsumed after the + // last plane is finalized once more after the loop. + std::vector prevPlane, curPlane; + std::vector prevCarry, curCarry; + std::vector slab; + + for (int z = 0; z < Dz; z++) + { + curPlane.assign ((size_t) W * H, bg); + r.raw_voxels_NT.read_slab ((size_t)(zmin + z), slab); + for (const auto& v : slab) + { + int lx = (int) v.x - (int) xmin, ly = (int) v.y - (int) ymin; + if (lx >= 0 && lx < W && ly >= 0 && ly < H) + curPlane[(size_t) ly * W + lx] = TextureFeature::bin_pixel (v.inten, minI, maxI, greyInfo); + } + curCarry.assign ((size_t) W * H, 0); + + std::vector consumedPrev; + if (z > 0) + consumedPrev.assign ((size_t) W * H, 0); + + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + PixIntens pi = curPlane[(size_t) y * W + x]; + if (pi == 0) // background/skip: matches gather_rl_zones's zeroI==0 check + continue; + + int py = y - ash.dy, px = x - ash.dx; + int length = 1; + if (z > 0 && py >= 0 && py < H && px >= 0 && px < W + && prevPlane[(size_t) py * W + px] == pi) + { + length = prevCarry[(size_t) py * W + px] + 1; + consumedPrev[(size_t) py * W + px] = 1; + } + curCarry[(size_t) y * W + x] = length; + } + + if (z > 0) + { + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + size_t idx = (size_t) y * W + x; + if (prevCarry[idx] > 0 && !consumedPrev[idx]) + finalize (prevPlane[idx], prevCarry[idx]); + } + } + + prevPlane.swap (curPlane); + prevCarry.swap (curCarry); + } + // Finalize whatever is still active after the last plane + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + size_t idx = (size_t) y * W + x; + if (prevCarry[idx] > 0) + finalize (prevPlane[idx], prevCarry[idx]); + } + } + + // --- assemble the direction's matrix from counts and reuse the shared feature math + int Nr = 0; + for (auto& row : counts) + Nr = (std::max) (Nr, (int) row.size()); + + P_matrix P; + P.allocate (Nr, Ng); + std::fill (P.begin(), P.end(), 0); + for (int row = 0; row < Ng; row++) + for (int col = 0; col < (int) counts[row].size(); col++) + P.xy (col, row) = counts[row][col]; + + double sum = 0; + for (auto p : P) sum += p; + + angled_SRE.push_back (calc_SRE (P, sum)); + angled_LRE.push_back (calc_LRE (P, sum)); + angled_GLN.push_back (calc_GLN (P, sum)); + angled_GLNN.push_back (calc_GLNN (P, sum)); + angled_RLN.push_back (calc_RLN (P, sum)); + angled_RLNN.push_back (calc_RLNN (P, sum)); + angled_RP.push_back (Np > 0 ? sum / double(Np) : STNGS_NAN(s)); + angled_GLV.push_back (calc_GLV (P, I, sum)); + angled_RV.push_back (calc_RV (P, sum)); + angled_RE.push_back (calc_RE (P, sum)); + angled_LGLRE.push_back (calc_LGLRE (P, I, sum)); + angled_HGLRE.push_back (calc_HGLRE (P, I, sum)); + angled_SRLGLE.push_back (calc_SRLGLE (P, I, sum)); + angled_SRHGLE.push_back (calc_SRHGLE (P, I, sum)); + angled_LRLGLE.push_back (calc_LRLGLE (P, I, sum)); + angled_LRHGLE.push_back (calc_LRHGLE (P, I, sum)); + } +} diff --git a/src/nyx/features/3d_glszm.cpp b/src/nyx/features/3d_glszm.cpp index 3b740e2b9..69f43e197 100644 --- a/src/nyx/features/3d_glszm.cpp +++ b/src/nyx/features/3d_glszm.cpp @@ -1,9 +1,11 @@ #include #include #include +#include #include #include #include "3d_glszm.h" +#include "streaming_ccl.h" #include "../environment.h" #include "../helpers/timing.h" @@ -36,9 +38,197 @@ D3_GLSZM_feature::D3_GLSZM_feature() : FeatureMethod("D3_GLSZM_feature") void D3_GLSZM_feature::osized_add_online_pixel(size_t x, size_t y, uint32_t intensity) {} // Not supporting +// Out-of-core 3D GLSZM. gather_size_zones() is a full 26-connectivity flood fill that greedily +// walks a zone (with explicit backtracking via 'history') and marks voxels VISITED -- workable +// in-core but assumes the whole cube is resident, since a zone can wander in any direction and +// therefore its final size isn't known until the whole volume has been explored. +// +// This streams a standard two-pass ("raster scan") connected-component labeling: process Z-planes +// in ascending order, and within each plane voxels in raster (row,col) order, checking only +// CAUSAL neighbours -- those already fully labeled by the scan order. For 26-connectivity that is +// 4 same-plane neighbours (West, North, North-West, North-East) plus all 9 neighbours in the +// previous (fully resolved) plane -- 13 of the 26, exactly the causal half. The symmetric "future" +// neighbours are not missed: they get discovered from the OTHER voxel's perspective once the scan +// reaches it. A growable union-find (LabelUnionFind, streaming_ccl.h) merges labels that turn out +// to belong to the same zone and accumulates each zone's voxel count (summed on union), so the +// final size is known the instant the volume finishes streaming -- no second voxel pass needed. +// Peak memory is a 2-plane label window (O(area)) plus the label table (bounded by zone count, +// worst case O(volume) for pathological data -- the same class of bound already accepted for +// GLRLM's per-row run-length histograms). void D3_GLSZM_feature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&) { - calculate (r, s); + clear_buffers(); + + if (r.aux_min == r.aux_max) + { + invalidate (STNGS_NAN(s)); + return; + } + + int greyInfo = STNGS_GLSZM_GREYDEPTH(s); + if (STNGS_IBSI(s)) greyInfo = 0; + PixIntens mn = r.aux_min, mx = r.aux_max; + const PixIntens bg = TextureFeature::bin_pixel (0, mn, mx, greyInfo); + // Background/skip value: matlab-aware, matching THIS feature's own convention (calculate()'s + // zeroI) -- NOT GLRLM's hardcoded 0, which is a different feature's own quirk. + const PixIntens zeroI = matlab_grey_binning (greyInfo) ? 1 : 0; + + const int W = (int) r.aabb.get_width(), + H = (int) r.aabb.get_height(), + Dz = (int) r.aabb.get_z_depth(); + const StatsInt xmin = r.aabb.get_xmin(), ymin = r.aabb.get_ymin(), zmin = r.aabb.get_zmin(); + const size_t nvox = r.raw_voxels_NT.size(); + const bool hasBackground = nvox < (size_t) W * H * Dz; + + // --- grey levels I (mirrors calculate(): ibsi_grey_binning(greyInfo) numeric check for + // construction -- NOT the STNGS_IBSI(s) compliance flag, which calculate() reserves for row + // lookup below; conflating the two was the exact bug found and fixed in GLRLM). + I.clear(); + if (ibsi_grey_binning (greyInfo)) + { + PixIntens maxbin = 0; + for (size_t i = 0; i < nvox; i++) + { + PixIntens b = TextureFeature::bin_pixel (r.raw_voxels_NT[i].inten, mn, mx, greyInfo); + if (b > maxbin) maxbin = b; + } + I.resize (maxbin); + for (int i = 0; i < (int) maxbin; i++) I[i] = i + 1; + } + else + { + std::set uniq; + if (hasBackground && bg != 0) + uniq.insert (bg); + for (size_t i = 0; i < nvox; i++) + { + PixIntens b = TextureFeature::bin_pixel (r.raw_voxels_NT[i].inten, mn, mx, greyInfo); + if (b != 0) uniq.insert (b); + } + I.assign (uniq.begin(), uniq.end()); + } + const bool ibsiRow = STNGS_IBSI(s); + auto row_of = [&](PixIntens pi) -> int + { + return ibsiRow ? (int) pi - 1 : (int)(std::lower_bound (I.begin(), I.end(), pi) - I.begin()); + }; + + // --- streaming 26-connectivity CCL over a 2-plane window + LabelUnionFind uf; + std::vector prevPlane, curPlane; + std::vector prevLabel, curLabel; + std::vector slab; + + static const int SAME_PLANE_CAUSAL[4][2] = { {0,-1}, {-1,0}, {-1,-1}, {-1,1} }; // dy,dx + + for (int z = 0; z < Dz; z++) + { + curPlane.assign ((size_t) W * H, bg); + r.raw_voxels_NT.read_slab ((size_t)(zmin + z), slab); + for (const auto& v : slab) + { + int lx = (int) v.x - (int) xmin, ly = (int) v.y - (int) ymin; + if (lx >= 0 && lx < W && ly >= 0 && ly < H) + curPlane[(size_t) ly * W + lx] = TextureFeature::bin_pixel (v.inten, mn, mx, greyInfo); + } + curLabel.assign ((size_t) W * H, -1); + + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + PixIntens pi = curPlane[(size_t) y * W + x]; + if (pi == zeroI) + continue; + + int found = -1; + for (auto& d : SAME_PLANE_CAUSAL) + { + int ny = y + d[0], nx = x + d[1]; + if (ny < 0 || ny >= H || nx < 0 || nx >= W) continue; + if (curPlane[(size_t) ny * W + nx] != pi) continue; + int lbl = curLabel[(size_t) ny * W + nx]; + if (lbl < 0) continue; + found = (found < 0) ? lbl : uf.union_sum (found, lbl); + } + if (z > 0) + { + for (int dy = -1; dy <= 1; dy++) + for (int dx = -1; dx <= 1; dx++) + { + int ny = y + dy, nx = x + dx; + if (ny < 0 || ny >= H || nx < 0 || nx >= W) continue; + if (prevPlane[(size_t) ny * W + nx] != pi) continue; + int lbl = prevLabel[(size_t) ny * W + nx]; + if (lbl < 0) continue; + found = (found < 0) ? lbl : uf.union_sum (found, lbl); + } + } + + if (found < 0) + found = uf.make_label (pi, 1); + else + uf.add_sum (found, 1); + curLabel[(size_t) y * W + x] = found; + } + + prevPlane.swap (curPlane); + prevLabel.swap (curLabel); + } + + // --- one zone per distinct root + std::vector> Zones; + std::vector seenRoot (uf.num_labels(), 0); + for (size_t i = 0; i < uf.num_labels(); i++) + { + int rt = uf.find ((int) i); + if (seenRoot[rt]) continue; + seenRoot[rt] = 1; + Zones.push_back ({ uf.intensity_of (rt), (int) uf.metric (rt) }); + } + + int maxZoneArea = 0; + for (auto& zo : Zones) + maxZoneArea = (std::max) (maxZoneArea, zo.second); + + Ng = (int) I.size(); + Ns = maxZoneArea; + Nz = (int) Zones.size(); + Np = (int) nvox; + + P.allocate (Ns, Ng); + P.fill (0); + for (auto& zo : Zones) + { + int row = row_of (zo.first); + int col = zo.second - 1; + P.xy (col, row)++; + } + + sum_p = 0; + for (auto a : P) sum_p += a; + if (sum_p == 0) + { + invalidate (STNGS_NAN(s)); + return; + } + + calc_sums_of_P(); + fv_SAE = calc_SAE(); + fv_LAE = calc_LAE(); + fv_GLN = calc_GLN(); + fv_GLNN = calc_GLNN(); + fv_SZN = calc_SZN(); + fv_SZNN = calc_SZNN(); + fv_ZP = calc_ZP(); + fv_GLV = calc_GLV(); + fv_ZV = calc_ZV(); + fv_ZE = calc_ZE(); + fv_LGLZE = calc_LGLZE(); + fv_HGLZE = calc_HGLZE(); + fv_SALGLE = calc_SALGLE(); + fv_SAHGLE = calc_SAHGLE(); + fv_LALGLE = calc_LALGLE(); + fv_LAHGLE = calc_LAHGLE(); } /*static*/ void D3_GLSZM_feature::gather_size_zones (std::vector> & Zones, SimpleCube & D, PixIntens zeroI) diff --git a/src/nyx/features/3d_intensity.cpp b/src/nyx/features/3d_intensity.cpp index 21d8b4d2b..e50e78419 100644 --- a/src/nyx/features/3d_intensity.cpp +++ b/src/nyx/features/3d_intensity.cpp @@ -209,14 +209,19 @@ void D3_VoxelIntensityFeatures::osized_calculate (LR & r, const Fsettings & s, c double energy = 0.0; double cen_x = 0.0, cen_y = 0.0, + cen_z = 0.0, integInten = 0.0; - for (size_t i = 0; i < r.raw_pixels_NT.size(); i++) + // Stream voxels off disk (raw_voxels_NT) rather than holding the cube (raw_pixels_3D) + // or reading the 2D z-less cloud (raw_pixels_NT). Values are unchanged - intensity + // statistics are per-voxel scalars, so a linear pass over the disk-backed cloud suffices. + for (size_t i = 0; i < r.raw_voxels_NT.size(); i++) { - Pixel2 px = r.raw_pixels_NT[i]; + Pixel3 px = r.raw_voxels_NT[i]; mean_ += px.inten; energy += px.inten * px.inten; cen_x += px.x; cen_y += px.y; + cen_z += px.z; integInten += px.inten; } mean_ /= n; @@ -228,9 +233,9 @@ void D3_VoxelIntensityFeatures::osized_calculate (LR & r, const Fsettings & s, c // --MAD, VARIANCE, STDDEV, COV double mad = 0.0, var = 0.0; - for (auto& px : r.raw_pixels_3D) + for (size_t i = 0; i < r.raw_voxels_NT.size(); i++) { - double diff = px.inten - mean_; + double diff = r.raw_voxels_NT[i].inten - mean_; mad += std::abs(diff); var += diff * diff; } @@ -251,7 +256,7 @@ void D3_VoxelIntensityFeatures::osized_calculate (LR & r, const Fsettings & s, c // P10, 25, 75, 90, IQR, QCOD, RMAD, entropy, uniformity int n_greybins = STNGS_NGREYS(s); TrivialHistogram H; - H.initialize (n_greybins, r.aux_min, r.aux_max, r.raw_pixels_NT); + H.initialize (n_greybins, r.aux_min, r.aux_max, r.raw_voxels_NT); auto [median_, mode_, p01_, p10_, p25_, p75_, p90_, p99_, iqr_, rmad_, entropy_, uniformity_] = H.get_stats(); val_MEDIAN = median_; val_P01 = p01_; @@ -269,20 +274,23 @@ void D3_VoxelIntensityFeatures::osized_calculate (LR & r, const Fsettings & s, c // Median absolute deviation double medad = 0.0; - for (auto& px : r.raw_pixels_3D) - medad += std::abs(px.inten - median_); + for (size_t i = 0; i < r.raw_voxels_NT.size(); i++) + medad += std::abs(double(r.raw_voxels_NT[i].inten) - median_); val_MEDIAN_ABSOLUTE_DEVIATION = medad / n; // FIX 3ROBUST_MEAN: same omission in the out-of-core path (defaulted to 0) -> compute the // mean of voxels within the [P10,P90] robust window, matching the 2D implementation. double robustMean = 0.0; size_t robustCount = 0; - for (auto& px : r.raw_pixels_3D) - if (px.inten >= p10_ && px.inten <= p90_) + for (size_t i = 0; i < r.raw_voxels_NT.size(); i++) + { + double inten = r.raw_voxels_NT[i].inten; + if (inten >= p10_ && inten <= p90_) { - robustMean += px.inten; + robustMean += inten; robustCount++; } + } val_ROBUST_MEAN = robustCount ? robustMean / double(robustCount) : 0.0; // --Uniformity calculated as PIU, percent image uniformity - see "A comparison of five standard methods for evaluating image @@ -293,25 +301,35 @@ void D3_VoxelIntensityFeatures::osized_calculate (LR & r, const Fsettings & s, c // Skewness Moments4 mom; - for (size_t i = 0; i < r.raw_pixels_NT.size(); i++) - { - Pixel2 px = r.raw_pixels_NT[i]; - mom.add(px.inten); - } + for (size_t i = 0; i < r.raw_voxels_NT.size(); i++) + mom.add(r.raw_voxels_NT[i].inten); val_SKEWNESS = mom.skewness(); // Kurtosis val_KURTOSIS = mom.kurtosis(); - // Excess kurtosis - val_EXCESS_KURTOSIS = val_KURTOSIS - 3; - - // Hyperskewness hs = E[x-mean].^5 / std(x).^5 - val_HYPERSKEWNESS = mom.hyperskewness(); + // Excess kurtosis. NOTE: this is NOT val_KURTOSIS-3 -- Moments4::excess_kurtosis() has its own + // independent zero-variance (M2==0) guard returning exactly 0.0, whereas kurtosis() ALSO + // returns exactly 0.0 under that same guard; subtracting 3 from the latter wrongly yields -3.0 + // for a degenerate (constant-intensity) ROI instead of 0.0. Only diverges from kurtosis()-3 in + // that degenerate case (for n>4 the two formulas are algebraically identical), which is why + // every non-degenerate fixture matched before this was caught by a blank-ROI OOC test. + val_EXCESS_KURTOSIS = mom.excess_kurtosis(); - // Hyperflatness hf = E[x-mean].^6 / std(x).^6 - val_HYPERFLATNESS = mom.hyperflatness(); + // Hyperskewness / hyperflatness: use the explicit sum-of-powers definition of the in-core + // calculate() (mom.hyperskewness()/hyperflatness() use a different definition and diverge). + double sumPow5 = 0, sumPow6 = 0; + for (size_t i = 0; i < r.raw_voxels_NT.size(); i++) + { + double diff = double(r.raw_voxels_NT[i].inten) - mean_; + sumPow5 += std::pow(diff, 5.); + sumPow6 += std::pow(diff, 6.); + } + double denomHS = (n * std::pow(val_STANDARD_DEVIATION, 5.)); + val_HYPERSKEWNESS = denomHS == 0. ? 0. : sumPow5 / denomHS; + double denomHF = (n * std::pow(val_STANDARD_DEVIATION, 6.)); + val_HYPERFLATNESS = denomHF == 0. ? 0. : sumPow6 / denomHF; } void D3_VoxelIntensityFeatures::save_value(std::vector>& fvals) diff --git a/src/nyx/features/3d_intensity.h b/src/nyx/features/3d_intensity.h index 98e2d259d..d12891b46 100644 --- a/src/nyx/features/3d_intensity.h +++ b/src/nyx/features/3d_intensity.h @@ -16,6 +16,9 @@ class D3_VoxelIntensityFeatures: public FeatureMethod void osized_add_online_pixel(size_t x, size_t y, uint32_t intensity); void osized_calculate(LR& r, const Fsettings& s, ImageLoader& ldr) { throw std::runtime_error("illegal call of D3_VoxelIntensityFeatures::osized_calculate(LR&, const Fsettings&, ImageLoader&)"); } void osized_calculate(LR& r, const Fsettings& s, const Dataset& ds, ImageLoader& ldr); + // The Dataset-less osized_calculate is a throwing guard, so the out-of-core dispatch must + // reach the Dataset-aware overload here (COVERED_IMAGE_INTENSITY_RANGE needs slide props). + void osized_scan_whole_image (LR& roi, const Fsettings& s, const Dataset& ds, ImageLoader& ldr) override { osized_calculate (roi, s, ds, ldr); save_value (roi.fvals); } void save_value(std::vector>& feature_vals); static void reduce (size_t start, size_t end, std::vector* ptrLabels, std::unordered_map * ptrLabelData, const Fsettings & s, const Dataset & ds); // FIX: extract() must carry the Dataset, because calculate() needs it for diff --git a/src/nyx/features/3d_ngldm.cpp b/src/nyx/features/3d_ngldm.cpp index 52532f0d5..b82f18db0 100644 --- a/src/nyx/features/3d_ngldm.cpp +++ b/src/nyx/features/3d_ngldm.cpp @@ -1,6 +1,10 @@ #include +#include +#include +#include #include "3d_ngldm.h" #include "../environment.h" +#include "../helpers/helpers.h" using namespace Nyxus; @@ -405,5 +409,100 @@ void D3_NGLDM_feature::osized_add_online_pixel(size_t x, size_t y, uint32_t inte void D3_NGLDM_feature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&) { - calculate (r, s); + // Out-of-core NGLDM. Streams the disk-backed voxel cloud through a 3-plane sliding window of + // dense grey-binned planes and fills the NGLD-matrix directly. Binning, interior-only scan, + // 24-neighbourhood and background handling mirror calc_ngld_matrix exactly; the totals and + // feature math (calc_rowwise_and_columnwise_totals / calc_features) are shared. + clear_buffers(); + + if (r.aux_min == r.aux_max) + { + f_LDE = f_HDE = f_LGLCE = f_HGLCE = f_LDLGLE = f_LDHGLE = f_HDLGLE = f_HDHGLE = + f_GLNU = f_GLNUN = f_DCNU = f_DCNUN = f_GLCM = f_GLV = f_DCM = f_DCP = f_DCV = + f_DCENT = f_DCENE = STNGS_NAN(s); + return; + } + + const int nGrays = STNGS_NGREYS(s); + const bool ibsi = STNGS_IBSI(s); + const PixIntens range = r.aux_max; // binning basis is [0, aux_max], as in calc_ngld_matrix + + const int W = (int) r.aabb.get_width(), + H = (int) r.aabb.get_height(), + Dz = (int) r.aabb.get_z_depth(); + const StatsInt xmin = r.aabb.get_xmin(), ymin = r.aabb.get_ymin(), zmin = r.aabb.get_zmin(); + + // Background grey level of the dense cube (raw 0 binned). Present in the LUT only if the ROI + // bbox actually contains a non-mask (background) voxel, matching unique(aux_image_cube). + const size_t nvox = r.raw_voxels_NT.size(); + const bool hasBackground = nvox < (size_t) W * H * Dz; + const PixIntens bg = Nyxus::to_grayscale (0, 0, range, nGrays, ibsi); + + // --- grey-level LUT: unique binned values over the whole cube (mask voxels + maybe background) + std::set uniq; + if (hasBackground) + uniq.insert (bg); + for (size_t i = 0; i < nvox; i++) + uniq.insert (Nyxus::to_grayscale (r.raw_voxels_NT[i].inten, 0, range, nGrays, ibsi)); + std::vector grey_levels_LUT (uniq.begin(), uniq.end()); // std::set already sorted + int Ng = (int) grey_levels_LUT.size(); + + const int maxNr = nsh + 1; + SimpleMatrix NGLDM; + NGLDM.allocate (maxNr, Ng); + NGLDM.fill (0); + int max_dep = 0; + + // --- 3-plane sliding window of dense grey-binned planes (bg-initialised) + std::vector> ring (3); + int ringZ[3] = { -1, -1, -1 }; + std::vector slab; + auto load = [&](int lz) + { + if (lz < 0 || lz >= Dz) return; + int slot = lz % 3; + if (ringZ[slot] == lz) return; + std::vector& pl = ring[slot]; + pl.assign ((size_t) W * H, bg); + r.raw_voxels_NT.read_slab ((size_t)(zmin + lz), slab); + for (const auto& v : slab) + { + int lx = (int) v.x - (int) xmin, ly = (int) v.y - (int) ymin; + if (lx >= 0 && lx < W && ly >= 0 && ly < H) + pl[(size_t) ly * W + lx] = Nyxus::to_grayscale (v.inten, 0, range, nGrays, ibsi); + } + ringZ[slot] = lz; + }; + + // Interior voxels only (skip the 1-voxel margin so every voxel has all neighbours), as in + // calc_ngld_matrix. Background is NOT skipped. + for (int c = 1; c < Dz - 1; c++) + { + load (c - 1); load (c); load (c + 1); + const std::vector& cur = ring[c % 3]; + + for (int y = 1; y < H - 1; y++) + for (int x = 1; x < W - 1; x++) + { + PixIntens cpi = cur[(size_t) y * W + x]; + int row = (int)(std::lower_bound (grey_levels_LUT.begin(), grey_levels_LUT.end(), cpi) - grey_levels_LUT.begin()); + + int n_matches = 0; + for (int i = 0; i < nsh; i++) + { + int nz = c + shifts[i].dz, ny = y + shifts[i].dy, nx = x + shifts[i].dx; + if (nz < 0 || nz >= Dz || ny < 0 || ny >= H || nx < 0 || nx >= W) + continue; + if (cpi == ring[nz % 3][(size_t) ny * W + nx]) + n_matches++; + } + NGLDM.yx (row, n_matches)++; + max_dep = (std::max) (max_dep, n_matches); + } + } + int Nr = max_dep + 1; + + std::vector Sg, Sr; + calc_rowwise_and_columnwise_totals (Sg, Sr, NGLDM, Ng, Nr); + calc_features (Sg, Sr, NGLDM, Nr, grey_levels_LUT, r.aux_area); } diff --git a/src/nyx/features/3d_ngtdm.cpp b/src/nyx/features/3d_ngtdm.cpp index cf6dce21d..3873cc470 100644 --- a/src/nyx/features/3d_ngtdm.cpp +++ b/src/nyx/features/3d_ngtdm.cpp @@ -274,144 +274,135 @@ void D3_NGTDM_feature::osized_add_online_pixel(size_t x, size_t y, uint32_t inte void D3_NGTDM_feature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&) { - // Clear variables + // Out-of-core NGTDM. Streams the disk-backed voxel cloud through a (2*radius+1)-plane sliding + // window of dense grey-binned planes, reproducing calculate() exactly: unique-over-cube grey + // levels (incl background), the "+1 if min level is 0" shift, the radius neighbourhood average + // (which includes background neighbours), then N/S/P and the shared feature math (calc_*). clear_buffers(); + I.clear(); - // Check if the ROI is degenerate (equal intensity) - if (r.aux_min == r.aux_max) - { - bad_roi_data = true; - return; - } - - // Prepare ROI's intensity range for normalize_I() - PixIntens piRange = r.aux_max - r.aux_min; + int greyInfo = STNGS_IBSI(s) ? 0 : STNGS_NGTDM_GREYDEPTH(s); + bool ibsi = STNGS_IBSI(s); + PixIntens mn = r.aux_min, mx = r.aux_max; + const PixIntens bg = TextureFeature::bin_pixel (0, mn, mx, greyInfo); - // Make a list of intensity clusters (zones) - using AveNeighborhoodInte = std::pair; // Pairs of (intensity, average intensity of all 8 neighbors) - std::vector Z; + const int W = (int) r.aabb.get_width(), + H = (int) r.aabb.get_height(), + Dz = (int) r.aabb.get_z_depth(); + const StatsInt xmin = r.aabb.get_xmin(), ymin = r.aabb.get_ymin(), zmin = r.aabb.get_zmin(); - // While scanning clusters, learn unique intensities + // --- unique binned levels over the whole cube (mask + background if the bbox has any) + const size_t nvox = r.raw_voxels_NT.size(); + const bool hasBackground = nvox < (size_t) W * H * Dz; std::unordered_set U; + if (hasBackground) + U.insert (bg); + PixIntens maxbin = hasBackground ? bg : 0; + for (size_t i = 0; i < nvox; i++) + { + PixIntens b = TextureFeature::bin_pixel (r.raw_voxels_NT[i].inten, mn, mx, greyInfo); + U.insert (b); + if (b > maxbin) maxbin = b; + } + Ngp = (int) U.size(); - // ROI image - WriteImageMatrix_nontriv D("D3_NGTDM_feature_osized_calculate_D", r.label); - D.allocate_from_cloud(r.raw_pixels_NT, r.aabb, false); + // --- grey levels I: IBSI uses a linspace [0, max]; otherwise the unique set. Then sort. + if (ibsi) + for (PixIntens i = 0; i <= maxbin; i++) + I.push_back (i); + else + I.assign (U.begin(), U.end()); + std::sort (I.begin(), I.end()); - // Gather zones - unsigned int nGrays = STNGS_NGTDM_GREYDEPTH(s); // former theEnvironment.get_coarse_gray_depth() - for (int row = 0; row < D.get_height(); row++) - for (int col = 0; col < D.get_width(); col++) - { - // Find a non-blank pixel - PixIntens pi = Nyxus::to_grayscale(D.yx(row, col), r.aux_min, piRange, nGrays, STNGS_IBSI(s)); - if (pi == 0) - continue; + // --- "+1 if min level is 0" shift (applied to I and every voxel value) + const bool shift = (!I.empty() && I[0] == 0); + if (shift) + for (auto& x : I) x += 1; + const PixIntens bgv = bg + (shift ? 1 : 0); // background value in the shifted planes + const PixIntens zeroI = matlab_grey_binning(greyInfo) ? 1 : 0; - // Update unique intensities - U.insert(pi); + // is binned data informative? + if (I.size() < 2) + { + _coarseness = _contrast = _busyness = _complexity = _strength = STNGS_NAN(s); + return; + } - // Evaluate the neighborhood - double neigsI = 0; + const int rad = STNGS_NGTDM_RADIUS(s); + Ng = (int) I.size(); + N.assign (Ng, 0); + S.assign (Ng, 0.0); + P.assign (Ng, 0.0); + Nvp = 0; - int nd = 0; // Number of dependencies + // --- (2*rad+1)-plane sliding window of dense grey-binned planes (shift baked in) + const int ringN = 2 * rad + 1; + std::vector> ring (ringN); + std::vector ringZ (ringN, -1); + std::vector slab; + auto load = [&](int lz) + { + if (lz < 0 || lz >= Dz) return; + int slot = lz % ringN; + if (ringZ[slot] == lz) return; + std::vector& pl = ring[slot]; + pl.assign ((size_t) W * H, bgv); + r.raw_voxels_NT.read_slab ((size_t)(zmin + lz), slab); + for (const auto& v : slab) + { + int lx = (int) v.x - (int) xmin, ly = (int) v.y - (int) ymin; + if (lx >= 0 && lx < W && ly >= 0 && ly < H) + pl[(size_t) ly * W + lx] = TextureFeature::bin_pixel (v.inten, mn, mx, greyInfo) + (shift ? 1 : 0); + } + ringZ[slot] = lz; + }; - if (D.safe(row - 1, col) && D.yx(row - 1, col) != 0) // North - { - neigsI += Nyxus::to_grayscale(D.yx(row - 1, col), r.aux_min, piRange, nGrays, STNGS_IBSI(s)); - nd++; - } + for (int c = 0; c < Dz; c++) + { + for (int lz = c - rad; lz <= c + rad; lz++) + load (lz); + const std::vector& cur = ring[c % ringN]; - if (D.safe(row - 1, col + 1) && D.yx(row - 1, col + 1) != 0) // North-East + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) { - neigsI += Nyxus::to_grayscale(D.yx(row - 1, col + 1), r.aux_min, piRange, nGrays, STNGS_IBSI(s)); - nd++; - } + PixIntens pi = cur[(size_t) y * W + x]; + if (pi == zeroI) // skip background/off-mask voxels + continue; - if (D.safe(row, col + 1) && D.yx(row, col + 1) != 0) // East - { - neigsI += Nyxus::to_grayscale(D.yx(row, col + 1), r.aux_min, piRange, nGrays, STNGS_IBSI(s)); - nd++; - } - if (D.safe(row + 1, col + 1) && D.yx(row + 1, col + 1) != 0) // South-East - { - neigsI += Nyxus::to_grayscale(D.yx(row + 1, col + 1), r.aux_min, piRange, nGrays, STNGS_IBSI(s)); - nd++; - } - if (D.safe(row + 1, col) && D.yx(row + 1, col) != 0) // South - { - neigsI += Nyxus::to_grayscale(D.yx(row + 1, col), r.aux_min, piRange, nGrays, STNGS_IBSI(s)); - nd++; - } - if (D.safe(row + 1, col - 1) && D.yx(row + 1, col - 1) != 0) // South-West - { - neigsI += Nyxus::to_grayscale(D.yx(row + 1, col - 1), r.aux_min, piRange, nGrays, STNGS_IBSI(s)); - nd++; - } - if (D.safe(row, col - 1) && D.yx(row, col - 1) != 0) // West - { - neigsI += Nyxus::to_grayscale(D.yx(row, col - 1), r.aux_min, piRange, nGrays, STNGS_IBSI(s)); - nd++; - } - if (D.safe(row - 1, col - 1) && D.yx(row - 1, col - 1) != 0) // North-West - { - neigsI += Nyxus::to_grayscale(D.yx(row - 1, col - 1), r.aux_min, piRange, nGrays, STNGS_IBSI(s)); - nd++; - } + double neigsI = 0; + int nd = 0; + for (int dz = -rad; dz <= rad; dz++) + for (int dy = -rad; dy <= rad; dy++) + for (int dx = -rad; dx <= rad; dx++) + { + if (dz == 0 && dy == 0 && dx == 0) + continue; + int nz = c + dz, ny = y + dy, nx = x + dx; + if (nz < 0 || nz >= Dz || ny < 0 || ny >= H || nx < 0 || nx >= W) + continue; + neigsI += ring[nz % ringN][(size_t) ny * W + nx]; + nd++; + } - // Save the intensity's average neighborhood intensity - if (nd > 0) - { - neigsI /= nd; - AveNeighborhoodInte z = { pi, neigsI }; - Z.push_back(z); + if (nd > 0) + { + double aveNeigI = neigsI / nd; + int row = (int)(std::lower_bound (I.begin(), I.end(), pi) - I.begin()); + N[row]++; + S[row] += std::abs ((double) I[row] - aveNeigI); + if (aveNeigI > 0.0) + Nvp++; + } } - } - - // Fill the matrix - - Ng = (int) U.size(); - Ngp = (int) U.size(); - - // --allocate the matrix - P.resize (Ng, 0); - S.resize (Ng, 0); - N.resize (Ng, 0); - - // --Set to vector to be able to know each intensity's index - std::vector I(U.begin(), U.end()); - std::sort(I.begin(), I.end()); // Optional - - // --Calculate N and S - for (auto& z : Z) - { - // row - auto iter = std::find(I.begin(), I.end(), z.first); - int row = (STNGS_IBSI(s)) ? - z.first : int(iter - I.begin()); - // col - int col = (int)z.second; // 1-based - // increment - N[row]++; - // --S - PixIntens pi = row; - double aveNeigI = z.second; - S[row] += std::abs(pi - aveNeigI); - // --Nvp - if (aveNeigI > 0.0) - Nvp++; } - // --Calculate Nvc (sum of N) Nvc = 0; - for (int i = 0; i < N.size(); i++) + for (size_t i = 0; i < N.size(); i++) Nvc += N[i]; + for (size_t i = 0; i < N.size(); i++) + P[i] = (Nvc > 0) ? (double) N[i] / Nvc : 0.0; - // --Calculate P - for (int i = 0; i < N.size(); i++) - P[i] = (double)N[i] / Nvc; - - // Calculate features _coarseness = calc_Coarseness(); _contrast = calc_Contrast(); _busyness = calc_Busyness(); diff --git a/src/nyx/features/3d_surface.cpp b/src/nyx/features/3d_surface.cpp index ddc30bf12..db4f30d56 100644 --- a/src/nyx/features/3d_surface.cpp +++ b/src/nyx/features/3d_surface.cpp @@ -290,6 +290,17 @@ void D3_SurfaceFeature::build_surface (LR & r) } } + build_hull (P); +} + +// Build the convex-hull facet complex from a contour point cloud. Shared by the in-core +// build_surface and the out-of-core osized_calculate (which collects the same contour points +// from the disk-backed voxel cloud), so both produce an identical hull. +void D3_SurfaceFeature::build_hull (const std::vector>& P) +{ + constexpr std::size_t dim = 3; + using Points = std::vector>; + const auto eps = 1e-10f; quick_hull qh{ dim, eps }; qh.add_points(std::cbegin(P), std::cend(P)); @@ -352,7 +363,7 @@ void D3_SurfaceFeature::calculate (LR& r, const Fsettings& s) } // volume - + // (fast approximation based on cubic lattice packaging of balls) double ball_r3 = 1. / 8., // r^3, after the anisotropy correction, lattice is expected to be cubic sumPackedV = 0.0; @@ -361,7 +372,7 @@ void D3_SurfaceFeature::calculate (LR& r, const Fsettings& s) fval_VOXEL_VOLUME = sumPackedV / 0.5236; // packaging density at kissing number = 4 (cubic lattice) // surface - + // -- order z-planes' indices std::vector zindices; for (auto& plane : r.zplanes) @@ -517,7 +528,180 @@ void D3_SurfaceFeature::calculate (LR& r, const Fsettings& s) void D3_SurfaceFeature::osized_add_online_pixel(size_t x, size_t y, uint32_t intensity) {} -void D3_SurfaceFeature::osized_calculate (LR& r, const Fsettings& s, ImageLoader& ldr) {} +void D3_SurfaceFeature::osized_calculate (LR& r, const Fsettings& s, ImageLoader& ldr) +{ + // Out-of-core surface: stream the disk-backed voxel cloud (raw_voxels_NT) one Z-plane at a + // time instead of holding the whole cube (raw_pixels_3D). Per plane we extract its contour and + // tally exposed-face adjacencies; covariance is accumulated online. Peak memory is the + // collected boundary points (~surface area) plus two Z-plane occupancy bitmaps -- bounded by + // area, not volume. Values mirror the in-core calculate(). + size_t n = r.raw_voxels_NT.size(); + if (n == 0) + { + cleanup_instance(); + return; + } + + if (STNGS_SINGLEROI(s)) + { + auto w = r.aabb.get_width(), + h = r.aabb.get_height(), + d = r.aabb.get_z_depth(); + + fval_AREA = 2 * (w*h + h*d + w*d); + fval_VOLUME_CONVEXHULL = fval_VOXEL_VOLUME = fval_MESH_VOLUME = w * h * d; + fval_AREA_2_VOLUME = fval_AREA / fval_VOXEL_VOLUME; + fval_COMPACTNESS1 = fval_VOXEL_VOLUME / std::sqrt(M_PI * fval_AREA * fval_AREA * fval_AREA); + fval_COMPACTNESS2 = 36. * M_PI * fval_VOXEL_VOLUME * fval_VOXEL_VOLUME / (fval_AREA * fval_AREA * fval_AREA); + fval_SPHERICAL_DISPROPORTION = fval_AREA / std::pow(36. * M_PI * fval_VOXEL_VOLUME * fval_VOXEL_VOLUME, 1. / 3.); + fval_SPHERICITY = std::pow(36. * M_PI * fval_VOXEL_VOLUME * fval_VOXEL_VOLUME, 1. / 3.) / fval_AREA; + fval_MAJOR_AXIS_LEN = fval_MINOR_AXIS_LEN = fval_LEAST_AXIS_LEN = fval_ELONGATION = fval_FLATNESS = 0; + return; + } + + // -- VOXEL_VOLUME: same cubic-lattice ball packing, a function of voxel count only + double sumPackedV = double(n) * (4. / 3. * M_PI * (1. / 8.)); + fval_VOXEL_VOLUME = sumPackedV / 0.5236; + + const int W = (int) r.aabb.get_width(), + H = (int) r.aabb.get_height(), + minx = (int) r.aabb.get_xmin(), + miny = (int) r.aabb.get_ymin(); + const int verb = STNGS_VERBOSLVL(s); + + // Collected boundary points for the convex hull (bounded by surface area), plus the running + // centroid of those points (matches the in-core centroid over contour voxels). + std::vector> P; + double cx = 0, cy = 0, cz = 0; + size_t hullCloudLen = 0; + + // Surface area as exposed faces = 6*N - 2*(adjacent voxel pairs). x/y adjacencies are tallied + // within a plane; z adjacencies between a plane and the previous one -- a 2-plane window. + double adjacencies = 0.0; + std::vector prevOcc; // occupancy bitmap of plane (z-1) over the [W x H] ROI bbox + long long prevZ = -2; // z index of prevOcc; -2 = none + + // Online covariance sums over all voxels + double sx = 0, sy = 0, sz = 0, sxx = 0, syy = 0, szz = 0, sxy = 0, sxz = 0, syz = 0; + + const size_t depth = r.raw_voxels_NT.depth(); + std::vector slab; + for (size_t z = 0; z < depth; z++) + { + r.raw_voxels_NT.read_slab (z, slab); + if (slab.empty()) + continue; + + // -- contour of this plane (build over a plane-local cloud; the returned indices point + // into 'slab', and the resulting boundary POINTS/order match the in-core path) + std::vector planeIdx (slab.size()); + for (size_t i = 0; i < slab.size(); i++) + planeIdx[i] = i; + std::vector K; + Nyxus::build_contour_imp (K, slab, planeIdx, (int) z, W, H, minx, miny, verb); + for (auto ik : K) + { + const auto& v = slab[ik]; + P.push_back (std::array({ (float) v.x, (float) v.y, (float) v.z })); + cx += v.x; cy += v.y; cz += v.z; + hullCloudLen++; + } + + // -- this plane's occupancy bitmap + online covariance sums + std::vector curOcc ((size_t) W * H, 0); + for (const auto& v : slab) + { + int lx = (int) v.x - minx, ly = (int) v.y - miny; + if (lx >= 0 && lx < W && ly >= 0 && ly < H) + curOcc[(size_t) ly * W + lx] = 1; + + double dx = (double) v.x, dy = (double) v.y, dz = (double) v.z; + sx += dx; sy += dy; sz += dz; + sxx += dx * dx; syy += dy * dy; szz += dz * dz; + sxy += dx * dy; sxz += dx * dz; syz += dy * dz; + } + + // -- adjacencies. +x / +y within this plane (each unordered in-plane pair once) + for (const auto& v : slab) + { + int lx = (int) v.x - minx, ly = (int) v.y - miny; + if (lx + 1 < W && ly >= 0 && ly < H && curOcc[(size_t) ly * W + (lx + 1)]) + adjacencies += 1.0; + if (ly + 1 < H && lx >= 0 && lx < W && curOcc[(size_t) (ly + 1) * W + lx]) + adjacencies += 1.0; + // +z: pair with the previous plane at the same (x,y) (each unordered z pair once) + if (prevZ == (long long) z - 1 && lx >= 0 && lx < W && ly >= 0 && ly < H + && prevOcc[(size_t) ly * W + lx]) + adjacencies += 1.0; + } + + prevOcc.swap (curOcc); + prevZ = (long long) z; + } + + fval_AREA = 6.0 * double(n) - 2.0 * adjacencies; + + // -- convex hull from the collected boundary points + build_hull (P); + + // -- convex hull volume via signed tetrahedra from the contour centroid + if (hullCloudLen) + { + cx /= double(hullCloudLen); + cy /= double(hullCloudLen); + cz /= double(hullCloudLen); + } + fval_VOLUME_CONVEXHULL = 0; + for (const auto& sx3 : hull_complex) + { + double d = Nyxus::det4( + sx3.a[0], sx3.a[1], sx3.a[2], 1, + sx3.b[0], sx3.b[1], sx3.b[2], 1, + sx3.c[0], sx3.c[1], sx3.c[2], 1, + cx, cy, cz, 1); + fval_VOLUME_CONVEXHULL += d / 6; + } + + // -- volume-area ratio features + fval_MESH_VOLUME = fval_VOLUME_CONVEXHULL; + fval_AREA_2_VOLUME = fval_AREA / fval_VOXEL_VOLUME; + fval_COMPACTNESS1 = fval_VOXEL_VOLUME / std::sqrt(M_PI * fval_AREA * fval_AREA * fval_AREA); + fval_COMPACTNESS2 = 36. * M_PI * fval_VOXEL_VOLUME * fval_VOXEL_VOLUME / (fval_AREA * fval_AREA * fval_AREA); + fval_SPHERICAL_DISPROPORTION = fval_AREA / std::pow(36. * M_PI * fval_VOXEL_VOLUME * fval_VOXEL_VOLUME, 1. / 3.); + fval_SPHERICITY = std::pow(36. * M_PI * fval_VOXEL_VOLUME * fval_VOXEL_VOLUME, 1. / 3.) / fval_AREA; + + // -- PCA axis lengths from the covariance matrix (sample covariance, matching calc_covariance: + // Cov(a,b) = (Sum a*b - N*mean_a*mean_b) / (N-1)) + double K[3][3]; + if (n > 1) + { + double mx = sx / double(n), my = sy / double(n), mz = sz / double(n); + double nf = double(n) - 1.0; + K[0][0] = (sxx - double(n) * mx * mx) / nf; + K[1][1] = (syy - double(n) * my * my) / nf; + K[2][2] = (szz - double(n) * mz * mz) / nf; + K[0][1] = K[1][0] = (sxy - double(n) * mx * my) / nf; + K[0][2] = K[2][0] = (sxz - double(n) * mx * mz) / nf; + K[1][2] = K[2][1] = (syz - double(n) * my * mz) / nf; + } + else + K[0][0] = K[0][1] = K[0][2] = K[1][0] = K[1][1] = K[1][2] = K[2][0] = K[2][1] = K[2][2] = 0; + + double L[3]; + if (Nyxus::calc_eigvals(L, K)) + { + // calc_eigvals returns L sorted DESCENDING (L[0] largest): MAJOR<-L[0], MINOR<-L[1], + // LEAST<-L[2]; ELONGATION=MINOR/MAJOR=sqrt(L[1]/L[0]), FLATNESS=LEAST/MAJOR=sqrt(L[2]/L[0]). + // Matches MIRP/IBSI and the in-RAM calculate() path. + fval_MAJOR_AXIS_LEN = 4.0 * sqrt(L[0]); + fval_MINOR_AXIS_LEN = 4.0 * sqrt(L[1]); + fval_LEAST_AXIS_LEN = 4.0 * sqrt(L[2]); + fval_ELONGATION = sqrt(L[1] / L[0]); + fval_FLATNESS = sqrt(L[2] / L[0]); + } + else + fval_MAJOR_AXIS_LEN = fval_MINOR_AXIS_LEN = fval_LEAST_AXIS_LEN = fval_ELONGATION = fval_FLATNESS = 0.0; +} void D3_SurfaceFeature::save_value(std::vector>& fvals) { diff --git a/src/nyx/features/3d_surface.h b/src/nyx/features/3d_surface.h index be1042f4e..8410e8bfe 100644 --- a/src/nyx/features/3d_surface.h +++ b/src/nyx/features/3d_surface.h @@ -1,6 +1,8 @@ #pragma once #pragma once +#include +#include #include "../dataset.h" #include "../featureset.h" #include "../feature_method.h" @@ -41,13 +43,13 @@ class D3_SurfaceFeature : public FeatureMethod private: - struct Simplex3 - { - float a[3], b[3], c[3]; // layout: x, y, z - Simplex3 (const float* a_, const float* b_, const float* c_) - { - for (int i = 0; i < 3; i++) - { + struct Simplex3 + { + float a[3], b[3], c[3]; // layout: x, y, z + Simplex3 (const float* a_, const float* b_, const float* c_) + { + for (int i = 0; i < 3; i++) + { a[i] = a_[i]; b[i] = b_[i]; c[i] = c_[i]; @@ -56,8 +58,10 @@ class D3_SurfaceFeature : public FeatureMethod }; std::vector hull_complex; - + void build_surface (LR& r); + // Build the hull complex from a contour point cloud (shared by in-core + out-of-core paths) + void build_hull (const std::vector>& P); double fval_AREA, fval_AREA_2_VOLUME, diff --git a/src/nyx/features/histogram.h b/src/nyx/features/histogram.h index e1547edd4..179fa6309 100644 --- a/src/nyx/features/histogram.h +++ b/src/nyx/features/histogram.h @@ -11,6 +11,7 @@ #include "../helpers/helpers.h" #include "pixel.h" #include "image_matrix_nontriv.h" +#include "voxel_cloud_nontriv.h" using HistoItem = unsigned int; @@ -169,15 +170,105 @@ class TrivialHistogram // percentiles calc_percentiles(); - // robust mean - bin10ctr_ = bins100_ [10]; - bin90ctr_ = bins100_ [90]; + // robust MAD: threshold on the p10/p90 percentile values, and average the + // 10..90 population before accumulating deviations (mirrors the in-core overload) mean1090val_ = 0.0; size_t pop1090 = 0; for (auto pxl : raw_data) { double a = double (pxl.inten); - if (a >= bin10ctr_ && a <= bin90ctr_) + if (a >= p10_ && a <= p90_) + { + mean1090val_ += a; + pop1090++; + } + } + rmad_ = 0.0; + if (pop1090) + { + mean1090val_ /= double(pop1090); + for (auto pxl : raw_data) + { + double a = double(pxl.inten); + if (a >= p10_ && a <= p90_) + rmad_ += (std::fabs)(a - mean1090val_); + } + rmad_ /= double(pop1090); + } + } + + // Out-of-core 3D voxel-cloud overload: identical math to the OutOfRamPixelCloud overload + // (the disk-backed 2D cloud), sourcing intensities from the disk-backed 3D voxel cloud so + // an oversized volumetric ROI's histogram streams instead of holding the cube. + void initialize (int n_cust_bins, HistoItem min_value, HistoItem max_value, const OutOfRamVoxelCloud& raw_data) + { + // safety + int n_custBins = std::abs(n_cust_bins); + + pop_ = raw_data.size(); + + // Allocate + // -- "percentile" + bins100_.reserve(100); + for (int i = 0; i < 100 + 1; i++) + bins100_.push_back(0); + // -- "uint8" + bins_cust_.reserve(n_custBins); + for (int i = 0; i < n_custBins + 1; i++) + bins_cust_.push_back(0); + + // Cache min/max + minVal_ = min_value; + maxVal_ = max_value; + auto valRange = maxVal_ - minVal_; + + // unique values + for (auto s : raw_data) + U_.push_back (s.inten); + + // Build the "percentile" histogram + binW100_ = double(valRange) / 100.; + for (auto s : raw_data) + { + HistoItem h = s.inten; + double realIdx = double(h - minVal_) / binW100_; + int idx = std::isnan(realIdx) ? 0 : int(realIdx); + (bins100_[idx])++; + } + + // -- Fix the special last bin + bins100_[100 - 1] += bins100_[100]; + bins100_[100] = 0; + + // Build the "uint8" histogram + binWcust_ = double(valRange) / double(n_custBins-1); + for (auto s : raw_data) + { + HistoItem h = Nyxus::to_grayscale(s.inten, minVal_, valRange, n_custBins); + bins_cust_[h] = bins_cust_[h] + 1; + } + + // -- Fix the special last bin + bins_cust_[n_custBins - 1] += bins_cust_[n_custBins]; + bins_cust_[n_custBins] = 0; + + // Mean calculation + meanVal_ = 0; + for (auto s : raw_data) + meanVal_ += double(s.inten); + meanVal_ /= double(pop_); + + // percentiles + calc_percentiles(); + + // robust MAD: threshold on the p10/p90 percentile values, and average the + // 10..90 population before accumulating deviations (mirrors the in-core overload) + mean1090val_ = 0.0; + size_t pop1090 = 0; + for (auto pxl : raw_data) + { + double a = double (pxl.inten); + if (a >= p10_ && a <= p90_) { mean1090val_ += a; pop1090++; @@ -186,10 +277,11 @@ class TrivialHistogram rmad_ = 0.0; if (pop1090) { + mean1090val_ /= double(pop1090); for (auto pxl : raw_data) { double a = double(pxl.inten); - if (a >= bin10ctr_ && a <= bin90ctr_) + if (a >= p10_ && a <= p90_) rmad_ += (std::fabs)(a - mean1090val_); } rmad_ /= double(pop1090); diff --git a/src/nyx/features/intensity.cpp b/src/nyx/features/intensity.cpp index f4f268dd8..6aa511e25 100644 --- a/src/nyx/features/intensity.cpp +++ b/src/nyx/features/intensity.cpp @@ -269,8 +269,10 @@ void PixelIntensityFeatures::osized_calculate (LR& r, const Fsettings& stng, con // --MAD, VARIANCE, STDDEV, COV double mad = 0.0, var = 0.0; - for (auto& px : r.raw_pixels) + // out-of-core: dispersion is accumulated from the disk-backed pixel cloud + for (size_t i = 0; i < r.raw_pixels_NT.size(); i++) { + Pixel2 px = r.raw_pixels_NT[i]; double diff = px.inten - mean_; mad += std::abs(diff); var += diff * diff; @@ -323,12 +325,16 @@ void PixelIntensityFeatures::osized_calculate (LR& r, const Fsettings& stng, con // Median absolute deviation double medad = 0.0; - for (auto& px : r.raw_pixels) + // out-of-core: read from the disk-backed pixel cloud + for (size_t i = 0; i < r.raw_pixels_NT.size(); i++) + { + Pixel2 px = r.raw_pixels_NT[i]; medad += std::abs(px.inten - median_); + } val_MEDIAN_ABSOLUTE_DEVIATION = medad / n; - // --Uniformity calculated as PIU, percent image uniformity - see "A comparison of five standard methods for evaluating image - // intensity uniformity in partially parallel imaging MRI" [https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3745492/] + // --Uniformity calculated as PIU, percent image uniformity - see "A comparison of five standard methods for evaluating image + // intensity uniformity in partially parallel imaging MRI" [https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3745492/] // and https://aapm.onlinelibrary.wiley.com/doi/abs/10.1118/1.2241606 double piu = (1.0 - double(r.aux_max - r.aux_min) / double(r.aux_max + r.aux_min)) * 100.0; val_UNIFORMITY_PIU = piu; @@ -349,11 +355,19 @@ void PixelIntensityFeatures::osized_calculate (LR& r, const Fsettings& stng, con // Excess kurtosis val_EXCESS_KURTOSIS = val_KURTOSIS - 3; - // Hyperskewness hs = E[x-mean].^5 / std(x).^5 - val_HYPERSKEWNESS = mom.hyperskewness(); - - // Hyperflatness hf = E[x-mean].^6 / std(x).^6 - val_HYPERFLATNESS = mom.hyperflatness(); + // Hyperskewness / Hyperflatness from explicit central moments over the disk-backed cloud, + // matching the in-core path's sum-of-powers definition rather than the Moments4 variant + double sumPow5 = 0, sumPow6 = 0; + for (size_t i = 0; i < r.raw_pixels_NT.size(); i++) + { + double diff = double(r.raw_pixels_NT[i].inten) - mean_; + sumPow5 += std::pow(diff, 5.); + sumPow6 += std::pow(diff, 6.); + } + double denom5 = n * std::pow(val_STANDARD_DEVIATION, 5.); + val_HYPERSKEWNESS = denom5 == 0. ? 0. : sumPow5 / denom5; + double denom6 = n * std::pow(val_STANDARD_DEVIATION, 6.); + val_HYPERFLATNESS = denom6 == 0. ? 0. : sumPow6 / denom6; } void PixelIntensityFeatures::save_value(std::vector>& fvals) diff --git a/src/nyx/features/intensity.h b/src/nyx/features/intensity.h index 140c61bf9..c99035b17 100644 --- a/src/nyx/features/intensity.h +++ b/src/nyx/features/intensity.h @@ -46,6 +46,10 @@ class PixelIntensityFeatures : public FeatureMethod void osized_add_online_pixel(size_t x, size_t y, uint32_t intensity); void osized_calculate (LR & roi, const Fsettings & s, const Dataset & dataset, ImageLoader& ldr); void osized_calculate (LR& roi, const Fsettings& s, ImageLoader& ldr) { throw std::runtime_error("illegal use of PixelIntensityFeatures::osized_calculate()"); } + // Out-of-core dispatch for intensity: route to the Dataset-taking osized_calculate, which + // needs slide props for COVERED_IMAGE_INTENSITY_RANGE. The Dataset-less overload above is a + // guard that throws, so the base osized_scan_whole_image cannot be used for this feature. + void osized_scan_whole_image (LR& roi, const Fsettings& s, const Dataset& ds, ImageLoader& ldr) override { osized_calculate (roi, s, ds, ldr); save_value (roi.fvals); } void save_value (std::vector>& feature_vals); static void reduce (size_t start, size_t end, std::vector* ptrLabels, std::unordered_map * ptrLabelData, const Fsettings & settings, const Dataset & dataset); static void extract (LR & roi, const Fsettings & settings, const Dataset & dataset); diff --git a/src/nyx/features/intensity_histogram.h b/src/nyx/features/intensity_histogram.h index 815fa0f66..f1d08811c 100644 --- a/src/nyx/features/intensity_histogram.h +++ b/src/nyx/features/intensity_histogram.h @@ -84,6 +84,10 @@ class IntensityHistogramFeatures : public FeatureMethod void osized_add_online_pixel (size_t x, size_t y, uint32_t intensity); void osized_calculate (LR& roi, const Fsettings& s, const Dataset& dataset, ImageLoader& ldr); void osized_calculate (LR& roi, const Fsettings& s, ImageLoader& ldr) { throw std::runtime_error("illegal use of IntensityHistogramFeatures::osized_calculate()"); } + // Out-of-core dispatch for the intensity histogram: route to the Dataset-taking + // osized_calculate. The Dataset-less overload above is a guard that throws, so the base + // osized_scan_whole_image cannot be used for this feature. + void osized_scan_whole_image (LR& roi, const Fsettings& s, const Dataset& ds, ImageLoader& ldr) override { osized_calculate (roi, s, ds, ldr); save_value (roi.fvals); } void save_value (std::vector>& feature_vals); static void reduce (size_t start, size_t end, std::vector* ptrLabels, std::unordered_map * ptrLabelData, const Fsettings& settings, const Dataset& dataset); static void extract (LR& roi, const Fsettings& settings, const Dataset& dataset); diff --git a/src/nyx/features/streaming_ccl.h b/src/nyx/features/streaming_ccl.h new file mode 100644 index 000000000..9bdb7217c --- /dev/null +++ b/src/nyx/features/streaming_ccl.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include "pixel.h" + +/// @brief Growable union-find over connected-component labels, shared by the out-of-core +/// GLSZM/GLDZM streaming connected-component labeling (see docs/13-3d-ooc-glszm-gldzm-design.md). +/// +/// A label is created for each newly-discovered zone (make_label) and merged with another when a +/// streaming scan discovers two previously-separate labels actually belong to the same zone +/// (union_sum/union_min). Each label carries a running metric -- the zone's voxel count for GLSZM +/// (combined by SUM on union) or its minimum distance-to-border for GLDZM (combined by MIN) -- so +/// after the whole volume streams once, each root label directly holds its zone's final metric; no +/// second pass over voxels is needed. +class LabelUnionFind +{ +public: + int make_label (PixIntens intensity, long long initial_metric) + { + parent_.push_back ((int) parent_.size()); + rank_.push_back (0); + metric_.push_back (initial_metric); + intensity_.push_back (intensity); + return (int) parent_.size() - 1; + } + + int find (int x) + { + while (parent_[x] != x) + { + parent_[x] = parent_[parent_[x]]; // path halving + x = parent_[x]; + } + return x; + } + + // Merge labels a and b, combining their metric as a SUM (GLSZM zone size) + int union_sum (int a, int b) { return union_impl (a, b, true); } + // Merge labels a and b, combining their metric as a MIN (GLDZM zone distance) + int union_min (int a, int b) { return union_impl (a, b, false); } + + // Add this voxel to an existing zone's running size (GLSZM) + void add_sum (int label, long long delta) { int r = find (label); metric_[r] += delta; } + // Fold this voxel's distance into an existing zone's running minimum (GLDZM) + void update_min (int label, long long val) { int r = find (label); metric_[r] = (std::min)(metric_[r], val); } + + long long metric (int label) { return metric_[find (label)]; } + PixIntens intensity_of (int label) const { return intensity_[label]; } + size_t num_labels() const { return parent_.size(); } + +private: + int union_impl (int a, int b, bool sumMode) + { + int ra = find (a), rb = find (b); + if (ra == rb) return ra; + if (rank_[ra] < rank_[rb]) std::swap (ra, rb); + parent_[rb] = ra; + if (rank_[ra] == rank_[rb]) rank_[ra]++; + metric_[ra] = sumMode ? (metric_[ra] + metric_[rb]) : (std::min)(metric_[ra], metric_[rb]); + return ra; + } + + std::vector parent_, rank_; + std::vector metric_; + std::vector intensity_; +}; diff --git a/src/nyx/features/voxel_cloud_nontriv.cpp b/src/nyx/features/voxel_cloud_nontriv.cpp new file mode 100644 index 000000000..d6baf2619 --- /dev/null +++ b/src/nyx/features/voxel_cloud_nontriv.cpp @@ -0,0 +1,180 @@ +#include +#include +#include +#include +#include +#include "../environment.h" +#include "../helpers/fsystem.h" +#include "../helpers/helpers.h" +#include "voxel_cloud_nontriv.h" + +OutOfRamVoxelCloud::OutOfRamVoxelCloud() {} + +OutOfRamVoxelCloud::~OutOfRamVoxelCloud() +{ + if (pF) + { + fclose(pF); + pF = nullptr; + fs::remove(filepath); + } +} + +void OutOfRamVoxelCloud::check_io_ok() const +{ + if (!pF) + throw (std::runtime_error("ERROR in OutOfRamVoxelCloud - file might not be open with init()")); +} + +Pixel3 OutOfRamVoxelCloud::decode_record (const char* rec) const +{ + Pixel3 vx; + std::memcpy (&vx.x, rec, sizeof(vx.x)); rec += sizeof(vx.x); + std::memcpy (&vx.y, rec, sizeof(vx.y)); rec += sizeof(vx.y); + std::memcpy (&vx.z, rec, sizeof(vx.z)); rec += sizeof(vx.z); + std::memcpy (&vx.inten, rec, sizeof(vx.inten)); + return vx; +} + +void OutOfRamVoxelCloud::init (unsigned int _roi_label, const std::string& name) +{ + n_items = 0; + slab_first.clear(); + slab_count.clear(); + invalidate_cache(); + + auto tid = std::this_thread::get_id(); + + std::stringstream ssPath; + ssPath << Nyxus::get_temp_dir_path() << name << "_roi" << _roi_label << "_tid" << tid << ".vox.nyxus"; + filepath = ssPath.str(); + + errno = 0; + pF = fopen(filepath.c_str(), "w+b"); + if (!pF) + throw (std::runtime_error("Error creating file " + filepath + " Error code:" + std::to_string(errno))); + + if (std::setvbuf(pF, nullptr, _IOFBF, 32768) != 0) + std::cout << "setvbuf failed\n"; +} + +void OutOfRamVoxelCloud::close() +{ + if (pF) + { + fclose(pF); + pF = nullptr; + } +} + +void OutOfRamVoxelCloud::clear() +{ + if (pF) + { + close(); + fs::remove (filepath); + n_items = 0; + } + slab_first.clear(); + slab_count.clear(); + invalidate_cache(); +} + +void OutOfRamVoxelCloud::begin_slab (size_t z) +{ + // Planes arrive in ascending order; grow the index so slab_first[z]/slab_count[z] are addressable + if (slab_first.size() <= z) + { + slab_first.resize (z + 1, n_items); + slab_count.resize (z + 1, 0); + } + slab_first[z] = n_items; + slab_count[z] = 0; +} + +void OutOfRamVoxelCloud::add_voxel (const Pixel3& v) +{ + check_io_ok(); + + fwrite ((const void*) &(v.x), sizeof(v.x), 1, pF); + fwrite ((const void*) &(v.y), sizeof(v.y), 1, pF); + fwrite ((const void*) &(v.z), sizeof(v.z), 1, pF); + fwrite ((const void*) &(v.inten), sizeof(v.inten), 1, pF); + + n_items++; + if (!slab_count.empty()) + slab_count.back()++; + + // A write past the cached block would make it stale; the population phase never interleaves + // reads, so this just holds the cache empty until the first read. + invalidate_cache(); +} + +void OutOfRamVoxelCloud::end_slab (size_t /*z*/) {} + +size_t OutOfRamVoxelCloud::size() const +{ + return n_items; +} + +Pixel3 OutOfRamVoxelCloud::get_at (size_t idx) const +{ + check_io_ok(); + + // Serve from the cached block; on a miss, bulk-read the block containing idx with one fread + // (sequential consumers then hit the cache BLK_RECORDS times before the next miss). + if (blk_first < 0 || idx < (size_t) blk_first || idx >= (size_t) blk_first + blk_count) + { + size_t start = (idx / BLK_RECORDS) * BLK_RECORDS; + size_t want = (std::min) (BLK_RECORDS, n_items - start); + blk_buf.resize (want * item_size); + fseek (pF, (long) (start * item_size), SEEK_SET); + size_t got = fread (blk_buf.data(), item_size, want, pF); + blk_first = (long long) start; + blk_count = got; + } + return decode_record (blk_buf.data() + (idx - (size_t) blk_first) * item_size); +} + +void OutOfRamVoxelCloud::read_range (size_t first, size_t count, std::vector& out) const +{ + check_io_ok(); + + out.clear(); + out.reserve (count); + if (count == 0) + return; + + // One bulk fread of the whole range, then decode in RAM (a slab is bounded by the plane bbox) + std::vector buf (count * item_size); + fseek (pF, (long) (first * item_size), SEEK_SET); + size_t got = fread (buf.data(), item_size, count, pF); + for (size_t i = 0; i < got; i++) + out.push_back (decode_record (buf.data() + i * item_size)); +} + +void OutOfRamVoxelCloud::read_slab (size_t z, std::vector& out) const +{ + if (z >= slab_first.size()) + { + out.clear(); + return; + } + read_range (slab_first[z], slab_count[z], out); +} + +void OutOfRamVoxelCloud::read_slab_window (size_t z0, size_t z1, std::vector& out) const +{ + // Contiguous on disk because records are Z-sorted: one range spans planes z0..z1 + if (slab_first.empty() || z0 >= slab_first.size()) + { + out.clear(); + return; + } + size_t zi = (z1 < slab_first.size()) ? z1 : slab_first.size() - 1; + size_t first = slab_first[z0]; + size_t count = 0; + for (size_t z = z0; z <= zi; z++) + count += slab_count[z]; + read_range (first, count, out); +} diff --git a/src/nyx/features/voxel_cloud_nontriv.h b/src/nyx/features/voxel_cloud_nontriv.h new file mode 100644 index 000000000..ae665785a --- /dev/null +++ b/src/nyx/features/voxel_cloud_nontriv.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include "pixel.h" + +/// @brief Writeable out-of-memory 3D voxel cloud. +/// +/// The 3D analog of OutOfRamPixelCloud: it stores Pixel3 records (x,y,z,inten) on disk +/// so an oversized volumetric ROI can be featurized without holding the whole voxel cube +/// in RAM. Records are appended in ascending-Z order by the streaming population scan, so +/// they land on disk already Z-sorted; a small in-RAM per-plane index (first-record offset +/// and count per Z) lets a consumer seek to any Z-slab and read just that slab's voxels. +/// The whole-cloud linear accessors (size/get_at/iterator) serve the Z-agnostic intensity +/// and histogram consumers; the slab accessors serve the sliding-window consumers (surface, +/// and later the 3D texture features). +class OutOfRamVoxelCloud +{ +public: + OutOfRamVoxelCloud(); + OutOfRamVoxelCloud (const OutOfRamVoxelCloud&) = delete; + ~OutOfRamVoxelCloud(); + + // Lifecycle. init() creates the backing file under get_temp_dir_path(). + void init (unsigned int roi_label, const std::string& name); + void close(); + void clear(); + + // Population (called in ascending-Z order). begin_slab(z) marks where plane z's records + // start; add_voxel() appends one record; end_slab(z) finalizes the plane's count. + void begin_slab (size_t z); + void add_voxel (const Pixel3& v); + void end_slab (size_t z); + + // Whole-cloud linear access (intensity, histogram) + size_t size() const; + Pixel3 get_at (size_t idx) const; + Pixel3 operator[] (size_t idx) const { return get_at(idx); } + + // Slab access (surface, later texture): a Z-plane or a [z0,z1] window + size_t depth() const { return slab_first.size(); } + void read_slab (size_t z, std::vector& out) const; + void read_slab_window (size_t z0, size_t z1, std::vector& out) const; + + struct iterator + { + public: + iterator (const OutOfRamVoxelCloud& obj, std::size_t idx) : m_object(obj), m_index(idx) {} + Pixel3 operator * () const { return m_object.get_at(m_index); } + bool operator == (iterator const& it) const { return (&m_object == &it.m_object) && (m_index == it.m_index); } + bool operator != (iterator const& it) const { return (&m_object != &it.m_object) || (m_index != it.m_index); } + iterator& operator ++ () { ++m_index; return *this; } + private: + const OutOfRamVoxelCloud& m_object; + std::size_t m_index; + }; + iterator begin() const { return iterator(*this, 0); } + iterator end() const { return iterator(*this, size()); } + +private: + size_t n_items = 0; + std::string filepath; + FILE* pF = nullptr; + // One on-disk record = x,y,z (Pixel3's Point3i fields) + intensity + size_t item_size = sizeof(Pixel3::x) + sizeof(Pixel3::y) + sizeof(Pixel3::z) + sizeof(Pixel3::inten); + // Per-Z index built during population: first record index and record count of each plane + std::vector slab_first, slab_count; + void check_io_ok() const; + void read_range (size_t first, size_t count, std::vector& out) const; + + // Block read cache so linear consumers (get_at/operator[]/iterator, histogram) do one bulk + // fread per block instead of an fseek + 4 freads per voxel -- the latter made the out-of-core + // intensity/histogram passes ~25x slower than the in-RAM path. One block resident (bounded). + static const size_t BLK_RECORDS = 65536; + mutable std::vector blk_buf; + mutable long long blk_first = -1; // index of first cached record; -1 = cache empty + mutable size_t blk_count = 0; + void invalidate_cache() const { blk_first = -1; blk_count = 0; } + Pixel3 decode_record (const char* rec) const; +}; diff --git a/src/nyx/globals.h b/src/nyx/globals.h index 929384eea..f61704f55 100644 --- a/src/nyx/globals.h +++ b/src/nyx/globals.h @@ -18,6 +18,8 @@ #include "results_cache.h" #include "roi_cache.h" #include "save_option.h" + +class FeatureMethod; // forward decl for is_3d_ooc_supported() #include "arrow_output_stream.h" #include "nested_feature_aggregation.h" // Nested ROI #include "cli_nested_roi_options.h" @@ -88,6 +90,19 @@ namespace Nyxus bool processTrivialRois_25D (Environment & env, const std::vector& trivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t memory_limit, const std::vector& z_indices); bool processTrivialRois_3D (Environment & env, size_t sidx, size_t t_index, size_t channel, const std::vector& trivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t memory_limit); bool processNontrivialRois (Environment& env, const std::vector& nontrivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath); + bool processNontrivialRois_3D (Environment& env, const std::vector& nontrivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t channel, size_t timeframe); + // Allow-list of 3D feature classes whose osized_calculate() streams from the disk-backed voxel + // cloud; used by processNontrivialRois_3D()'s per-feature out-of-core guard, and independently + // unit-testable (see TEST_3D_OOC_GUARD_REJECTS_UNSUPPORTED_FEATURE). + bool is_3d_ooc_supported (FeatureMethod* f); + // Streams r.raw_voxels_NT from 'imlo' plane-by-plane; when wholevolume is true every voxel is + // kept (no mask), otherwise only voxels matching r.label. Returns false (cloud cleared) if the + // loader can't deliver the volume plane-by-plane. Shared by the segmented and whole-volume + // out-of-core paths (processNontrivialRois_3D, workflow_3d_whole.cpp's oversized branch). + bool populate_3d_voxel_cloud (ImageLoader& imlo, LR& r, size_t channel, size_t timeframe, bool wholevolume); + // Runs every requested feature's out-of-core path over an already-populated r.raw_voxels_NT, + // guarded by is_3d_ooc_supported(); writes into r.fvals via save_value(). Shared the same way. + void run_3d_ooc_features (Environment& env, LR& r, ImageLoader& imloader); bool scan_trivial_wholeslide (LR& vroi, const std::string& intens_fpath, ImageLoader& ldr); // reads pixels of whole slide 'intens_fpath' into virtual ROI 'vroi' bool scan_trivial_wholeslide_anisotropic (LR& vroi, const std::string& intens_fpath, ImageLoader& ldr, double aniso_x, double aniso_y); diff --git a/src/nyx/image_loader.cpp b/src/nyx/image_loader.cpp index 940e812aa..2b7c8d87b 100644 --- a/src/nyx/image_loader.cpp +++ b/src/nyx/image_loader.cpp @@ -346,6 +346,78 @@ bool ImageLoader::load_volume (size_t channel, size_t timeframe, size_t mask_tim return true; } +void ImageLoader::assemble_one_plane (AbstractTileLoader* fl, + std::shared_ptr>& ptr, + std::vector& dst_plane, size_t gz, size_t channel, size_t timeframe) +{ + // Per-plane loader (tileDepth==1): the global Z index IS the loader's layer, and the + // within-slab plane offset is 0. Mirrors assemble_volume's per-tile copy for one plane. + const size_t ltt = fl->tileTimestamps (lvl); + const size_t frameStride = th * tw; // tileDepth==1 + const size_t frameBase = (ltt > 1) ? timeframe * frameStride : 0; + const size_t lnth = fl->numberTileHeight (lvl); + const size_t lntw = fl->numberTileWidth (lvl); + + for (size_t tr = 0; tr < lnth; ++tr) + for (size_t tc = 0; tc < lntw; ++tc) + { + fl->loadTileFromFile (ptr, tr, tc, gz, channel, timeframe, lvl); + + const size_t row0 = tr * th, col0 = tc * tw; + if (row0 >= fh || col0 >= fw) + continue; + const size_t validH = (std::min) (th, fh - row0), + validW = (std::min) (tw, fw - col0); + + for (size_t row = 0; row < validH; ++row) + { + const size_t src = frameBase + row * tw; + const size_t d = (row0 + row) * fw + col0; + std::copy (ptr->begin() + src, ptr->begin() + src + validW, dst_plane.begin() + d); + } + } +} + +bool ImageLoader::stream_volume_planes (size_t channel, size_t timeframe, size_t mask_timeframe, + const std::function&, const std::vector&)>& sink) +{ + cur_channel = channel; + cur_timeframe = timeframe; + + // A whole-4D loader (NIfTI) hands over the entire cube in one read; there is nothing to + // stream plane-by-plane, so decline and let the caller fail loudly. + if (intFL->tileDepth (lvl) != 1) + return false; + if (segFL != nullptr && segFL->tileDepth (lvl) != 1) + return false; + + // Mask is usually channel-agnostic and single-timeframe; clamp like load_volume does + size_t mask_channel = 0, mask_tf = 0; + if (segFL != nullptr) + { + mask_channel = (channel < segFL->numberChannels()) ? channel : 0; + mask_tf = (mask_timeframe < segFL->fullTimestamps (lvl)) ? mask_timeframe : 0; + } + + const size_t sliceSize = (size_t) fw * fh; + std::vector intPlane (sliceSize), segPlane (segFL != nullptr ? sliceSize : 0); + + for (size_t gz = 0; gz < fd; ++gz) + { + std::fill (intPlane.begin(), intPlane.end(), 0u); + assemble_one_plane (intFL, ptrI, intPlane, gz, channel, timeframe); + + if (segFL != nullptr) + { + std::fill (segPlane.begin(), segPlane.end(), 0u); + assemble_one_plane (segFL, ptrL, segPlane, gz, mask_channel, mask_tf); + } + + sink (gz, intPlane, segPlane); + } + return true; +} + const std::vector& ImageLoader::get_int_tile_buffer() { return *ptrI; diff --git a/src/nyx/image_loader.h b/src/nyx/image_loader.h index 01acb8a1e..ca92c30b8 100644 --- a/src/nyx/image_loader.h +++ b/src/nyx/image_loader.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -32,6 +33,15 @@ class ImageLoader const std::vector& get_int_volume_buffer() const { return vol_int_; } const std::vector& get_seg_volume_buffer() const { return vol_seg_; } + // Stream the whole X*Y*Z volume for one (channel, timeframe) WITHOUT materializing it: + // one Z-plane is assembled into a reused buffer and handed to 'sink', so peak memory is + // two planes (intensity + mask) rather than the whole cube. This is what lets an oversized + // volumetric ROI be featurized out-of-core. Applies the same mask channel/timeframe clamp as + // load_volume (the mask is usually channel-agnostic). Returns false for a whole-4D loader + // (NIfTI, tileDepth>1) which delivers the entire cube in one read and cannot be streamed here. + bool stream_volume_planes (size_t channel, size_t timeframe, size_t mask_timeframe, + const std::function& int_plane, const std::vector& seg_plane)>& sink); + const std::vector& get_int_tile_buffer(); const std::vector& get_seg_tile_buffer(); const std::shared_ptr>& get_seg_tile_sptr(); @@ -96,5 +106,11 @@ class ImageLoader void assemble_volume (AbstractTileLoader* fl, std::shared_ptr>& ptr, std::vector& dst, size_t channel, size_t timeframe); + + // Assemble a single global Z-plane 'gz' (X*Y) into 'dst_plane' for a per-plane loader + // (tileDepth==1). The streaming counterpart of assemble_volume's inner plane fill. + void assemble_one_plane (AbstractTileLoader* fl, + std::shared_ptr>& ptr, + std::vector& dst_plane, size_t gz, size_t channel, size_t timeframe); }; diff --git a/src/nyx/omezarr.h b/src/nyx/omezarr.h index cc2763f33..1a0efd7b3 100644 --- a/src/nyx/omezarr.h +++ b/src/nyx/omezarr.h @@ -227,28 +227,40 @@ class NyxusOmeZarrLoader : public AbstractTileLoader if (pixel_col_index + data_width > full_width_) { data_width = full_width_ - pixel_col_index; } + // A chunk may span several Z-planes (tile_depth_ > 1); read its whole Z-extent in one + // subarray call, clamped at the last (possibly partial) chunk. + size_t data_depth = 1; + if (iz_ >= 0) { + data_depth = tile_depth_; + if (pixel_layer_index + data_depth > full_depth_) + data_depth = full_depth_ - pixel_layer_index; + } // Create a buffer to hold the read data - std::vector buffer(data_height * data_width); + std::vector buffer(data_height * data_width * data_depth); // Build the read window by axis ROLE (honoring the resolved 'axes' order), - // reading one Y*X plane at the requested Z/C/T. z5 3.0.1 uses ArrayView. + // reading a Z*Y*X block (data_depth Z-planes) at the requested C/T. z5 3.0.1 uses ArrayView. z5::types::ShapeType shape(ndim_, 1), offset(ndim_, 0); shape[iy_] = data_height; offset[iy_] = pixel_row_index; shape[ix_] = data_width; offset[ix_] = pixel_col_index; - if (iz_ >= 0) offset[iz_] = pixel_layer_index; + if (iz_ >= 0) { shape[iz_] = data_depth; offset[iz_] = pixel_layer_index; } if (ic_ >= 0) offset[ic_] = pixel_channel_index; if (it_ >= 0) offset[it_] = pixel_timeframe_index; auto view = z5::multiarray::makeView(buffer.data(), shape); - + // Read subarray from the cached z5 dataset z5::multiarray::readSubarray(*ds_, view, offset.begin()); - - // Copy from buffer to destination tile, handling partial tiles - for (size_t k = 0; k < data_height; ++k) { - std::copy(buffer.begin() + k * data_width, - buffer.begin() + (k + 1) * data_width, - dest->begin() + k * tile_width_); + + // Copy from buffer to destination tile, handling partial tiles and multiple Z-planes + // (dest is laid out plane-major: plane p, row k at offset (p*tile_height_ + k)*tile_width_, + // matching ImageLoader::assemble_volume's/RawImageLoader::for_each_voxel's expected stride). + for (size_t p = 0; p < data_depth; ++p) { + for (size_t k = 0; k < data_height; ++k) { + std::copy(buffer.begin() + (p * data_height + k) * data_width, + buffer.begin() + (p * data_height + k) * data_width + data_width, + dest->begin() + (p * tile_height_ + k) * tile_width_); + } } } diff --git a/src/nyx/phase3.cpp b/src/nyx/phase3.cpp index 8650a1877..86201fd5e 100644 --- a/src/nyx/phase3.cpp +++ b/src/nyx/phase3.cpp @@ -99,7 +99,9 @@ namespace Nyxus try { const Fsettings& s = env.get_feature_settings (typeid(f)); - f->osized_scan_whole_image (r, s, env.theImLoader); + // Pass the Dataset so intensity/histogram osized features reach their + // Dataset-aware osized_calculate; the Dataset-less overload is a guard that throws. + f->osized_scan_whole_image (r, s, env.dataset, env.theImLoader); } catch (std::exception const& e) { diff --git a/src/nyx/phase3_3d.cpp b/src/nyx/phase3_3d.cpp new file mode 100644 index 000000000..05971252f --- /dev/null +++ b/src/nyx/phase3_3d.cpp @@ -0,0 +1,187 @@ +#include +#include + +#ifdef WITH_PYTHON_H + #include +#endif + +#include "environment.h" +#include "feature_mgr.h" +#include "globals.h" +#include "features/3d_intensity.h" +#include "features/3d_surface.h" +#include "features/3d_glcm.h" +#include "features/3d_gldm.h" +#include "features/3d_ngldm.h" +#include "features/3d_ngtdm.h" +#include "features/3d_glrlm.h" +#include "features/3d_glszm.h" +#include "features/3d_gldzm.h" +#include "features/pixel.h" +#include "helpers/timing.h" + +namespace Nyxus +{ + /// @brief Returns true if 'f' is one of the 3D feature classes whose osized_calculate() streams + /// from the disk-backed voxel cloud (r.raw_voxels_NT) instead of reading the in-memory cube. + /// Every other 3D FeatureMethod (e.g. a future feature added without a streaming path) would + /// silently read an empty cube if allowed through, so processNontrivialRois_3D()'s per-feature + /// guard calls this and fails loudly on false. Kept as a standalone function (rather than inline + /// in the guard) so the allow-list itself is unit-testable independent of the streaming plumbing. + bool is_3d_ooc_supported (FeatureMethod* f) + { + return dynamic_cast(f) != nullptr + || dynamic_cast(f) != nullptr + || dynamic_cast(f) != nullptr + || dynamic_cast(f) != nullptr + || dynamic_cast(f) != nullptr + || dynamic_cast(f) != nullptr + || dynamic_cast(f) != nullptr + || dynamic_cast(f) != nullptr + || dynamic_cast(f) != nullptr; + } + + /// @brief Populates r.raw_voxels_NT by streaming the volume plane-by-plane. When 'wholevolume' + /// is false (the segmented-ROI case), only voxels whose mask value equals r.label are kept -- + /// segPlane must be non-empty (a real mask loader). When 'wholevolume' is true, every voxel is + /// kept regardless of any mask (there is none: workflow_3d_whole.cpp opens the loader with an + /// empty label path, so ImageLoader::stream_volume_planes's segPlane is empty in that case). + /// Shared by the segmented (processNontrivialRois_3D) and whole-volume out-of-core paths so + /// both stream through the exact same primitive. Returns false (cloud cleared) if the loader + /// can't deliver the volume plane-by-plane (e.g. NIfTI, a whole-4D-in-one-read format). + bool populate_3d_voxel_cloud (ImageLoader& imlo, LR& r, size_t channel, size_t timeframe, bool wholevolume) + { + r.raw_voxels_NT.init (r.label, "raw_voxels_NT"); + + const size_t W = imlo.get_full_width(), + H = imlo.get_full_height(); + const uint32_t want = (uint32_t) r.label; + + bool streamed = imlo.stream_volume_planes (channel, timeframe, timeframe, + [&](size_t z, const std::vector& intPlane, const std::vector& segPlane) + { + r.raw_voxels_NT.begin_slab (z); + for (size_t y = 0; y < H; y++) + for (size_t x = 0; x < W; x++) + { + size_t i = y * W + x; + if (wholevolume || segPlane[i] == want) + r.raw_voxels_NT.add_voxel (Pixel3(x, y, z, intPlane[i])); + } + r.raw_voxels_NT.end_slab (z); + }); + + if (! streamed) + r.raw_voxels_NT.clear(); + return streamed; + } + + /// @brief Runs every requested feature's out-of-core path over an already-populated + /// r.raw_voxels_NT, guarded by is_3d_ooc_supported(); writes results into r.fvals via + /// save_value(). Shared by the segmented and whole-volume out-of-core paths. 'imloader' is + /// passed through to match FeatureMethod's signature but is not read by any of the streaming + /// 3D features (they read raw_voxels_NT exclusively), so either path's own loader instance works. + void run_3d_ooc_features (Environment& env, LR& r, ImageLoader& imloader) + { + int nrf = env.theFeatureMgr.get_num_requested_features(); + for (int i = 0; i < nrf; i++) + { + auto f = env.theFeatureMgr.get_feature_method (i); + + try + { + // Every 3D feature that streams from raw_voxels_NT is covered by + // is_3d_ooc_supported(); anything else would silently read an empty cube, so fail + // loudly per-feature instead of emitting wrong values. Under the CLI this logs and + // moves on (the supported features still compute); under Python it raises. + if (! is_3d_ooc_supported (f)) + throw std::runtime_error("feature '" + f->feature_info + + "' is not yet supported out-of-core for oversized 3D ROIs; " + + "segment into smaller ROIs, raise --ramLimit, or add RAM"); + + const Fsettings& s = env.get_feature_settings (typeid(f)); + f->osized_scan_whole_image (r, s, env.dataset, imloader); + } + catch (std::exception const& e) + { + std::string erm = "Error while computing feature " + f->feature_info + " over oversized 3D ROI " + std::to_string(r.label) + " : " + e.what(); +#ifdef WITH_PYTHON_H + throw std::runtime_error(erm); +#endif + std::cerr << erm << "\n"; + } + + f->cleanup_instance(); + } + } + + /// @brief Processes oversized volumetric (3D) ROIs out-of-core: streams the voxel cloud to + /// disk one Z-plane at a time (bounded memory) instead of holding the whole cube. + /// @param nontrivRoiLabels Labels of ROIs whose in-memory footprint exceeds the RAM limit + /// @param intens_fpath Intensity image path + /// @param label_fpath Mask image path + /// @param channel Intensity channel being featurized + /// @param timeframe Timeframe being featurized (used for both intensity and mask) + /// @return Success status + /// + bool processNontrivialRois_3D (Environment& env, const std::vector& nontrivRoiLabels, const std::string& intens_fpath, const std::string& label_fpath, size_t channel, size_t timeframe) + { + // Sort labels for reproducibility with the trivial counterpart + auto L = nontrivRoiLabels; + std::sort (L.begin(), L.end()); + + for (auto lab : L) + { + LR& r = env.roiData[lab]; + + VERBOSLVL1 (env.get_verbosity_level(), std::cout << "processing oversized 3D ROI " << lab << "\n"); + + // Scan one label-intensity pair + SlideProps p (intens_fpath, label_fpath); + // Preserve the scanned HU-domain slide min/max + flag so load-time HU offset matches the + // prescan (bare SlideProps defaults min to -1). Guarded on preserve_hu so the default path + // is unchanged. Mirrors the 2D processNontrivialRois. + if (env.fpimageOptions.preserve_hu() && r.slide_idx >= 0) + { + const SlideProps& scanned = env.dataset.dataset_props [r.slide_idx]; + p.min_preroi_inten = scanned.min_preroi_inten; + p.max_preroi_inten = scanned.max_preroi_inten; + p.preserve_hu = scanned.preserve_hu; + } + if (! env.theImLoader.open(p, env.fpimageOptions)) + { + std::cout << "Terminating\n"; + return false; + } + + // Populate the ROI's disk-backed voxel cloud by streaming the volume plane-by-plane. + // Only this ROI's label voxels are written; z is preserved. Peak memory is two X*Y + // planes (intensity + mask) plus this plane's ROI voxels, never the whole cube. + if (! populate_3d_voxel_cloud (env.theImLoader, r, channel, timeframe, /*wholevolume=*/ false)) + { + std::string erm = "Error: out-of-core featurization of oversized 3D ROI " + std::to_string(r.label) + + " is not supported for this input format (the volume is not delivered plane-by-plane). " + + "Segment into smaller ROIs, raise --ramLimit, or add RAM."; +#ifdef WITH_PYTHON_H + throw std::runtime_error(erm); +#endif + std::cerr << erm << "\n"; + continue; + } + + //=== Reduce features over the streamed voxel cloud + run_3d_ooc_features (env, r, env.theImLoader); + + //=== Clean the ROI's cache + r.raw_voxels_NT.clear(); + + #ifdef WITH_PYTHON_H + // Allow keyboard interrupt + if (PyErr_CheckSignals() != 0) + throw pybind11::error_already_set(); + #endif + } + + return true; + } +} diff --git a/src/nyx/raw_omezarr.h b/src/nyx/raw_omezarr.h index 9c5dd42fc..c795dea33 100644 --- a/src/nyx/raw_omezarr.h +++ b/src/nyx/raw_omezarr.h @@ -137,8 +137,9 @@ class RawOmezarrLoader: public RawFormatLoader else if (dtype_str == " (tile_height_ * tile_width_); + // The buffer holds the full chunk depth (tile_depth_ planes), so callers iterating + // pz in [0, tileDepth()) index valid data. + dest = std::vector (tile_height_ * tile_width_ * tile_depth_); } ~RawOmezarrLoader() override @@ -224,30 +225,42 @@ class RawOmezarrLoader: public RawFormatLoader if (pixel_col_index + data_width > full_width_) { data_width = full_width_ - pixel_col_index; } + // A chunk may span several Z-planes (tile_depth_ > 1); read its whole Z-extent in one + // subarray call, clamped at the last (possibly partial) chunk. + size_t data_depth = 1; + if (iz_ >= 0) { + data_depth = tile_depth_; + if (pixel_layer_index + data_depth > full_depth_) + data_depth = full_depth_ - pixel_layer_index; + } // Create a buffer to hold the read data - std::vector buffer(data_height * data_width); + std::vector buffer(data_height * data_width * data_depth); // Build the read window by axis ROLE (honoring the resolved 'axes' order), - // reading one Y*X plane at the requested Z/C/T. z5 3.0.1 uses ArrayView. + // reading a Z*Y*X block (data_depth Z-planes) at the requested C/T. z5 3.0.1 uses ArrayView. z5::types::ShapeType shape(ndim_, 1), offset(ndim_, 0); shape[iy_] = data_height; offset[iy_] = pixel_row_index; shape[ix_] = data_width; offset[ix_] = pixel_col_index; - if (iz_ >= 0) offset[iz_] = pixel_layer_index; + if (iz_ >= 0) { shape[iz_] = data_depth; offset[iz_] = pixel_layer_index; } if (ic_ >= 0) offset[ic_] = pixel_channel_index; if (it_ >= 0) offset[it_] = pixel_timeframe_index; auto view = z5::multiarray::makeView(buffer.data(), shape); - + // Read subarray from the cached z5 dataset z5::multiarray::readSubarray(*ds_, view, offset.begin()); - + // zero-fill the buffer foreseeing its partial filling at incomplete (tail) tiles std::fill(dest.begin(), dest.end(), 0); - - // Copy from buffer to destination tile, handling partial tiles and type conversion - for (size_t k = 0; k < data_height; ++k) { - for (size_t j = 0; j < data_width; ++j) { - dest[k * tile_width_ + j] = static_cast(buffer[k * data_width + j]); + + // Copy from buffer to destination tile, handling partial tiles, multiple Z-planes and + // type conversion (dest is plane-major: plane p, row k at (p*tile_height_+k)*tile_width_). + for (size_t p = 0; p < data_depth; ++p) { + for (size_t k = 0; k < data_height; ++k) { + for (size_t j = 0; j < data_width; ++j) { + dest[(p * tile_height_ + k) * tile_width_ + j] = + static_cast(buffer[(p * data_height + k) * data_width + j]); + } } } } diff --git a/src/nyx/roi_cache.h b/src/nyx/roi_cache.h index e01b6ac43..6116fada5 100644 --- a/src/nyx/roi_cache.h +++ b/src/nyx/roi_cache.h @@ -7,6 +7,7 @@ #include "features/aabb.h" #include "features/image_matrix.h" #include "features/image_matrix_nontriv.h" +#include "features/voxel_cloud_nontriv.h" #include "features/image_cube.h" #include "features/pixel.h" #include "featureset.h" @@ -50,6 +51,9 @@ class LR: public BasicLR std::unordered_map> zplanes; OutOfRamPixelCloud raw_pixels_NT; + // Disk-backed 3D voxel source for oversized volumetric ROIs (populated by the 3D + // out-of-core scan). Keeps z, unlike raw_pixels_NT which is the 2D (z-less) cloud. + OutOfRamVoxelCloud raw_voxels_NT; unsigned int aux_area = 0; PixIntens aux_min, aux_max; diff --git a/src/nyx/workflow_3d_segmented.cpp b/src/nyx/workflow_3d_segmented.cpp index 3b382d607..f749f53e9 100644 --- a/src/nyx/workflow_3d_segmented.cpp +++ b/src/nyx/workflow_3d_segmented.cpp @@ -131,7 +131,9 @@ namespace Nyxus if (nontrivRois.size()) { VERBOSLVL2 (env.get_verbosity_level(), std::cout << "Processing oversized ROIs\n";) - processNontrivialRois (env, nontrivRois, intens_fpath, label_fpath); + // Volumetric out-of-core path: streams the voxel cloud to disk keeping z (the shared 2D + // processNontrivialRois scans a single plane and drops z). t_index/channel select the plane. + processNontrivialRois_3D (env, nontrivRois, intens_fpath, label_fpath, channel, t_index); } return true; diff --git a/src/nyx/workflow_3d_whole.cpp b/src/nyx/workflow_3d_whole.cpp index 0ff28ca08..c2fbc06d0 100644 --- a/src/nyx/workflow_3d_whole.cpp +++ b/src/nyx/workflow_3d_whole.cpp @@ -37,7 +37,11 @@ namespace Nyxus const std::string& ifpath = env.dataset.dataset_props[sidx].fname_int; // can we process this slide ? - size_t footp = vroi.get_ram_footprint_estimate (1); // 1 since single-ROI + // FIX: use the 3D estimator (W*H*D). The 2D one counts only W*H for the image matrix, + // so it under-counts a whole VOLUME's cube by a factor of the depth -> the oversized + // check let too-large volumes through and they OOM'd. The segmented path already uses + // get_ram_footprint_estimate_3D; match it. (Found by running under a hard memory cap.) + size_t footp = vroi.get_ram_footprint_estimate_3D (1); // 1 since single-ROI if (footp > memory_limit) { std::string erm = "Error: cannot process slide " + ifpath + " , reason: its memory footprint " + virguler_ulong(footp) + " exceeds available memory " + virguler_ulong(memory_limit); @@ -117,22 +121,42 @@ namespace Nyxus vroi.initialize_fvals(); // assess ROI's memory footprint and check if we can featurize it as phase 2 (trivially) ? - size_t roiFootprint = vroi.get_ram_footprint_estimate (1), // 1 since single-ROI + // FIX: 3D estimator (W*H*D) -- the 2D one ignores depth and under-counts the volume cube, + // so oversized volumes slipped through and OOM'd. Matches the segmented path. + size_t roiFootprint = vroi.get_ram_footprint_estimate_3D (1), // 1 since single-ROI ramLim = env.get_ram_limit(); if (roiFootprint >= ramLim) { - VERBOSLVL2(env.get_verbosity_level(), - std::cout << "oversized slide " + VERBOSLVL1(env.get_verbosity_level(), + std::cout << "oversized whole volume " << " (S=" << vroi.aux_area << " W=" << vroi.aabb.get_width() << " H=" << vroi.aabb.get_height() << " D=" << vroi.aabb.get_z_depth() << " px footprint=" << Nyxus::virguler_ulong(roiFootprint) << " b" - << ") while RAM limit is " << Nyxus::virguler_ulong(ramLim) << "\n" + << ") while RAM limit is " << Nyxus::virguler_ulong(ramLim) + << " -- streaming out-of-core\n" ); - std::cerr << p.fname_int << ": slide is non-trivial \n"; - return false; + // Out-of-core whole volume: stream every voxel (no mask -- workflow_3d_whole.cpp opens + // the loader with an empty label path) plane-by-plane instead of holding the whole + // cube, mirroring the segmented ROI path (processNontrivialRois_3D). Only fails when + // the input format can't deliver the volume plane-by-plane (e.g. NIfTI). + if (! populate_3d_voxel_cloud (imlo, vroi, channel, timeframe, /*wholevolume=*/ true)) + { + std::string erm = "Error: cannot featurize whole volume " + p.fname_int + + " out-of-core: this input format does not deliver the volume plane-by-plane. " + + "Segment into smaller ROIs, raise --ramLimit, or add RAM."; +#ifdef WITH_PYTHON_H + throw std::runtime_error(erm); +#endif + std::cerr << erm << "\n"; + return false; + } + + run_3d_ooc_features (env, vroi, imlo); + vroi.raw_voxels_NT.clear(); + return true; } //***** phase 2: extract features @@ -141,7 +165,11 @@ namespace Nyxus return true; } - void featurize_3d_wv_thread( + // FIX: return the per-thread status by VALUE (through the future) rather than writing a + // referenced int. A std::async worker's write through std::ref(int) is not reliably visible + // to the caller after future::get() here, so an oversized slide's rv=1 was silently lost and + // the run reported success. Returning it makes future::get() carry the status deterministically. + int featurize_3d_wv_thread( Environment & env, const std::vector& intensFiles, const std::vector& labelFiles, @@ -149,9 +177,9 @@ namespace Nyxus size_t nf, const std::string& outputPath, bool write_apache, - Nyxus::SaveOption saveOption, - int& rv) + Nyxus::SaveOption saveOption) { + int rv = 0; SlideProps& p = env.dataset.dataset_props[slide_idx]; // scan one slide @@ -172,8 +200,16 @@ namespace Nyxus if (featurize_wholevolume (env, slide_idx, imlo, vroi, c, t) == false) // non-wsi counterpart: processIntSegImagePair() { - std::cerr << "Error featurizing slide " << p.fname_int << " @ " << __FILE__ << ":" << __LINE__ << "\n"; + // An oversized whole volume now streams out-of-core (see featurize_wholevolume's + // oversized branch), so featurize_wholevolume returns false only when that streaming + // itself failed -- either the input format can't deliver the volume plane-by-plane + // (e.g. NIfTI), or a requested feature isn't yet OOC-supported. That specific reason + // was already reported (thrown under Python, printed to stderr under the CLI) from + // inside featurize_wholevolume/run_3d_ooc_features. Previously we ALSO fell through + // to save the zero-initialized buffer here -- emitting a row of all-0 features, i.e. + // silently-wrong data. Skip the save instead: fail loudly, write no row. rv = 1; + continue; // do NOT emit a (misleading all-zero) row for this plane } // thread-safely save results of this single (slide, channel, timeframe) @@ -207,6 +243,7 @@ namespace Nyxus } //- channels x timeframes imlo.close(); + return rv; // // Not saving nested ROI related info because this image is single-ROI (whole-slide) @@ -285,13 +322,15 @@ namespace Nyxus // run batches of threads + int worst_rv = 0; // FIX: aggregate per-thread failure so an oversized/failed slide + // makes the whole run fail loudly (nonzero exit) instead of returning + // success -- the per-thread rvals were collected but never checked. size_t n_jobs = (nf + n_threads - 1) / n_threads; for (size_t j = 0; j < n_jobs; j++) { VERBOSLVL1 (env.get_verbosity_level(), std::cout << "whole-slide job " << j + 1 << "/" << n_jobs << "\n"); - std::vector> T; - std::vector rvals(n_threads, 0); + std::vector> T; for (int t = 0; t < n_threads; t++) { size_t idx = j * n_threads + t; @@ -311,12 +350,12 @@ namespace Nyxus nf, outputPath, write_apache, - saveOption, - std::ref(rvals[t]))); + saveOption)); } else { - featurize_3d_wv_thread( + // FIX: aggregate the returned status (was written through a lost reference) + int r = featurize_3d_wv_thread( env, intensFiles, intensFiles, @@ -324,14 +363,18 @@ namespace Nyxus nf, outputPath, write_apache, - saveOption, - rvals[t]); + saveOption); + if (r > worst_rv) worst_rv = r; } } - // wait for all threads to complete before proceeding + // wait for all threads to complete, aggregating each one's returned status so an + // oversized/failed slide makes the whole run fail loudly (nonzero exit) for (auto& f : T) - f.get(); + { + int r = f.get(); + if (r > worst_rv) worst_rv = r; + } // allow keyboard interrupt #ifdef WITH_PYTHON_H @@ -361,6 +404,15 @@ namespace Nyxus // future: free GPU cache for all participating devices // + // FIX: a slide that could not be featurized is a hard failure, not a silent success -- + // report it so the CLI exits nonzero and the Python API raises. An oversized whole volume + // streams out-of-core now (see featurize_wholevolume), so this only fires when that + // streaming itself failed (input format can't deliver plane-by-plane, or an unsupported + // feature was requested) -- the specific reason was already reported above. + if (worst_rv != 0) + return { false, "one or more slides could not be featurized (see errors above; " + "an unsupported format needs a segmented mask, or a smaller/plane-deliverable input)" }; + return {true, std::nullopt}; // success } diff --git a/src/nyx/workflow_pythonapi.cpp b/src/nyx/workflow_pythonapi.cpp index 33dcd4035..625359d7b 100644 --- a/src/nyx/workflow_pythonapi.cpp +++ b/src/nyx/workflow_pythonapi.cpp @@ -33,7 +33,10 @@ namespace Nyxus { bool scan_slide_props_montage(SlideProps& p, int dim, const AnisotropyOptions& aniso); - bool processIntSegImagePairInMemory (Environment & env, const py::array_t& intens, const py::array_t& label, int pair_index, const std::string& intens_name, const std::string& seg_name, std::vector unprocessed_rois) + // unprocessed_rois is an OUT parameter: the in-memory montage pipeline has no out-of-core + // path, so ROIs whose footprint reaches the RAM limit are returned to the caller to be + // reported as an error, instead of being dropped and emitting an all-zero feature row. + bool processIntSegImagePairInMemory (Environment & env, const py::array_t& intens, const py::array_t& label, int pair_index, const std::string& intens_name, const std::string& seg_name, std::vector& unprocessed_rois) { std::vector trivRoiLabels; diff --git a/tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/0/0/0/0 b/tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/0/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..45d5baea3b5b063e0340e21cd61921b48252b5c5 GIT binary patch literal 3588 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 Q{@*J2&kDcaF8|;E0{|effB*mh literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/0/0/0/1 b/tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/0/0/0/1 new file mode 100644 index 0000000000000000000000000000000000000000..404d4508b587102189aaf661f257d47feb5cb6d0 GIT binary patch literal 3588 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 N7ALd%=s*8X{THW?sTH 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 GfBy%AzCe!v literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/1/0/0/1 b/tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/1/0/0/1 new file mode 100644 index 0000000000000000000000000000000000000000..56392de8b24f2ede5a12185284816174064a74ca GIT binary patch literal 3588 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 literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/1/0/1/0 b/tests/data/omezarr/dim5_v3_multishard.ome.zarr/0/c/0/1/0/1/0 new file mode 100644 index 0000000000000000000000000000000000000000..bc4ed732bc9fd9395afc81e66453526b255d90c0 GIT binary patch literal 3588 zcmZ9ERdQds(?G|Loy2yWMH7}qmDZ2EMdY7OPDaj62{E1gfTO0;Hw%l!xF~K zu!Qg7Kl6~EURtM5oi0ntlZvD=sYWnH-h3c$2r_QS@$|_J-l~bs$DX+@X^0XqYOsmrB zv?i@h+tT*5BkfGP((be;?M=th@pK}cOsCT6bS9lm*V6TLBi&55((QC7-A(g!zCNQ1 zbfG@0&*}5}inbc`RqZtDYuf8u>bAP0?y7t0zIvb@s#of@dZXT|cj~?RpgyWHRjw*j zrK(cZsz%kSHr1{=RHy1v-Kt0RsxdXLCe);wQqyWi&8juEt~S)B+EUwUNA0Rx`nJBK z@9KN{zJ8z|>R0-;exu*&cly2lpg-y|U9Ky1rLNM|x<=RPHr=i}bf@mp-MUBj>M=d8 zC-kJA($jiI&+0Y3t~d0i-qPE8NAK#oq&{g#8k44^IcZ5+lfI-s8At|`p=3B2Nk)^o zWIkC)7L%o9Iax_olf7g=IY5m(HgP>0-K+E~hK$YPy&1rw8d_dXyfg zC+TTgq>FWlzMxC>MSV$M)}*iNpjqG0QH#E*AF0RciF&G@spsm2dZ|9C&+3c%s=lf3 z>WBKN>QudIP>rfdHLDiYs`^yF8c>63NDZqIHLB*+yjoC;YDq1t6}77N)V?}Uhw4Zj zs}psq9_h#WiGHe|>F4@|eyKm{&-#o0s=w*)`iK6h>vX+t(2crDH|rMNs{3@m9?*k& zNDu1~J*wyQyk5|YdPy(q6}_tW^u9jOhx$k#>l1yd%d+yUBCE`*vg)iRtIgW7_N*i8 z%(}AftS9Tu#C%(k-aY$w~z@=U%tV+u^6Icv_D^X7`N z2Fz9C44P}kn=)H&D{Q5$vemZ6*4j4PZaZwJ?Xumr$M)JWJ8mcJq@A+UcE--yHM?#% z?55qa+jht9+C0dIGf)79a2C$NdAI@=0Iq@qglpj8mbq>2n7ihlxo;krhvt=eZQhu- z=AC(OKA4ZD%#@o7Q)#M9wW%?+rp>gQ4%2D6Ot%rp&aNF|%gPteXw9 zX|~L^*)hB37TktAa2M{ueRu#5;T61wH}DqT!F%`sAE6A&p#mzQ3aX(7YM~9E1VFNZ{3$|egcA+k-&lFWb)!vcv2sJI+qB)2zr8n-X)ul$wj?lDTZiTsOh6 zxnZIabJNt>dfQ+dZIf-bEwEI@UA8NB)$ZASdteXk zkv+C2_S6$nmSW&8cd^UGR>yNw3=M=&{U~ z`wCy_t9-Su@wL9qxBCv?>AQTl@A18U%#ZsCKk29Zw4d>_e$B7@4ZrEP{I=imyFQQd z=?oQ6A)Tdjbe^t|C7`S12`vUNE5c$d!3$W57x5BaM#AeDknsja6ugObzTP+ZM&IO{eT#4PeZJoh z_(4D9hy92j^>cpSFZe~jw-i9hv4R7@pwflBEjU82iGbe#e* z-JnQ9H|Y^R#wYj`pW$3|ODh>q!mPN^)E zhl)@cszPQl?~DW@J{@WL-97Q?_JVc4SxTLVaimjiD(thnCP9`a*vg z2!mlL42O|08s@@$SO|+@DJ+MTup0KlemDq+;V2x3lW-b}xR^`$0+;edzQmWA_&Nt> zzQK`&Z*pC%j}5UgHpS-H5?f7xI1)$WT%3;!aWO8%<+u`8<6hj42k|f- z#p8GqPh*i3ONm^NQn@IX4`3ryLZ~UEq z@K3JedT!uGZsKNc;a2YBejeaK9^zph;ZdIBd0yZ}UgBk5;Z@$_eLmnrKH_6O;ZuGj zkL8Ixm1pu?UdT)NB%kGre3ftVU4F<63mLkU<%eVHuH8nUi^0 zkVRRNWm%C`*^_-akV83=V>yvi`R(8L$G`K>y#IavttQ5oAt0>*2ntUIGbRTY>GwLG@D_wY?&q4 z3R`7sEXmf{20LJBcF2y{F*{)ycFJzSZMXw>;U3(FJa_;P;SoHBC-4-WK_L`D2k+!v zyqov%Uf#$1`8c28lYELt_%xs4vwWE+_zGX;Ydp!<`367WX@1C$_%T1>8Gg!d;cdKw zckv$H$2@$15AhK`#wYj`pJ5>uVHLc9YN&x)cnNh-4^8j^nxO?+p$$I5C+L7q=z?zO zfnMl?ei(-dn1m^az%oWliN!WCrU8bbN6@HM`{xA+dZLyFr*WE~Nt&VvP16j`(lRAzg;r^elC(}6 zbUC9pK`|tT#fTUcV23vnr~L{?mj5HSMECr(HOB#5Y(N~n~| zsGOct1y$03Gw2LC!_J5^>Wn#IXU>^-qRxU7a~7Q?C+=)ITh6w#)i`lk+kv7i3H> z$|V_>n{rET%N@BZ_vF4z$uoH_FXW}Xl395zLx1mZJKZj~+wF0C-9ES99d{?(Nq5SP zxYO>8JL@jH33tU^b=TaayY6nd2X5LubdTI)_r%S(r|vCvTisE2)jf4z<*5hip?ahq zt0(HIdZr3hk?Qa|y)LiY>+yQMKCj;!_a?kaZ_0~!)833X>n(c;Z^c{n*1V**?rnGn zUfMhKj=W><#LIZ6-YtDw-_dvVJ$+y2=?D6uexx7kC;F*=rVDkEu2L^lwW?9I>ZPhv z^{PpIP|d1EwW>DtQGHS!s#A5TZq=iDRiElt<7z@pswovw(`rV|s%4c>D{57(siaz0 z8|px%)uB34$Ld67)Tye{FLbr8(Y5-euG96pNq^AIx<$9@HvLh5(jB@}cj<23qkDCq z?$_gbLQm=`9nsTzM$hVHozN?KRj=u!Ue_D?K&SPgKGMhfL}&D=9&iWUA$QmvaYx-T zH|);2^KR5#aAWSGyX3~*O?S)Pc6Z!echB8-Q|_62?q0Z;?v^h_ey%HYrGBMet2gScdZ*s2 z2GyuOt1s%S`li0CAL^%SR|9HL4XI%@qDIx23adFaucB%}#nhr&QgOAZw$!%TQM+nS z?W>eJQ|IbJU8*aURo5!?k5~G&exu*&cly0<(2e@D{-VF?Z~D9bp?~UjJ)j5mkRH|} zdQ^|;u%6TNI;t0ROfTvs9oL(BOK9bKm5d2j-!9WFDI*=BasR3Qdve2s(qVpgZUZdV{{8KNt@tg2`Yihy>HYOfVZP z2Z>-MSPj;KWUwA=1P4JnI1G-0|>!lQ1i0)vTGMSvMQzz@*KgIWouQ#AM8=sj@F@wXLzW_NA?}^|r}=u+6r`w%RuP z(SEWWw$pamZrfvfZJ+J8<95PM+9?~c({{$r+GU%tD|Xec*`!^!8}`7a?V&xg$M(c# z?5Q2_2mK*`*dOsn{V_l6&-wFy)L-yp{-VF+$Nf!z%is2Q{9S*~-}h7gnSbtI_?P~b zpY^Z(kYNT)zTpNX3z|oVKZVz&6o+BIWupfX2HbF zqFFL=vuU=>w%IYeX3y-KlsPl!=E7W>E0ZIstVOFf?K*Vo^3p4>b?fm)uRi?-3>xy*uo0uiyfbdXq$%%Bn=xz7ybnJ5tF8p=r%0Jg`+`980gJCGi literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/0/1/0/0 b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/0/1/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..8367b807b964ef802116ab84649257b8a44c86bf GIT binary patch literal 288 zcmV~$hawOF00qE%EooA;w0=3!kdh=7B`SpW&`v`rg+zsxh<10ol-sRBr9JlAXTK^3 zR6FR98i&<7qRvtEj%je*2`4pba>{AV&S=row{_p=#tB>xaykgZn)`| z9=F|b*FE<=@X#Z@9(&@cXZk$XZ@{1zUK;YsYr{s2dSlF6<0icG-UpMWO#A4Q&%XF- z#y7L(d^c~w4?q3#+aG`ZBgvvH&XO$6vJ_-_3bP_Bvns2zCTmla;;hU1l%zBpvN4;o QIa^Yet=X3C*^!<34+$7p>Hq)$ literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/0/2/0/0 b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/0/2/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..466ed4ddd65c60280e324a56a2e91d4f11f19adc GIT binary patch literal 288 zcmdVH2Lgcr002Pu6d9Eg%9fCs{r}f^hr6~wM^{f@C=we;q=qshV-vZ;)Xdz%(#qOK bX{)lccThVzIlH*JY1}-^U8c literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/0/0/0 b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/0/0/0 new file mode 100644 index 000000000..8fa9f3f9a --- /dev/null +++ b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/0/0/0 @@ -0,0 +1 @@ +QRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ \ No newline at end of file diff --git a/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/1/0/0 b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/1/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..ba5e40e9e4d122f31ebb241c0b60027119bf1234 GIT binary patch literal 288 zcmV~$he8Md00ltrWGk{o8I{behOBHFMs}f4lw|yJR7#RW5yjnLKICJDGMtfo%4o*& zIpg_~iG0muzGW)kGo2s#nVHPySAJ(MfATl;S;)UE5-e6=iKUiVZiSUrS*=i!HP$M& z&Uzbcw8>^$Y*k{L?Mju|VW(YoD_5b?9#!_*XTJjuI;7fRM;vv`aW!hysdqwylTJD9 zjI$b@bKV6PU2@qKS6$PjS<J-Eh+_ZEm~cu6yo#;GuRM9(nAEPF literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/2/0/0 b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/c/0/1/2/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..a49e44443422bd85bd63b26bc1ba77af0c1ec0d2 GIT binary patch literal 288 zcmdVH2@-(-002O7jFu-g{s=<4YUMPdVq)KF$*Y$8{fnweWzT3OpD bZB=&m4r)gyXBSsDjk|}Zm$#3v)}Q+UET|dz literal 0 HcmV?d00001 diff --git a/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/zarr.json b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/zarr.json new file mode 100644 index 000000000..32c56dcb8 --- /dev/null +++ b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/0/zarr.json @@ -0,0 +1,41 @@ +{ + "shape": [ + 1, + 2, + 7, + 6, + 8 + ], + "data_type": "uint16", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 1, + 1, + 3, + 6, + 8 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/zarr.json b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/zarr.json new file mode 100644 index 000000000..ea6be0fcd --- /dev/null +++ b/tests/data/omezarr/dim5_v3_zchunked.ome.zarr/zarr.json @@ -0,0 +1,52 @@ +{ + "attributes": { + "ome": { + "version": "0.5", + "multiscales": [ + { + "axes": [ + { + "name": "t", + "type": "time" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space" + }, + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/tests/data/omezarr/gen_dim5.py b/tests/data/omezarr/gen_dim5.py index aa42dfb4f..254f0305f 100644 --- a/tests/data/omezarr/gen_dim5.py +++ b/tests/data/omezarr/gen_dim5.py @@ -171,6 +171,83 @@ def write_v3_sharded(name, order): print("wrote %-24s (Zarr v3 SHARDED, inner=%s shard=%s)" % (name, inner, shards)) +def write_v3_multishard(name): + """A LARGER Zarr v3 sharded store exercising MULTIPLE SPATIAL SHARDS per (c,t) plane-volume -- + the existing dim5_v3_sharded fixture has exactly one shard per (t,c) (the shard's y/x extent + equals the full plane), so it only ever proves multiple INNER CHUNKS packed into one shard + file. This fixture instead splits each (t,c) volume into a 2x2 grid of shards across Y and X, + so the volumetric assembly must cross shard-FILE boundaries mid-plane (not just inner-chunk + boundaries within one shard) -- closer to how a real, larger Axle v3 store is laid out. + + Own local dims (not the module T/C/Z/Y/X): C=2, T=1, Z=8, Y=24, X=32. + Inner chunk (z,y,x)=(1,6,8) -- z-chunk MUST stay 1: this codebase's OME-Zarr readers + (raw_omezarr.h/omezarr.h) read exactly one Z-plane per tile regardless of what + tileDepth() reports (see this file's module docstring: "chunked one z-slice per + chunk"); a z-chunk>1 silently under-reads (proven while building this fixture -- + a z=2 inner chunk read only its first z-plane and left the rest zero). That is a + separate, real chunking-granularity gap from the shard-FILE-crossing behavior this + fixture targets, so it's dodged here rather than fixed as a drive-by. + Shard (z,y,x)=(8,12,16) -> covers the full Z depth (all 8 inner z-chunks) but HALF of + Y and HALF of X -> a 2x2 shard grid per (c,t), each shard packing 8*(12/6)*(16/8)=32 + inner chunks. Total: C*T*2*2 = 8 shard FILES, and the volumetric assembly must cross + shard-FILE boundaries mid-plane (not just inner-chunk boundaries within one shard). + Same coordinate encoding as the rest of this file: value(x,y,z,c,t) = 1 + ((((t*C+c)*Z+z)*Y+y)*X+x). + """ + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + + C, T, Z, Y, X = 2, 1, 8, 24, 32 + t, c, z, y, x = np.meshgrid( + np.arange(T), np.arange(C), np.arange(Z), np.arange(Y), np.arange(X), indexing="ij") + data = (1 + ((((t * C + c) * Z + z) * Y + y) * X + x)).astype("uint16") # [t,c,z,y,x] + + inner = (1, 1, 1, 6, 8) + shards = (1, 1, 8, 12, 16) + + g = zarr.open_group(path, mode="w", zarr_format=3) + a = g.create_array("0", shape=data.shape, dtype=data.dtype, + chunks=inner, shards=shards, compressors=None) + a[:] = data + g.attrs["ome"] = {"version": "0.5", + "multiscales": [{"axes": [_AX[ax] for ax in "tczyx"], + "datasets": [{"path": "0", + "coordinateTransformations": [{"type": "scale", "scale": [1.0] * 5}]}]}]} + print("wrote %-24s (Zarr v3 MULTI-SHARD: C=%d T=%d Z=%d Y=%d X=%d, inner=%s shard=%s, %dx%d shard grid per c,t)" + % (name, C, T, Z, Y, X, inner, shards, -(-Y // shards[3]), -(-X // shards[4]))) + + +def write_v3_zchunked(name): + """A Zarr v3 store with a MULTI-PLANE Z chunk (chunk z-extent > 1), unsharded -- isolates the + Z-chunking behavior from shard-file addressing (see write_v3_multishard for that). Every other + fixture in this file chunks exactly one Z-slice per chunk (see the module docstring); this one + deliberately doesn't, to catch readers that silently under-read past the first Z-plane of a + multi-plane chunk. + + Own local dims: C=2, T=1, Z=7, Y=6, X=8. Chunk (z,y,x)=(3,6,8) -> 3 Z-chunks of depth 3,3,1 -- + the UNEVEN split (Z=7 not a multiple of 3) also exercises the partial last-chunk clamp. + Same coordinate encoding as the rest of this file: value(x,y,z,c,t) = 1 + ((((t*C+c)*Z+z)*Y+y)*X+x). + """ + path = os.path.join(HERE, name) + shutil.rmtree(path, ignore_errors=True) + + C, T, Z, Y, X = 2, 1, 7, 6, 8 + t, c, z, y, x = np.meshgrid( + np.arange(T), np.arange(C), np.arange(Z), np.arange(Y), np.arange(X), indexing="ij") + data = (1 + ((((t * C + c) * Z + z) * Y + y) * X + x)).astype("uint16") # [t,c,z,y,x] + + chunks = (1, 1, 3, 6, 8) + + g = zarr.open_group(path, mode="w", zarr_format=3) + a = g.create_array("0", shape=data.shape, dtype=data.dtype, chunks=chunks, compressors=None) + a[:] = data + g.attrs["ome"] = {"version": "0.5", + "multiscales": [{"axes": [_AX[ax] for ax in "tczyx"], + "datasets": [{"path": "0", + "coordinateTransformations": [{"type": "scale", "scale": [1.0] * 5}]}]}]} + print("wrote %-24s (Zarr v3 Z-CHUNKED: C=%d T=%d Z=%d Y=%d X=%d, chunk=%s -> Z-chunk depths 3,3,1)" + % (name, C, T, Z, Y, X, chunks)) + + def write_label_mask(name): """P4/N3: a single-channel ZYX label mask (mostly background, a few small ROIs), the OME-Zarr twin of dim3_mask.ome.tif. Pairs with a T>1 Zarr intensity to exercise the @@ -247,6 +324,11 @@ def main(): # bytes+blosc v3 pipeline when built WITH_BLOSC (already required for OME-Zarr). write_v3("dim5_v3_blosc.ome.zarr", "tczyx", compressors=[BloscCodec(cname="lz4", clevel=5, shuffle="shuffle")]) + # Larger multi-shard store: a 2x2 grid of SHARD FILES per (c,t), not just multiple inner + # chunks within one shard -- exercises crossing shard-file boundaries mid-plane. + write_v3_multishard("dim5_v3_multishard.ome.zarr") + # multi-plane Z chunk (chunk z-extent 3, uneven split 3+3+1) -- isolated Z-chunking check + write_v3_zchunked("dim5_v3_zchunked.ome.zarr") # plane split across a 2x2 chunk grid (3x4 chunks over the 6x8 plane) -> multi-tile assembly write_multichunk("dim5_multichunk.ome.zarr", "tczyx", 3, 4) # PARTIAL edge chunks: chunk (4,5) does NOT divide the 6x8 plane, so the last row-chunk is diff --git a/tests/python/test_ooc_mechanics.py b/tests/python/test_ooc_mechanics.py new file mode 100644 index 000000000..3056627fa --- /dev/null +++ b/tests/python/test_ooc_mechanics.py @@ -0,0 +1,316 @@ +"""Mechanics tests for the out-of-core (oversized-ROI) path. + +An ROI whose memory footprint reaches ram_limit is streamed through the disk-backed +OutOfRamPixelCloud instead of an in-memory pixel vector, and its features are computed by +the osized_calculate path. These tests exercise that plumbing (not an external oracle): + +- test_ooc_2d_matches_in_ram: the out-of-core path must produce the same intensity features + as the in-RAM path on the same file pair. +- test_ooc_montage_oversized_fails_loudly: the in-memory montage path has no out-of-core + support and must raise on an oversized ROI rather than emit a silent all-zero row. + +ram_limit is a process-global in Nyxus, so each test sets it explicitly on both sides to stay +order-independent. +""" +import os +import pathlib +import numpy as np +import pytest + +import nyxus + +tifffile = pytest.importorskip("tifffile") + +DATA_NIFTI = pathlib.Path(__file__).resolve().parent.parent / "data" / "nifti" + + +def _make_pair(tmp_path): + # Deterministic, non-degenerate: per-row offset + per-column gradient. 500x500 gives an + # in-memory footprint well above 1 MB, so ram_limit=1 forces the out-of-core path. + Y, X = 500, 500 + xg = (np.arange(X) % 256).astype(np.uint32) + yg = ((np.arange(Y) % 200) * 256).astype(np.uint32) + inten = (1 + xg[None, :] + yg[:, None]).astype(np.uint32) + mask = np.ones((Y, X), np.uint32) # a single ROI covering the whole image + intdir = tmp_path / "int" + segdir = tmp_path / "seg" + intdir.mkdir() + segdir.mkdir() + tifffile.imwrite(str(intdir / "img.tif"), inten) + tifffile.imwrite(str(segdir / "img.tif"), mask) + return str(intdir) + os.sep, str(segdir) + os.sep + + +def _feature_cols(df): + cols = [ + c + for c in df.select_dtypes(include=[np.number]).columns + if c not in ("ROI_label", "t_index", "c_index") + ] + return cols, df[cols].to_numpy(dtype=float).ravel() + + +def test_ooc_2d_matches_in_ram(tmp_path): + intdir, segdir = _make_pair(tmp_path) + feats = ["*ALL_INTENSITY*"] + + n_ram = nyxus.Nyxus(feats) + n_ram.set_params(ram_limit=8000) # large -> in-RAM (trivial); explicit so test is order-independent + df_ram = n_ram.featurize_directory(intdir, segdir) + + n_ooc = nyxus.Nyxus(feats) + n_ooc.set_params(ram_limit=1) # 1 MB -> forces the oversized / out-of-core path + df_ooc = n_ooc.featurize_directory(intdir, segdir) + + cols, a = _feature_cols(df_ram) + _, b = _feature_cols(df_ooc) + assert a.size > 0 and a.shape == b.shape + + bad = [ + (c, p, q) + for c, p, q in zip(cols, a, b) + if abs(p - q) > 1e-6 * max(abs(p), abs(q), 1.0) + 1e-9 + ] + assert not bad, "out-of-core intensity features diverge from in-RAM: %r" % (bad[:8],) + + +def _make_volume_pair(tmp_path): + # A 3D OME-TIFF volume (one IFD per Z) + a whole-volume mask. Z*Y*X = 8*90*90 = 64800 + # voxels; the in-memory 3D footprint is well over 1 MB, so ram_limit=1 forces the oversized + # (out-of-core) volumetric path. Intensity is a deterministic, non-degenerate function of + # (x,y,z) so every 3D intensity feature is meaningful. + Z, Y, X = 8, 90, 90 + z = np.arange(Z)[:, None, None] + y = np.arange(Y)[None, :, None] + x = np.arange(X)[None, None, :] + inten = (1 + (x % 256) + (y % 200) * 256 + z * 10000).astype(np.uint32) + inten = np.broadcast_to(inten, (Z, Y, X)).astype(np.uint32) + mask = np.ones((Z, Y, X), np.uint32) # a single ROI covering the whole volume + intp = tmp_path / "vol_int.ome.tif" + segp = tmp_path / "vol_seg.ome.tif" + tifffile.imwrite(str(intp), inten, metadata={"axes": "ZYX"}) + tifffile.imwrite(str(segp), mask, metadata={"axes": "ZYX"}) + return str(intp), str(segp) + + +def test_ooc_3d_matches_in_ram(tmp_path): + """The 3D out-of-core path (voxel cloud streamed to disk, keeping z) must produce the same + 3D intensity AND surface/morphology features as the in-RAM path on the same volume pair.""" + intp, segp = _make_volume_pair(tmp_path) + feats = ["*3D_ALL_INTENSITY*", "*3D_ALL_MORPHOLOGY*"] + + # Nyxus3D takes ram_limit in the constructor (its set_params does not expose it) + n_ram = nyxus.Nyxus3D(feats, ram_limit=8000) # large -> in-RAM (trivial) + df_ram = n_ram.featurize_files([intp], [segp], False) + + n_ooc = nyxus.Nyxus3D(feats, ram_limit=1) # 1 MB -> forces the oversized / out-of-core volumetric path + df_ooc = n_ooc.featurize_files([intp], [segp], False) + + cols, a = _feature_cols(df_ram) + _, b = _feature_cols(df_ooc) + assert a.size > 0 and a.shape == b.shape + + bad = [ + (c, p, q) + for c, p, q in zip(cols, a, b) + if abs(p - q) > 1e-6 * max(abs(p), abs(q), 1.0) + 1e-9 + ] + assert not bad, "3D out-of-core intensity features diverge from in-RAM: %r" % (bad[:8],) + + +def _make_volume_pair_partial(tmp_path): + # Same intensity field as _make_volume_pair, but a non-cuboid (ellipsoid) mask that leaves + # background voxels INSIDE the ROI's bounding box. Out-of-core paths that build a grey-level + # LUT from the whole binned cube (mask + background) -- e.g. matlab binning maps the raw-0 + # background to a nonzero bin that must still appear in the LUT -- are only exercised when the + # bbox actually contains background; a whole-volume mask (hasBackground == false) never does. + Z, Y, X = 8, 90, 90 + z = np.arange(Z)[:, None, None] + y = np.arange(Y)[None, :, None] + x = np.arange(X)[None, None, :] + inten = (1 + (x % 256) + (y % 200) * 256 + z * 10000).astype(np.uint32) + inten = np.broadcast_to(inten, (Z, Y, X)).astype(np.uint32) + zz, yy, xx = np.meshgrid(np.arange(Z), np.arange(Y), np.arange(X), indexing="ij") + cz, cy, cx = (Z - 1) / 2.0, (Y - 1) / 2.0, (X - 1) / 2.0 + rz, ry, rx = Z / 2.0, Y / 3.0, X / 3.0 + inside = (((zz - cz) / rz) ** 2 + ((yy - cy) / ry) ** 2 + ((xx - cx) / rx) ** 2) <= 1.0 + mask = inside.astype(np.uint32) + intp = tmp_path / "vol_int_partial.ome.tif" + segp = tmp_path / "vol_seg_partial.ome.tif" + tifffile.imwrite(str(intp), inten, metadata={"axes": "ZYX"}) + tifffile.imwrite(str(segp), mask, metadata={"axes": "ZYX"}) + return str(intp), str(segp) + + +def _ooc_vs_ram_3d(tmp_path, feats, pair_fn=_make_volume_pair): + intp, segp = pair_fn(tmp_path) + n_ram = nyxus.Nyxus3D(feats, ram_limit=8000) + df_ram = n_ram.featurize_files([intp], [segp], False) + n_ooc = nyxus.Nyxus3D(feats, ram_limit=1) + df_ooc = n_ooc.featurize_files([intp], [segp], False) + cols, a = _feature_cols(df_ram) + _, b = _feature_cols(df_ooc) + assert a.size > 0 and a.shape == b.shape + bad = [ + (c, p, q) + for c, p, q in zip(cols, a, b) + if abs(p - q) > 1e-6 * max(abs(p), abs(q), 1.0) + 1e-9 + ] + assert not bad, "3D out-of-core features diverge from in-RAM: %r" % (bad[:8],) + + +def test_ooc_3d_glcm_matches_in_ram(tmp_path): + """3D GLCM out-of-core (13 co-occurrence matrices built over a streaming 2-plane window) + must match the in-RAM path.""" + _ooc_vs_ram_3d(tmp_path, ["*3D_GLCM*"]) + + +def test_ooc_3d_gldm_matches_in_ram(tmp_path): + """3D GLDM out-of-core (dependence matrix built over a streaming 3-plane window) must match + the in-RAM path.""" + _ooc_vs_ram_3d(tmp_path, ["*3D_GLDM*"]) + + +def test_ooc_3d_ngldm_matches_in_ram(tmp_path): + """3D NGLDM out-of-core (3-plane window, interior scan) must match the in-RAM path.""" + _ooc_vs_ram_3d(tmp_path, ["*3D_NGLDM*"]) + + +def test_ooc_3d_ngtdm_matches_in_ram(tmp_path): + """3D NGTDM out-of-core (radius-window neighbourhood averages) must match the in-RAM path.""" + _ooc_vs_ram_3d(tmp_path, ["*3D_NGTDM*"]) + + +def test_ooc_3d_glrlm_matches_in_ram(tmp_path): + """3D GLRLM out-of-core (in-plane runs reuse gather_rl_zones on a per-plane depth-1 cube; + cross-plane runs use a 2-plane carry) must match the in-RAM path.""" + _ooc_vs_ram_3d(tmp_path, ["*3D_GLRLM*"]) + + +def test_ooc_3d_glszm_matches_in_ram(tmp_path): + """3D GLSZM out-of-core (streaming 26-connectivity connected-component labeling via a + growable union-find over a 2-plane window) must match the in-RAM path.""" + _ooc_vs_ram_3d(tmp_path, ["*3D_GLSZM*"]) + + +def test_ooc_3d_gldzm_matches_in_ram(tmp_path): + """3D GLDZM out-of-core (streaming 6-connectivity connected-component labeling with a + min-distance-to-border metric per zone) must match the in-RAM path.""" + _ooc_vs_ram_3d(tmp_path, ["*3D_GLDZM*"]) + + +def test_ooc_3d_wholevolume_streams_oob(tmp_path): + """The WHOLE-VOLUME (single_roi) 3D path -- Nyxus3D.featurize_directory(dir, dir, ...), i.e. + label_dir==intensity_dir -- used to have NO out-of-core support at all: an oversized whole + volume failed loudly (workflow_3d_whole.cpp). It now streams via the same + populate_3d_voxel_cloud/run_3d_ooc_features primitives as the segmented path. This is the + Python-facing equivalent of the gtest TEST_3D_WHOLEVOLUME_OVERSIZED_STREAMS_OOC: force + oversized with ram_limit=0 (every footprint >= 0 is always true) and compare against an + in-RAM run of the identical volume.""" + Z, Y, X = 8, 90, 90 + z = np.arange(Z)[:, None, None] + y = np.arange(Y)[None, :, None] + x = np.arange(X)[None, None, :] + inten = (1 + (x % 256) + (y % 200) * 256 + z * 10000).astype(np.uint32) + inten = np.broadcast_to(inten, (Z, Y, X)).astype(np.uint32) + voldir = tmp_path / "wv" + voldir.mkdir() + tifffile.imwrite(str(voldir / "vol.ome.tif"), inten, metadata={"axes": "ZYX"}) + + feats = ["*3D_ALL_INTENSITY*", "*3D_ALL_MORPHOLOGY*", "*3D_GLCM*"] + + n_ram = nyxus.Nyxus3D(feats, ram_limit=8000) + df_ram = n_ram.featurize_directory(str(voldir), str(voldir), ".*") + + n_ooc = nyxus.Nyxus3D(feats, ram_limit=0) # every footprint >= 0 -> always oversized + df_ooc = n_ooc.featurize_directory(str(voldir), str(voldir), ".*") + + cols, a = _feature_cols(df_ram) + _, b = _feature_cols(df_ooc) + assert a.size > 0 and a.shape == b.shape + + bad = [ + (c, p, q) + for c, p, q in zip(cols, a, b) + if abs(p - q) > 1e-6 * max(abs(p), abs(q), 1.0) + 1e-9 + ] + assert not bad, "whole-volume out-of-core features diverge from in-RAM: %r" % (bad[:8],) + + +def _make_volume_pair_blank(tmp_path): + # A degenerate ROI (constant intensity everywhere) at the same size as _make_volume_pair, so it + # is still classified oversized at ram_limit=1 -- exercises each osized_calculate's early-return + # "blank ROI" guard (aux_min==aux_max) via the out-of-core path, which no other fixture reaches + # (every other fixture here varies intensity). Must not crash or hang, and must match the + # in-RAM path's degenerate-ROI output (STNGS_NAN defaults to plain 0.0 in this build, not IEEE + # NaN, so the existing tolerance-based comparison in _ooc_vs_ram_3d applies unchanged). + Z, Y, X = 8, 90, 90 + inten = np.full((Z, Y, X), 42, dtype=np.uint32) + mask = np.ones((Z, Y, X), np.uint32) + intp = tmp_path / "vol_int_blank.ome.tif" + segp = tmp_path / "vol_seg_blank.ome.tif" + tifffile.imwrite(str(intp), inten, metadata={"axes": "ZYX"}) + tifffile.imwrite(str(segp), mask, metadata={"axes": "ZYX"}) + return str(intp), str(segp) + + +def test_ooc_3d_blank_matches_in_ram(tmp_path): + """A degenerate (constant-intensity) oversized 3D ROI must not crash and must match the in-RAM + path's degenerate-ROI output for intensity, surface, and all seven texture families.""" + _ooc_vs_ram_3d( + tmp_path, + ["*3D_ALL_INTENSITY*", "*3D_ALL_MORPHOLOGY*", "*3D_GLCM*", "*3D_GLDM*", "*3D_NGLDM*", "*3D_NGTDM*", "*3D_GLRLM*", "*3D_GLSZM*", "*3D_GLDZM*"], + pair_fn=_make_volume_pair_blank, + ) + + +@pytest.mark.skipif( + not (DATA_NIFTI / "compat_int" / "compat_int_mri.nii").exists(), + reason="NIfTI compat fixtures not present in tests/data/nifti", +) +def test_ooc_3d_nifti_unsupported_format_fails_loudly(): + """NIfTI delivers the whole X*Y*Z*T volume in one read (ImageLoader::stream_volume_planes + declines it, since it isn't plane-by-plane), so an oversized ROI through this loader cannot + stream out-of-core. Must fail loudly with an actionable message rather than crash, hang, or + silently emit a wrong/zero row. ram_limit=0 forces every ROI oversized regardless of its actual + footprint (roiFootprint >= 0 is always true), so this doesn't depend on the fixture's ROI size.""" + intp = str(DATA_NIFTI / "compat_int" / "compat_int_mri.nii") + segp = str(DATA_NIFTI / "compat_seg" / "compat_seg_liver.nii") + n = nyxus.Nyxus3D(["*3D_ALL_INTENSITY*"], ram_limit=0) + with pytest.raises(Exception, match="not supported for this input format"): + n.featurize_files([intp], [segp], False) + + +def test_ooc_3d_partial_mask_matches_in_ram(tmp_path): + """Same equivalence check as the whole-volume tests above, but with a non-cuboid mask so the + ROI bbox contains background voxels -- exercises grey-level LUT construction (must include the + background bin, matching each feature's whole-cube-based calculate()) that a whole-volume mask + never triggers. Covers intensity/surface plus all four texture families in one pass.""" + _ooc_vs_ram_3d( + tmp_path, + ["*3D_ALL_INTENSITY*", "*3D_ALL_MORPHOLOGY*", "*3D_GLCM*", "*3D_GLDM*", "*3D_NGLDM*", "*3D_NGTDM*", "*3D_GLRLM*", "*3D_GLSZM*", "*3D_GLDZM*"], + pair_fn=_make_volume_pair_partial, + ) + + +def test_ooc_montage_oversized_fails_loudly(): + """The in-memory (montage) path has no out-of-core support, so an ROI whose footprint + reaches ram_limit must fail loudly rather than emit a silent all-zero feature row.""" + Y, X = 300, 300 + xg = (np.arange(X) % 256).astype(np.uint32) + yg = ((np.arange(Y) % 200) * 256).astype(np.uint32) + inten = (1 + xg[None, :] + yg[:, None]).astype(np.uint32) + mask = np.ones((Y, X), np.uint32) + + # sanity: with a large ram_limit the montage path succeeds and is non-zero. Set it + # explicitly (nyxus ram_limit is process-global) so this does not depend on test order. + ok = nyxus.Nyxus(["*ALL_INTENSITY*"]) + ok.set_params(ram_limit=8000) + df_ok = ok.featurize(inten, mask, intensity_names=["I"], label_names=["M"]) + assert df_ok["MEAN"].iloc[0] > 0 + + # ram_limit=1 makes the single ROI oversized -> must raise, not return zeros + n = nyxus.Nyxus(["*ALL_INTENSITY*"]) + n.set_params(ram_limit=1) + with pytest.raises(Exception, match="oversized"): + n.featurize(inten, mask, intensity_names=["I"], label_names=["M"]) diff --git a/tests/test_all.cc b/tests/test_all.cc index 047bb871a..a0883c9d8 100644 --- a/tests/test_all.cc +++ b/tests/test_all.cc @@ -4,6 +4,9 @@ #include "test_gabor_regression.h" #include "../src/nyx/environment.h" #include "../src/nyx/globals.h" +#include "../src/nyx/feature_method.h" // TEST_3D_OOC_GUARD_REJECTS_UNSUPPORTED_FEATURE +#include "../src/nyx/features/3d_intensity.h" +#include "../src/nyx/features/3d_glcm.h" #include "../src/nyx/ome/format_detect.h" // detect_input_format (P1) #include "test_contour.h" #include "test_ome_meta.h" // native-OME metadata parsers / OmeAxes descriptor @@ -2667,6 +2670,137 @@ TEST(TEST_NYXUS, TEST_OMEZARR_V3_BLOSC_FACADE_VOLUME) { ASSERT_NO_THROW (test_omezarr_facade_volume("dim5_v3_blosc.ome.zarr", 2, 3, 4)); } +// Larger multi-SHARD-FILE Zarr v3 store (gen_dim5.py write_v3_multishard): C=2,T=1,Z=8,Y=24,X=32, +// inner chunk (z,y,x)=(2,6,8), shard (z,y,x)=(8,12,16) -> a 2x2 GRID OF SHARD FILES per (c,t), +// each packing 16 inner chunks (8 shard files total). Unlike dim5_v3_sharded (exactly one shard +// per (t,c), so it only proves multiple inner chunks packed into ONE shard), this exercises the +// volumetric assembly crossing SHARD-FILE boundaries mid-plane -- closer to a real, larger v3 +// store's layout. Own local encoding (dim5_enc's C/Z/Y/X are hardcoded to the small fixture and +// don't apply here): value(x,y,z,c,t) = 1 + ((((t*C+c)*Z+z)*Y+y)*X+x), C=2,T=1,Z=8,Y=24,X=32. +static inline uint32_t dim5_multishard_enc(int x, int y, int z, int c, int t) +{ + const int C = 2, Z = 8, Y = 24, X = 32; + return static_cast(1 + ((((t * C + c) * Z + z) * Y + y) * X + x)); +} + +TEST(TEST_NYXUS, TEST_OMEZARR_V3_MULTISHARD_FACADE_VOLUME) { + const int T = 1, C = 2, Z = 8, Y = 24, X = 32; + fs::path ds = omezarr_data_path("dim5_v3_multishard.ome.zarr"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ds.string(); + ASSERT_EQ(il.get_full_width(), (size_t)X); + ASSERT_EQ(il.get_full_height(), (size_t)Y); + ASSERT_EQ(il.get_full_depth(), (size_t)Z); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + { + ASSERT_TRUE(il.load_volume(c, t)); + const std::vector& vol = il.get_int_volume_buffer(); + ASSERT_EQ(vol.size(), (size_t)X * Y * Z); + for (int z = 0; z < Z; ++z) + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(vol[(size_t)z * X * Y + (size_t)y * X + x], dim5_multishard_enc(x, y, z, c, t)) + << "multishard vol (x" << x << " y" << y << " z" << z << " c" << c << " t" << t << ")"; + } + il.close(); +} + +TEST(TEST_NYXUS, TEST_OMEZARR_V3_MULTISHARD_CT_COUNTS) { + fs::path ds = omezarr_data_path("dim5_v3_multishard.ome.zarr"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto ldr = NyxusOmeZarrLoader(1, ds.string()); + ASSERT_EQ(ldr.numberChannels(), (size_t)2); + ASSERT_EQ(ldr.fullTimestamps(0), (size_t)1); + ASSERT_EQ(ldr.fullDepth(0), (size_t)8); + + auto raw = RawOmezarrLoader(ds.string()); + ASSERT_EQ(raw.numberChannels(), (size_t)2); + ASSERT_EQ(raw.fullTimestamps(0), (size_t)1); + ASSERT_EQ(raw.fullDepth(0), (size_t)8); +} + +// Prescan (raw loader's readSubarray driven across all 8 shard files) must see the full encoded +// range across BOTH channels, and the whole-slide ROI area -- not garbage, not just channel 0. +TEST(TEST_NYXUS, TEST_OMEZARR_V3_MULTISHARD_PRESCAN) { + fs::path ip = omezarr_data_path("dim5_v3_multishard.ome.zarr"); + ASSERT_TRUE(fs::exists(ip)) << ip.string(); + + Environment e; + SlideProps p (ip.string(), ""); // whole-slide: no mask + ASSERT_TRUE(Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + + EXPECT_DOUBLE_EQ(p.min_preroi_inten, 1.0); + EXPECT_DOUBLE_EQ(p.max_preroi_inten, dim5_multishard_enc(31, 23, 7, 1, 0)); // last voxel, last channel + EXPECT_EQ(p.max_roi_area, (size_t)(32 * 24 * 8)); +} + +// Zarr v3 store with a MULTI-PLANE Z chunk (chunk z-extent 3 over Z=7 -> depths 3,3,1, an UNEVEN +// split), unsharded -- isolated regression test for a real bug found while building the multishard +// fixture above: omezarr.h/raw_omezarr.h's loadTile() always read exactly ONE Z-plane per tile +// (shape[iz_] left at its default of 1) regardless of the chunk's actual Z extent, so every plane +// past the first within a multi-plane chunk silently came back zero. No existing fixture before +// this one ever used a Z-chunk > 1. Own local encoding (dim5_enc/dim5_multishard_enc don't apply -- +// different dims): value(x,y,z,c,t) = 1 + ((((t*C+c)*Z+z)*Y+y)*X+x), C=2,T=1,Z=7,Y=6,X=8. +static inline uint32_t dim5_zchunked_enc(int x, int y, int z, int c, int t) +{ + const int C = 2, Z = 7, Y = 6, X = 8; + return static_cast(1 + ((((t * C + c) * Z + z) * Y + y) * X + x)); +} + +// Exercises omezarr.h's NyxusOmeZarrLoader via ImageLoader::load_volume/assemble_volume -- the +// path that read zero past the first Z-plane of a chunk before the fix. +TEST(TEST_NYXUS, TEST_OMEZARR_V3_ZCHUNKED_FACADE_VOLUME) { + const int T = 1, C = 2, Z = 7, Y = 6, X = 8; + fs::path ds = omezarr_data_path("dim5_v3_zchunked.ome.zarr"); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + SlideProps p; + p.fname_int = ds.string(); + p.fname_seg = ""; + FpImageOptions fp; + ImageLoader il; + ASSERT_TRUE(il.open(p, fp)) << ds.string(); + ASSERT_EQ(il.get_full_depth(), (size_t)Z); + + for (int t = 0; t < T; ++t) + for (int c = 0; c < C; ++c) + { + ASSERT_TRUE(il.load_volume(c, t)); + const std::vector& vol = il.get_int_volume_buffer(); + ASSERT_EQ(vol.size(), (size_t)X * Y * Z); + for (int z = 0; z < Z; ++z) + for (int y = 0; y < Y; ++y) + for (int x = 0; x < X; ++x) + ASSERT_EQ(vol[(size_t)z * X * Y + (size_t)y * X + x], dim5_zchunked_enc(x, y, z, c, t)) + << "zchunked vol (x" << x << " y" << y << " z" << z << " c" << c << " t" << t << ")"; + } + il.close(); +} + +// Exercises raw_omezarr.h's RawOmezarrLoader via RawImageLoader::for_each_voxel (the prescan path) +// -- the OTHER consumer of the same buggy loadTile(), independently regressed here. +TEST(TEST_NYXUS, TEST_OMEZARR_V3_ZCHUNKED_PRESCAN) { + fs::path ip = omezarr_data_path("dim5_v3_zchunked.ome.zarr"); + ASSERT_TRUE(fs::exists(ip)) << ip.string(); + + Environment e; + SlideProps p (ip.string(), ""); // whole-slide: no mask + ASSERT_TRUE(Nyxus::scan_slide_props(p, 3, e.anisoOptions, e.resultOptions.need_annotation())); + + EXPECT_DOUBLE_EQ(p.min_preroi_inten, 1.0); + EXPECT_DOUBLE_EQ(p.max_preroi_inten, dim5_zchunked_enc(7, 5, 6, 1, 0)); // last voxel, last channel + EXPECT_EQ(p.max_roi_area, (size_t)(8 * 6 * 7)); +} + // No 'axes' metadata -> the loader falls back to legacy 5D TCZYX and still reads. TEST(TEST_NYXUS, TEST_OMEZARR_NOAXES_FALLBACK) { ASSERT_NO_THROW (test_omezarr_addressing("dim5_noaxes.ome.zarr", 2, 3, 4)); @@ -3151,6 +3285,161 @@ TEST(TEST_NYXUS, TEST_3D_WHOLEVOLUME_REDUCE) { ilo.close(); } +// Regression: an OVERSIZED whole volume of a format that delivers plane-by-plane (e.g. OME-TIFF) +// must now featurize SUCCESSFULLY out-of-core, producing the SAME feature values as the in-RAM +// (fitting) run of the identical file -- not fail, and not emit a zero row. This supersedes the +// old TEST_3D_WHOLEVOLUME_OVERSIZED_FAILS_LOUDLY premise: before workflow_3d_whole.cpp's oversized +// branch streamed via populate_3d_voxel_cloud/run_3d_ooc_features, NO whole-volume streaming path +// existed at all, so oversized-but-streamable volumes failed loudly (a deliberate, validated +// fallback at the time). Now the only remaining loud-fail case is a genuinely unstreamable format +// (whole-4D-in-one-read, e.g. NIfTI) -- see TEST_3D_WHOLEVOLUME_UNSTREAMABLE_FORMAT_FAILS_LOUDLY. +TEST(TEST_NYXUS, TEST_3D_WHOLEVOLUME_OVERSIZED_STREAMS_OOC) { + fs::path ds = ometiff_data_path("dim3_zyx.ome.tif"); // 3D X8 Y6 Z4 + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + auto run_and_read_row = [&](bool oversized, const fs::path& outdir) -> std::string + { + fs::remove_all(outdir); fs::create_directories(outdir); + Environment e; + e.theFeatureSet.enableAll(false); + e.theFeatureSet.enableFeatures(D3_VoxelIntensityFeatures::featureset); + e.theFeatureSet.enableFeatures(D3_SurfaceFeature::featureset); + e.theFeatureSet.enableFeatures(D3_GLCM_feature::featureset); + // The full sequence main_nyxus.cpp runs before any workflow (theFeatureMgr.compile() -> + // apply_user_selection() -> init_feature_classes() -> compile_feature_settings()). Skipping + // theFeatureMgr setup leaves get_num_requested_features()==0, so run_3d_ooc_features's loop + // never executes and every feature stays at its zero-initialized default (first symptom: an + // all-zero OOC row). Skipping compile_feature_settings() leaves fsett_D3_* at size 0, so any + // STNGS_*(s) macro access (e.g. surface's STNGS_SINGLEROI) reads out of bounds -- crashed + // (SEH 0xc0000005) before this was added. + EXPECT_TRUE(e.theFeatureMgr.compile()); + e.theFeatureMgr.apply_user_selection (e.theFeatureSet); + EXPECT_TRUE(e.theFeatureMgr.init_feature_classes()); + e.compile_feature_settings(); + EXPECT_TRUE(e.set_ram_limit(oversized ? 0 : 64)); // 0 -> force oversized; 64 MB comfortably fits this tiny (X8 Y6 Z4) volume + e.output_dir = outdir.string(); + + std::vector ifiles{ ds.string() }; + auto [ok, erm] = Nyxus::processDataset_3D_wholevolume(e, ifiles, 1, Nyxus::SaveOption::saveCSV, outdir.string()); + EXPECT_TRUE(ok) << (erm ? *erm : std::string("(no error message)")); + + std::string row; + size_t datarows = 0; + for (auto& de : fs::directory_iterator(outdir)) + if (de.path().extension() == ".csv") + { + std::ifstream f(de.path()); std::string header, ln; + std::getline(f, header); + while (std::getline(f, ln)) if (!ln.empty()) { row = ln; ++datarows; } + } + EXPECT_EQ(datarows, (size_t)1) << "expected exactly one feature row"; + fs::remove_all(outdir); + return row; + }; + + std::string ooc_row = run_and_read_row(true, fs::temp_directory_path() / "nyxus_wv_ooc_test"); + std::string ram_row = run_and_read_row(false, fs::temp_directory_path() / "nyxus_wv_ram_test"); + + ASSERT_FALSE(ooc_row.empty()); + ASSERT_FALSE(ram_row.empty()); + EXPECT_EQ(ooc_row, ram_row) << "the out-of-core whole-volume row must match the in-RAM row exactly"; +} + +// Regression: a whole volume whose loader delivers the ENTIRE X*Y*Z*T blob in one read (e.g. +// NIfTI -- see ImageLoader::stream_volume_planes) cannot stream plane-by-plane, so an oversized +// NIfTI whole volume must still fail loudly (no streaming path exists for it) rather than crash or +// emit a silent zero row. ram_limit=0 forces oversized regardless of the fixture's actual size. +TEST(TEST_NYXUS, TEST_3D_WHOLEVOLUME_UNSTREAMABLE_FORMAT_FAILS_LOUDLY) { + fs::path p(__FILE__); + fs::path ds(p.parent_path().string() + fs::path("/data/hounsfield/ct3d_int16.nii").make_preferred().string()); + ASSERT_TRUE(fs::exists(ds)) << ds.string(); + + fs::path outdir = fs::temp_directory_path() / "nyxus_wv_nifti_ooc_test"; + fs::remove_all(outdir); fs::create_directories(outdir); + + Environment e; + e.theFeatureSet.enableAll(false); + e.theFeatureSet.enableFeatures(D3_VoxelIntensityFeatures::featureset); + ASSERT_TRUE(e.theFeatureMgr.compile()); + e.theFeatureMgr.apply_user_selection (e.theFeatureSet); + ASSERT_TRUE(e.theFeatureMgr.init_feature_classes()); + e.compile_feature_settings(); + ASSERT_TRUE(e.set_ram_limit(0)); // force oversized regardless of this small fixture's real size + + std::vector ifiles{ ds.string() }; + auto [ok, erm] = Nyxus::processDataset_3D_wholevolume(e, ifiles, 1, Nyxus::SaveOption::saveCSV, outdir.string()); + + EXPECT_FALSE(ok) << "an oversized NIfTI whole volume has no streaming path and must fail loudly"; + + size_t datarows = 0; + for (auto& de : fs::directory_iterator(outdir)) + if (de.path().extension() == ".csv") + { + std::ifstream f(de.path()); std::string ln; size_t n = 0; + while (std::getline(f, ln)) if (!ln.empty()) ++n; + if (n) datarows += n - 1; // minus header + } + EXPECT_EQ(datarows, (size_t)0) << "no feature row should be written for an unstreamable oversized volume"; + fs::remove_all(outdir); +} + +// Regression (found by running nyxus under a hard memory cap): the whole-volume oversized check +// must use the 3D footprint estimator (W*H*D for the image cube), not the 2D one (W*H). The 2D +// estimator ignores depth, so it under-counted a volume's memory by ~depth x, let oversized +// volumes slip through the "trivial" path, and they OOM-crashed under a real memory limit even +// with a matching --ramLimit. This pins that the 3D estimator accounts for depth (the 2D one +// does not) so featurize_wholevolume's switch to get_ram_footprint_estimate_3D stays correct. +TEST(TEST_NYXUS, TEST_3D_RAM_FOOTPRINT_COUNTS_DEPTH) { + // Two ROIs with identical W/H and voxel count, differing ONLY in bounding-box depth. This + // isolates depth's effect on each estimator. + LR flat(1); + flat.aabb.init_from_whd(64, 64, 1); + flat.aux_area = 4096; + LR tall(1); + tall.aabb.init_from_whd(64, 64, 64); + tall.aux_area = 4096; + + // the 2D estimator's image-matrix term is W*H -> it IGNORES depth: identical for both + EXPECT_EQ(flat.get_ram_footprint_estimate(1), tall.get_ram_footprint_estimate(1)); + + // the 3D estimator's image-cube term is W*H*D -> the 64x-deeper bbox is far larger. This is + // the term the 2D estimator missed, which under-counted whole volumes and let them OOM. + EXPECT_GT(tall.get_ram_footprint_estimate_3D(1), flat.get_ram_footprint_estimate_3D(1) * 10) + << "3D footprint estimator is not counting depth"; +} + +// Regression-guard: processNontrivialRois_3D's per-feature out-of-core dispatch +// (phase3_3d.cpp) must throw for any 3D FeatureMethod NOT covered by is_3d_ooc_supported() -- +// otherwise a future feature added without a streaming osized_calculate would silently read +// raw_voxels_NT (which OOC never populates for it) via the base FeatureMethod::osized_scan_whole_image +// default, producing a wrong/zero row instead of an actionable error. Every CURRENT 3D feature class +// is supported, so there is no live "unsupported" feature to exercise this through the normal +// featurize path; this pins the ALLOW-LIST FUNCTION ITSELF directly, using a minimal stand-in +// FeatureMethod that is deliberately never added to the allow-list, alongside real supported classes. +class DummyUnsupported3DFeature : public FeatureMethod +{ +public: + DummyUnsupported3DFeature() : FeatureMethod("DummyUnsupported3DFeature") {} + void calculate (LR&, const Fsettings&) override {} + void osized_add_online_pixel (size_t, size_t, uint32_t) override {} + void osized_calculate (LR&, const Fsettings&, ImageLoader&) override {} + void save_value (std::vector>&) override {} +}; + +TEST(TEST_NYXUS, TEST_3D_OOC_GUARD_REJECTS_UNSUPPORTED_FEATURE) { + DummyUnsupported3DFeature unsupported; + EXPECT_FALSE(Nyxus::is_3d_ooc_supported(&unsupported)) + << "a 3D feature class outside the allow-list must be rejected by the OOC guard"; + + D3_VoxelIntensityFeatures intensityFeature; + EXPECT_TRUE(Nyxus::is_3d_ooc_supported(&intensityFeature)) + << "a real streaming-supported 3D feature (intensity) must be accepted"; + + D3_GLCM_feature glcmFeature; + EXPECT_TRUE(Nyxus::is_3d_ooc_supported(&glcmFeature)) + << "a real streaming-supported 3D texture feature (GLCM) must be accepted"; +} + // Regression: separatecsv derives ONE output path per slide, but the CSV sinks are invoked // once per (channel, timeframe) plane and used to open that path with mode "w" every time -- // so each plane truncated the one before it and the file ended up holding only the LAST diff --git a/tests/vetting/oracle_coverage.csv b/tests/vetting/oracle_coverage.csv index 8882b7448..0bd6d622e 100644 --- a/tests/vetting/oracle_coverage.csv +++ b/tests/vetting/oracle_coverage.csv @@ -538,219 +538,219 @@ dim,feature,family,status,oracle,agreement,config_recipe,tolerance,current_test, 2D,IH_ROBUST_MEAN_IDX,intensity_histogram,vetted,analytic,,ih.ibsi_fbn,,test_intensity_histogram_ibsi.h,,mirp (IBSI Intensity-Histogram family); vet _IDX,not-in-tracker;oracle-identified,audit,"vetted by test_ih_dispersion_robust_analytic (hand-computed on the robust-window discriminating fixture; no clean IBSI anchor - robust-mean has no IBSI feature; QCoD_VAL/IQR_VAL are Nyxus continuous-percentile extensions of the IBSI discrete-percentile IQR/QCoD, so they have no IBSI counterpart by design)" 2D,IH_NUM_BINS,intensity_histogram,vetted,analytic,,,,test_intensity_histogram_regression.h;test_intensity_histogram.py,test_intensity_histogram_analytic.h,numpy/scipy analytic,not-in-tracker,audit, 2D,IH_BIN_SIZE,intensity_histogram,vetted,analytic,,,,test_intensity_histogram_regression.h;test_intensity_histogram.py,test_intensity_histogram_analytic.h,numpy/scipy analytic,not-in-tracker,audit, -3D,3COV,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,mirp:stat_cov (IBSI 7TET; no pyradiomics equiv),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3COVERED_IMAGE_INTENSITY_RANGE,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,none (Nyxus-specific: (roi_max-roi_min)/(slide_max-slide_min)),bug-degenerate,tracker,BUG: 3D segmented workflow never sets roi.slide_idx (workflow_3d_segmented.cpp) so 3d_intensity.cpp takes the else branch and always emits degenerate 1; unvettable until slide_idx+min/max_preroi_inten are wired through the 3D pipeline (2D sets it via pixel_feed.cpp). Oracle (octave/oracle_3d) confirms intended value = (roi_range)/(slide_range) -3D,3ENERGY,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3ENTROPY,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3EXCESS_KURTOSIS,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3HYPERFLATNESS,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,none (scipy.moment order 5/6 only),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3HYPERSKEWNESS,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,none (scipy.moment order 5/6 only),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3INTEGRATED_INTENSITY,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3INTERQUARTILE_RANGE,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3KURTOSIS,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3MAX,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3MEAN,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3MEAN_ABSOLUTE_DEVIATION,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3MEDIAN,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3MEDIAN_ABSOLUTE_DEVIATION,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,mirp:stat_medad (IBSI N72L),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3MIN,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3MODE,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,scipy.stats.mode on raw voxels (IBSI mode is on histogram),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3P01,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) -3D,3P10,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3P25,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) -3D,3P75,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) -3D,3P90,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3P99,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) -3D,3QCOD,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) -3D,3RANGE,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3ROBUST_MEAN,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,octave/MATLAB (mean of voxels in [P10,P90]),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; rel<1e-9) after fixing 3ROBUST_MEAN which was hardcoded 0 in both 3d_intensity.cpp paths -3D,3ROBUST_MEAN_ABSOLUTE_DEVIATION,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3ROOT_MEAN_SQUARED,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3SKEWNESS,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3STANDARD_DEVIATION,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3STANDARD_DEVIATION_BIASED,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3STANDARD_ERROR,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3VARIANCE,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3VARIANCE_BIASED,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3UNIFORMITY,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A -3D,3UNIFORMITY_PIU,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) -3D,3AREA,morphology,regression,,needs_audit,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,matlab,convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." -3D,3AREA_2_VOLUME,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:SurfaceVolumeRatio(3D),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." -3D,3COMPACTNESS1,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:Compactness1/Compactness2(3D),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." -3D,3COMPACTNESS2,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:Compactness1/Compactness2(3D),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." -3D,3MESH_VOLUME,morphology,vetted,matlab,agreed,,,test_3d_morphology_coverage.h;test_3d_morphology_matlab.h,test_3d_morphology_matlab.h,,,tracker, -3D,3SPHERICAL_DISPROPORTION,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:SphericalDisproportion(3D shape),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." -3D,3SPHERICITY,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_regression.h,pyradiomics:Sphericity(3D shape),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." -3D,3VOLUME_CONVEXHULL,morphology,vetted,matlab,agreed,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_matlab.h,,,tracker, -3D,3VOXEL_VOLUME,morphology,vetted,matlab,agreed,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h,test_3d_morphology_matlab.h,,,tracker, -3D,3MAJOR_AXIS_LEN,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:MajorAxisLength(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" -3D,3MINOR_AXIS_LEN,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:MinorAxisLength(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" -3D,3LEAST_AXIS_LEN,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:LeastAxisLength(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" -3D,3ELONGATION,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:Elongation(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" -3D,3FLATNESS,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h,test_3d_morphology_regression.h,pyradiomics:Flatness(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" -3D,3GLCM_ACOR,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_ASM,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_CLUPROM,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_CLUSHADE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_CLUTEND,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_CONTRAST,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_CORRELATION,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_DIFAVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_DIFENTRO,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_DIFVAR,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_DIS,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_DIFAVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_ENERGY,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ASM (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_ENTROPY,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_HOM1,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ID (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_HOM2,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_IDM (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_ID,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_IDN,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_IDM,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_IDMN,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_INFOMEAS1,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_INFOMEAS2,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_IV,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_JAVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_JE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_JMAX,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_JVAR,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_SUMAVERAGE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_SUMENTROPY,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A -3D,3GLCM_SUMVARIANCE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_CLUTEND (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_VARIANCE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JVAR (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_ASM_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_ACOR_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_CLUPROM_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_CLUSHADE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_CLUTEND_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_CONTRAST_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_CORRELATION_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_DIFAVE_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_DIFENTRO_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_DIFVAR_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_DIS_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_ENERGY_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ASM_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_ENTROPY_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JE_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_HOM1_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ID_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_ID_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_IDN_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_IDM_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_IDMN_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_IV_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_JAVE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_JE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_INFOMEAS1_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_INFOMEAS2_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_mirp.h,,,tracker, -3D,3GLCM_VARIANCE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JVAR_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLCM_JMAX_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_JVAR_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_SUMAVERAGE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_SUMENTROPY_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" -3D,3GLCM_SUMVARIANCE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_CLUTEND_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" -3D,3GLDM_SDE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_LDE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_GLN,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_DN,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_DNN,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_GLV,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_DV,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_DE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_LGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_HGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_SDLGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_SDHGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_LDLGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDM_LDHGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLDZM_SDE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_LDE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_LGLZE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_HGLZE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_SDLGLE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_SDHGLE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_LDLGLE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_LDHGLE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_GLNU,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_GLNUN,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_ZDNU,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_ZDNUN,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_ZP,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_GLM,gldzm,regression,,na,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,none (intermediate mean; indirect via GLV/ZDV/DCV variances),no-mainstream-oracle,tracker, -3D,3GLDZM_GLV,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_ZDM,gldzm,regression,,na,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,none (intermediate mean; indirect via GLV/ZDV/DCV variances),no-mainstream-oracle,tracker, -3D,3GLDZM_ZDV,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3GLDZM_ZDE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, -3D,3NGLDM_LDE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LDE: table=0.1 vs MIRP=0.2559." -3D,3NGLDM_HDE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HDE: table=261 vs MIRP=28.07." -3D,3NGLDM_LGLCE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LGLCE: table=0.00036 vs MIRP=0.0322." -3D,3NGLDM_HGLCE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HGLCE: table=740 vs MIRP=1324." -3D,3NGLDM_LDLGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LDLGLE: table=5.8e-05 vs MIRP=0.000685." -3D,3NGLDM_LDHGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LDHGLE: table=74 vs MIRP=474.8." -3D,3NGLDM_HDLGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HDLGLE: table=0.025 vs MIRP=8.714." -3D,3NGLDM_HDHGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HDHGLE: table=20030 vs MIRP=14942.8." -3D,3NGLDM_GLNU,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_GLNU: table=115443 vs MIRP=4350.3." -3D,3NGLDM_GLNUN,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_GLNUN: table=0.23 vs MIRP=0.01585." -3D,3NGLDM_DCNU,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCNU: table=115443 vs MIRP=40745.0." -3D,3NGLDM_DCNUN,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCNUN: table=0.23 vs MIRP=0.14847." -3D,3NGLDM_DCP,ngldm,vetted,mirp,agreed,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_mirp.h,,,tracker, -3D,3NGLDM_GLM,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,no-external-oracle,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. Additionally 3NGLDM_GLM has NO counterpart in IBSI/MIRP at all (MIRP's NGLDM emits no grey-level-mean or dependence-count-mean column; the 2D table in test_ngldm_ibsi.h likewise marks GLM '--not in IBSI--'), so no external oracle exists for it." -3D,3NGLDM_GLV,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_GLV: table=190 vs MIRP=350.17." -3D,3NGLDM_DCM,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,no-external-oracle,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. Additionally 3NGLDM_DCM has NO counterpart in IBSI/MIRP at all (MIRP's NGLDM emits no grey-level-mean or dependence-count-mean column; the 2D table in test_ngldm_ibsi.h likewise marks GLM '--not in IBSI--'), so no external oracle exists for it." -3D,3NGLDM_DCV,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCV: table=86.17 vs MIRP=11.948." -3D,3NGLDM_DCENT,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCENT: table=5.23 vs MIRP=8.676." -3D,3NGLDM_DCENE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCENE: table=0.14 vs MIRP=0.002875." -3D,3NGTDM_COARSENESS,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A -3D,3NGTDM_CONTRAST,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A -3D,3NGTDM_BUSYNESS,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A -3D,3NGTDM_COMPLEXITY,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A -3D,3NGTDM_STRENGTH,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_SAE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_LAE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_GLN,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_GLNN,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_SZN,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_SZNN,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_ZP,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_GLV,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_ZV,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_ZE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_LGLZE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_HGLZE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_SALGLE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_SAHGLE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_LALGLE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLSZM_LAHGLE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_SRE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_LRE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_GLN,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_GLNN,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_RLN,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_RLNN,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_RP,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_GLV,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_RV,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_RE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_LGLRE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_HGLRE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_SRLGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_SRHGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_LRLGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_LRHGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A -3D,3GLRLM_SRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_LRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_GLN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_GLNN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_RLN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_RLNN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_RP_AVE,glrlm,vetted,mirp,agreed,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_mirp.h,,,tracker, -3D,3GLRLM_GLV_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_RV_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_RE_AVE,glrlm,vetted,mirp,agreed,,,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_mirp.h,,,tracker, -3D,3GLRLM_LGLRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_HGLRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_SRLGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_SRHGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_LRLGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." -3D,3GLRLM_LRHGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3COV,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,mirp:stat_cov (IBSI 7TET; no pyradiomics equiv),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3COVERED_IMAGE_INTENSITY_RANGE,firstorder,regression,,na,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,none (Nyxus-specific: (roi_max-roi_min)/(slide_max-slide_min)),bug-degenerate,tracker,BUG: 3D segmented workflow never sets roi.slide_idx (workflow_3d_segmented.cpp) so 3d_intensity.cpp takes the else branch and always emits degenerate 1; unvettable until slide_idx+min/max_preroi_inten are wired through the 3D pipeline (2D sets it via pixel_feed.cpp). Oracle (octave/oracle_3d) confirms intended value = (roi_range)/(slide_range) +3D,3ENERGY,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3ENTROPY,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3EXCESS_KURTOSIS,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3HYPERFLATNESS,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,none (scipy.moment order 5/6 only),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3HYPERSKEWNESS,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,none (scipy.moment order 5/6 only),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3INTEGRATED_INTENSITY,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3INTERQUARTILE_RANGE,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3KURTOSIS,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3MAX,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3MEAN,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3MEAN_ABSOLUTE_DEVIATION,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3MEDIAN,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3MEDIAN_ABSOLUTE_DEVIATION,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,mirp:stat_medad (IBSI N72L),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3MIN,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3MODE,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,scipy.stats.mode on raw voxels (IBSI mode is on histogram),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3P01,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) +3D,3P10,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3P25,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) +3D,3P75,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) +3D,3P90,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3P99,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,numpy (trivial closed-form; IBSI has only P10/P90),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) +3D,3QCOD,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d); Nyxus 100-bin CDF-interp reproduced exactly; ~0.2% vs prctile (definitional) +3D,3RANGE,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3ROBUST_MEAN,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,octave/MATLAB (mean of voxels in [P10,P90]),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; rel<1e-9) after fixing 3ROBUST_MEAN which was hardcoded 0 in both 3d_intensity.cpp paths +3D,3ROBUST_MEAN_ABSOLUTE_DEVIATION,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3ROOT_MEAN_SQUARED,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3SKEWNESS,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3STANDARD_DEVIATION,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3STANDARD_DEVIATION_BIASED,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3STANDARD_ERROR,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3VARIANCE,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3VARIANCE_BIASED,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3UNIFORMITY,firstorder,vetted,pyradiomics,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_3d_firstorder_pyradiomics.h;test_ooc_mechanics.py,test_3d_firstorder_pyradiomics.h,,,tracker,test_3d_inten.h ORPHANED (not #included; never run) - left per decision A +3D,3UNIFORMITY_PIU,firstorder,vetted,matlab,agreed,,,test_3d_firstorder_coverage.h;test_3d_inten.h;test_ooc_mechanics.py,test_3d_firstorder_regression.h,pyradiomics/mirp (native 3D first-order; kurtosis -3),,tracker,vetted vs Octave/MATLAB oracle (octave/oracle_3d; raw-formula rel<1e-9) +3D,3AREA,morphology,regression,,needs_audit,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,matlab,convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3AREA_2_VOLUME,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:SurfaceVolumeRatio(3D),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3COMPACTNESS1,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:Compactness1/Compactness2(3D),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3COMPACTNESS2,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:Compactness1/Compactness2(3D),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3MESH_VOLUME,morphology,vetted,matlab,agreed,,,test_3d_morphology_coverage.h;test_3d_morphology_matlab.h;test_ooc_mechanics.py,test_3d_morphology_matlab.h,,,tracker, +3D,3SPHERICAL_DISPROPORTION,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:SphericalDisproportion(3D shape),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3SPHERICITY,morphology,regression,,na,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:Sphericity(3D shape),convention-mismatch,tracker,"convention: Nyxus surface area is voxel-face-based (3AREA 59992) vs MIRP/pyradiomics mesh area (46739, ~28% diff); all area/volume-derived features inherit it. Needs surface-convention decision (see Vetting-Work-Log)." +3D,3VOLUME_CONVEXHULL,morphology,vetted,matlab,agreed,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h;test_ooc_mechanics.py,test_3d_morphology_matlab.h,,,tracker, +3D,3VOXEL_VOLUME,morphology,vetted,matlab,agreed,,,test_3d_morphology_coverage.h;test_3d_morphology_regression.h;test_ooc_mechanics.py,test_3d_morphology_matlab.h,,,tracker, +3D,3MAJOR_AXIS_LEN,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:MajorAxisLength(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" +3D,3MINOR_AXIS_LEN,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:MinorAxisLength(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" +3D,3LEAST_AXIS_LEN,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:LeastAxisLength(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" +3D,3ELONGATION,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:Elongation(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" +3D,3FLATNESS,morphology,vetted,mirp,agreed,,,test_3d_morphology_coverage.h;test_ooc_mechanics.py,test_3d_morphology_regression.h,pyradiomics:Flatness(3D shape),,tracker,"FIXED axis-length mislabel (3d_surface.cpp used wrong eigenvalue indices -> LEAST>MAJOR, FLATNESS>1) + vetted vs MIRP morph_pca_* (exact): MAJOR 104.71, MINOR 88.30, LEAST 71.51, ELONGATION 0.843, FLATNESS 0.683" +3D,3GLCM_ACOR,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_ASM,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_CLUPROM,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_CLUSHADE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_CLUTEND,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_CONTRAST,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_CORRELATION,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_DIFAVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_DIFENTRO,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_DIFVAR,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_DIS,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_DIFAVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ENERGY,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ASM (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ENTROPY,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_HOM1,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ID (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_HOM2,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_IDM (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ID,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_IDN,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_IDM,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_IDMN,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_INFOMEAS1,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_INFOMEAS2,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_IV,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_JAVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_JE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_JMAX,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_JVAR,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_SUMAVERAGE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_SUMENTROPY,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_3d_glcm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glcm_pyradiomics.h,,,tracker,test_3d_glcm.h ORPHANED (not #included in test_all.cc; 26 test_3glcm_* fns never run) - left as-is per decision A +3D,3GLCM_SUMVARIANCE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_3d_glcm.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_CLUTEND (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_VARIANCE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,"pyradiomics/mirp (DIS=DifferenceAverage, HOM1=Id, HOM2=Idm, SUMVAR=ClusterTendency, ENERGY=JointEnergy=ASM, VAR=mirp cm_var); CONFIG-SENSITIVE: symmetric+13dir vs Nyxus asym/1-offset/100lvl",,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JVAR (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ASM_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_ACOR_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CLUPROM_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CLUSHADE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CLUTEND_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CONTRAST_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_CORRELATION_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_DIFAVE_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_mirp.h,,,tracker, +3D,3GLCM_DIFENTRO_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_DIFVAR_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_DIS_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_mirp.h,,,tracker, +3D,3GLCM_ENERGY_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ASM_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ENTROPY_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JE_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_HOM1_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_ID_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_ID_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_IDN_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_mirp.h,,,tracker, +3D,3GLCM_IDM_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_IDMN_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_mirp.h,,,tracker, +3D,3GLCM_IV_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_JAVE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_JE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_INFOMEAS1_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_INFOMEAS2_AVE,glcm,vetted,mirp,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_mirp.h,,,tracker, +3D,3GLCM_VARIANCE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_JVAR_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLCM_JMAX_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_JVAR_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_SUMAVERAGE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_SUMENTROPY_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: 3GLCM_*_AVE = calc_ave(per-direction base), which is exactly the value test_3d_glcm_pyradiomics.h asserts == pyradiomics for the vetted base feature" +3D,3GLCM_SUMVARIANCE_AVE,glcm,vetted,pyradiomics,agreed,,,test_3d_glcm_coverage.h;test_ooc_mechanics.py,test_3d_glcm_regression.h,mirp,,tracker,"vetted by equivalence: numerically identical to pyradiomics-vetted 3GLCM_CLUTEND_AVE (rel<1e-6, gtest TEST_3DGLCM_EQUIVALENCE_DUMP asserts it). ENTROPY/HOM2 also required the /sum_p fix" +3D,3GLDM_SDE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_LDE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_GLN,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_DN,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_DNN,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_GLV,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_DV,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_DE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_LGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_HGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_SDLGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_SDHGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_LDLGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDM_LDHGLE,gldm,vetted,pyradiomics,agreed,,,test_3d_gldm_coverage.h;test_3d_gldm.h;test_3d_gldm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_gldm_pyradiomics.h,,,tracker,test_3d_gldm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLDZM_SDE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_LDE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_LGLZE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_HGLZE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_SDLGLE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_SDHGLE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_LDLGLE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_LDHGLE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_GLNU,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_GLNUN,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_ZDNU,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_ZDNUN,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_ZP,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_GLM,gldzm,regression,,na,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,none (intermediate mean; indirect via GLV/ZDV/DCV variances),no-mainstream-oracle,tracker, +3D,3GLDZM_GLV,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_ZDM,gldzm,regression,,na,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,none (intermediate mean; indirect via GLV/ZDV/DCV variances),no-mainstream-oracle,tracker, +3D,3GLDZM_ZDV,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3GLDZM_ZDE,gldzm,regression,,needs_audit,,,test_3d_gldzm_coverage.h;test_3d_gldzm_ibsi.h;test_ooc_mechanics.py,test_3d_gldzm_regression.h,mirp,promote-after-deepdive,tracker, +3D,3NGLDM_LDE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LDE: table=0.1 vs MIRP=0.2559." +3D,3NGLDM_HDE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HDE: table=261 vs MIRP=28.07." +3D,3NGLDM_LGLCE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LGLCE: table=0.00036 vs MIRP=0.0322." +3D,3NGLDM_HGLCE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HGLCE: table=740 vs MIRP=1324." +3D,3NGLDM_LDLGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LDLGLE: table=5.8e-05 vs MIRP=0.000685." +3D,3NGLDM_LDHGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_LDHGLE: table=74 vs MIRP=474.8." +3D,3NGLDM_HDLGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HDLGLE: table=0.025 vs MIRP=8.714." +3D,3NGLDM_HDHGLE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_HDHGLE: table=20030 vs MIRP=14942.8." +3D,3NGLDM_GLNU,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_GLNU: table=115443 vs MIRP=4350.3." +3D,3NGLDM_GLNUN,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_GLNUN: table=0.23 vs MIRP=0.01585." +3D,3NGLDM_DCNU,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCNU: table=115443 vs MIRP=40745.0." +3D,3NGLDM_DCNUN,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCNUN: table=0.23 vs MIRP=0.14847." +3D,3NGLDM_DCP,ngldm,vetted,mirp,agreed,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_mirp.h,,,tracker, +3D,3NGLDM_GLM,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,no-external-oracle,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. Additionally 3NGLDM_GLM has NO counterpart in IBSI/MIRP at all (MIRP's NGLDM emits no grey-level-mean or dependence-count-mean column; the 2D table in test_ngldm_ibsi.h likewise marks GLM '--not in IBSI--'), so no external oracle exists for it." +3D,3NGLDM_GLV,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_GLV: table=190 vs MIRP=350.17." +3D,3NGLDM_DCM,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,no-external-oracle,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. Additionally 3NGLDM_DCM has NO counterpart in IBSI/MIRP at all (MIRP's NGLDM emits no grey-level-mean or dependence-count-mean column; the 2D table in test_ngldm_ibsi.h likewise marks GLM '--not in IBSI--'), so no external oracle exists for it." +3D,3NGLDM_DCV,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCV: table=86.17 vs MIRP=11.948." +3D,3NGLDM_DCENT,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCENT: table=5.23 vs MIRP=8.676." +3D,3NGLDM_DCENE,ngldm,regression,,needs_audit,,,test_3d_ngldm_coverage.h;test_3d_ngldm_regression.h;test_ooc_mechanics.py,test_3d_ngldm_regression.h,mirp,unproven-reference,tracker,"NOT externally vetted. The reference table `d3ngldm_GT` in test_3d_ngldm_regression.h has NO provenance (no tool/version/config, contrary to SPEC 6.4) and is evaluated on the Nyxus coverage phantom ut_inten.nii -- NOT the IBSI digital phantom -- so IBSI consensus values cannot apply to it. An independent MIRP NGLDM run on the same phantom at the same discretisation (fixed_bin_number n=64, 3D) disagrees on every comparable feature. The passing test is a drift guard, not an oracle; needs a documented, config-matched external oracle (MIRP) before promotion. Harness: morph_oracle/mirp_ngldm_3d.py. For 3NGLDM_DCENE: table=0.14 vs MIRP=0.002875." +3D,3NGTDM_COARSENESS,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A +3D,3NGTDM_CONTRAST,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A +3D,3NGTDM_BUSYNESS,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A +3D,3NGTDM_COMPLEXITY,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A +3D,3NGTDM_STRENGTH,ngtdm,vetted,pyradiomics,agreed,,,test_3d_ngtdm_coverage.h;test_3d_ngtdm.h;test_3d_ngtdm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_ngtdm_pyradiomics.h,,,tracker,test_3d_ngtdm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_SAE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_LAE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_GLN,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_GLNN,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_SZN,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_SZNN,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_ZP,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_GLV,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_ZV,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_ZE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_LGLZE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_HGLZE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_SALGLE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_SAHGLE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_LALGLE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLSZM_LAHGLE,glszm,vetted,pyradiomics,agreed,,,test_3d_glszm_coverage.h;test_3d_glszm.h;test_3d_glszm_pyradiomics.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glszm_pyradiomics.h,,,tracker,test_3d_glszm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_SRE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_LRE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_GLN,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_GLNN,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_RLN,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_RLNN,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_RP,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_GLV,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_RV,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_RE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_LGLRE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_HGLRE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_SRLGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_SRHGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_LRLGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_LRHGLE,glrlm,vetted,pyradiomics,agreed,,,test_3d_glrlm_coverage.h;test_3d_glrlm.h;test_3d_glrlm_pyradiomics.h;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,test_3d_glrlm.h ORPHANED (not #included; never run) - left per decision A +3D,3GLRLM_SRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_LRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_GLN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_GLNN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_RLN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_RLNN_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_RP_AVE,glrlm,vetted,mirp,agreed,,,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_mirp.h,,,tracker, +3D,3GLRLM_GLV_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_RV_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_RE_AVE,glrlm,vetted,mirp,agreed,,,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_mirp.h,,,tracker, +3D,3GLRLM_LGLRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_HGLRE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_SRLGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_SRHGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_LRLGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." +3D,3GLRLM_LRHGLE_AVE,glrlm,vetted,pyradiomics,agreed,"glrlm.pyradiomics_bincount20 -- ibsi=false, GREYDEPTH=100, GLRLM_GREYDEPTH=-20 (radiomics binCount binning), 3D compat phantom, calc_ave over directions. VETTED ONLY ON THIS RECIPE (the pyradiomics-compat config); other configs are not covered.",,test_3d_glrlm_coverage.h;test_nyxus.py;test_ooc_mechanics.py,test_3d_glrlm_pyradiomics.h,,,tracker,"vetted vs PyRadiomics 3D GLRLM (binCount=20, weightingNorm=None) on the 3D compat phantom (ut_inten.nii label 57) at 10% in test_3d_glrlm_pyradiomics.h (TEST_COMPAT_3GLRLM_AVE_FEATURES). save_value stores fvals[X_AVE][0] = calc_ave(angled_X), the same direction-averaged quantity the base test asserts == PyRadiomics, so the _AVE slot is a direct PyRadiomics assertion. CONFIG SCOPE: vetted only on the pyradiomics-compat recipe above; Nyxus' default binning is not covered by this assertion." IMQ,FOCUS_SCORE,imq,regression,,,,,test_imq_regression.h,test_imq_regression.h,reference IQ library (e.g. skimage/BRISQUE) or analytic,not-in-tracker,audit, IMQ,LOCAL_FOCUS_SCORE,imq,regression,,,,,test_imq_regression.h,test_imq_regression.h,reference IQ library (e.g. skimage/BRISQUE) or analytic,not-in-tracker,audit, IMQ,POWER_SPECTRUM_SLOPE,imq,regression,,,,,test_imq_regression.h,test_imq_regression.h,reference IQ library (e.g. skimage/BRISQUE) or analytic,not-in-tracker,audit,