From 2cc0057a4595b99f038662f179ad5c1afd031745 Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Thu, 6 Aug 2026 17:15:46 +0200 Subject: [PATCH 1/2] fix(compare-images): collapse the preview slice direction with Guess compare-double-images extracts a middle 2D slice of the difference image for a rescaled preview, with SetDirectionCollapseToSubmatrix(). That mode throws when the collapsed sub-matrix is singular, which it is for the axis permutations that are normal in 3D medical images: NRRD space directions (0,1,0) (0,0,-1) (-1.3,0,0) collapse to a 2x2 with determinant 0. Comparing such an image against itself therefore aborted the module. Guess keeps the sub-matrix when it is valid, so images that work today are unaffected, and falls back to the identity otherwise. The slice only feeds a preview, so its direction carries no meaning either way. Adds tests over identity, permuted and flipped directions, covering both a self-comparison and a single-voxel difference so the path is exercised rather than merely silenced. Closes #1579 --- .../compare-images/compare-double-images.cxx | 5 +- .../tests/test_compare_images_direction.py | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 packages/compare-images/python/itkwasm-compare-images-wasi/tests/test_compare_images_direction.py diff --git a/packages/compare-images/compare-double-images.cxx b/packages/compare-images/compare-double-images.cxx index 3592d82f8..f1624bc71 100644 --- a/packages/compare-images/compare-double-images.cxx +++ b/packages/compare-images/compare-double-images.cxx @@ -198,7 +198,10 @@ CompareImages(itk::wasm::Pipeline & pipeline, const TImage * testImage) region.SetSize(size); auto extract = ExtractType::New(); - extract->SetDirectionCollapseToSubmatrix(); + // Collapsing to the sub-matrix throws when it is singular, which it is for + // the axis permutations common in 3D medical images. The slice only feeds a + // rescaled preview, so its direction carries no meaning. + extract->SetDirectionCollapseToGuess(); extract->SetInput(diff->GetOutput()); extract->SetExtractionRegion(region); diff --git a/packages/compare-images/python/itkwasm-compare-images-wasi/tests/test_compare_images_direction.py b/packages/compare-images/python/itkwasm-compare-images-wasi/tests/test_compare_images_direction.py new file mode 100644 index 000000000..85713b714 --- /dev/null +++ b/packages/compare-images/python/itkwasm-compare-images-wasi/tests/test_compare_images_direction.py @@ -0,0 +1,68 @@ +import numpy as np +import pytest +from itkwasm import FloatTypes, Image, ImageType, PixelTypes +from itkwasm_compare_images_wasi import compare_double_images + +# A non-identity direction is the normal case for a 3D medical image. Collapsing +# it to a sub-matrix for the 2D preview slice fails when that sub-matrix is +# singular, which it is for an axis permutation. +DIRECTIONS = { + "identity": [1, 0, 0, 0, 1, 0, 0, 0, 1], + # NRRD space directions (0,1,0) (0,0,-1) (-1,0,0): the 2x2 sub-matrix is singular. + "permuted": [0, 0, -1, 1, 0, 0, 0, -1, 0], + "flipped": [-1, 0, 0, 0, -1, 0, 0, 0, 1], +} + + +def _image(direction, spike=None): + size = [8, 8, 8] + data = np.arange(np.prod(size), dtype=np.float64).reshape(size[::-1]) + if spike is not None: + data[4, 4, 4] += spike + return Image( + imageType=ImageType( + dimension=3, + componentType=FloatTypes.Float64, + pixelType=PixelTypes.Scalar, + components=1, + ), + name="image", + origin=[0.0, 0.0, 0.0], + spacing=[1.0, 1.0, 1.0], + direction=np.asarray(direction, dtype=np.float64).reshape(3, 3), + size=size, + data=data, + ) + + +@pytest.mark.parametrize("name", list(DIRECTIONS)) +def test_compare_double_images_direction(name): + """An image compared against itself is equal, whatever its direction.""" + image = _image(DIRECTIONS[name]) + metrics, _difference, _preview = compare_double_images( + image, + baseline_images=[image], + difference_threshold=0.0, + radius_tolerance=0, + number_of_pixels_tolerance=0, + ) + assert metrics["almostEqual"] + assert metrics["numberOfPixelsWithDifferences"] == 0 + + +@pytest.mark.parametrize("name", list(DIRECTIONS)) +def test_compare_double_images_detects_differences(name): + """Differences are still found, so the fix does not just silence the path.""" + direction = DIRECTIONS[name] + metrics, _difference, preview = compare_double_images( + _image(direction, spike=50.0), + baseline_images=[_image(direction)], + difference_threshold=0.0, + radius_tolerance=0, + number_of_pixels_tolerance=0, + ) + assert not metrics["almostEqual"] + assert metrics["numberOfPixelsWithDifferences"] == 1 + assert metrics["maximumDifference"] == 50.0 + # The 2D preview is the slice whose direction collapse used to throw. + assert preview is not None From 508caccf0ea53d887d4783c78d497bdc890c742a Mon Sep 17 00:00:00 2001 From: Valentin Boussot Date: Fri, 14 Aug 2026 10:28:16 +0200 Subject: [PATCH 2/2] fix(compare-images): rebuild the vendored compare-double-images wasi module The Windows and macOS Python jobs test the wasm binary committed in wasm_modules rather than rebuilding it, so the direction-collapse fix needs the regenerated module alongside the source change. Built with docker.io/itkwasm/wasi:latest; the new direction tests pass against it. --- .../compare-double-images.wasi.wasm | Bin 1938991 -> 1938991 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/compare-images/python/itkwasm-compare-images-wasi/itkwasm_compare_images_wasi/wasm_modules/compare-double-images.wasi.wasm b/packages/compare-images/python/itkwasm-compare-images-wasi/itkwasm_compare_images_wasi/wasm_modules/compare-double-images.wasi.wasm index 3bd92c6cccd2c767e19b2ee23386a8313d37971d..b47ee7b5b006e9c248eddf530a1a46f98c98f792 100644 GIT binary patch delta 138 zcmWNFJrcoC00upY|Nnm;oyHlQCO6QfN`c~Y1T(gAfS5E!E}(Y;g-VK;aRXklvpbu_ zVG@VitNERbCyXcx7rrQovZ#ovsEN90h^ApsFa1Pz iw{+c{?R)8a>q36~Ms}6%@Am2bA;_#_o(9?Carpz4>^ZUk