Skip to content

ENH: Add StructuralSimilarityImageFilter for SSIM image quality#6034

Open
hjmjohnson wants to merge 1 commit intoInsightSoftwareConsortium:mainfrom
hjmjohnson:copilot/add-structural-similarity-filter-clean
Open

ENH: Add StructuralSimilarityImageFilter for SSIM image quality#6034
hjmjohnson wants to merge 1 commit intoInsightSoftwareConsortium:mainfrom
hjmjohnson:copilot/add-structural-similarity-filter-clean

Conversation

@hjmjohnson
Copy link
Copy Markdown
Member

Closes #6030. Supersedes #6031 (which had compile errors and was based on an off-by-default uniform window rather than the canonical Wang Gaussian).

Summary

N-dimensional, multi-threaded ITK filter for the Structural Similarity Index Measure (Wang et al., IEEE TIP 2004). Two inputs in, a per-pixel SSIM map out, and a scalar mean SSIM available via GetMeanSSIM() after Update().

Resources consulted

The implementation and tests follow the canonical SSIM formulation. Sources used while building this PR:

Algorithm

For two images $x$ and $y$, local statistics are computed by convolving with a discrete Gaussian (default $\sigma=1.5$, $11\times 11$):

$$\mu_x = G_\sigma * x,\quad \mu_y = G_\sigma * y$$

$$\sigma_x^2 = G_\sigma * x^2 - \mu_x^2,\quad \sigma_y^2 = G_\sigma * y^2 - \mu_y^2,\quad \sigma_{xy} = G_\sigma * (xy) - \mu_x\mu_y$$

The three SSIM components (with $C_1 = (K_1 L)^2$, $C_2 = (K_2 L)^2$, $C_3 = C_2/2$):

$$l(x,y) = \frac{2\mu_x\mu_y + C_1}{\mu_x^2 + \mu_y^2 + C_1},\quad c(x,y) = \frac{2\sigma_x\sigma_y + C_2}{\sigma_x^2 + \sigma_y^2 + C_2},\quad s(x,y) = \frac{\sigma_{xy} + C_3}{\sigma_x\sigma_y + C_3}$$

Combined: $\text{SSIM}(x,y) = l^\alpha c^\beta s^\gamma$. The default $\alpha=\beta=\gamma=1$ takes the simplified product fast path that matches Wang's reference SSIM exactly:

$$\text{SSIM}(x,y) = \frac{(2\mu_x\mu_y + C_1)(2\sigma_{xy} + C_2)}{(\mu_x^2 + \mu_y^2 + C_1)(\sigma_x^2 + \sigma_y^2 + C_2)}$$

Architecture

Composite ImageToImageFilter:

  1. Internal sub-pipeline of 5 DiscreteGaussianImageFilter passes (for $\mu_x$, $\mu_y$, $\mu_{xx}$, $\mu_{yy}$, $\mu_{xy}$), reusing ITK's well-optimized multi-threaded smoothing.
  2. A parallelized per-pixel combination via MultiThreaderBase::ParallelizeImageRegion reads from the five smoothed buffers and writes the SSIM map.
  3. Mean SSIM is accumulated only over the cropped interior region (excluding the half-Gaussian-kernel border), matching scikit-image and the MATLAB reference.

API (covers all of #6030)

Parameter Default Notes
GaussianSigma 1.5 Wang canonical
MaximumKernelWidth 11 gives 11×11 with σ=1.5
K1 / K2 0.01 / 0.03 Wang defaults
DynamicRange $L$ NumericTraits-derived 1.0 for float/double, 255 for uchar, 65535 for ushort, etc.
LuminanceExponent $\alpha$ 1.0
ContrastExponent $\beta$ 1.0
StructureExponent $\gamma$ 1.0
ScaleWeights {1.0} Single-scale. Length > 1 reserved for MS-SSIM (currently throws not-yet-implemented).

The filter is N-dimensional (tested 2D, 3D, 4D), templated over input/output image types, and exception-safe for invalid configurations.

Test strategy (30 GTest assertions)

Reference values were generated against skimage.metrics.structural_similarity with gaussian_weights=True, sigma=1.5, use_sample_covariance=False, data_range=255, win_size=11 (canonical Wang configuration).

Tests are split into seven categories that decouple correctness checks from sensitivity to discrete-Gaussian implementation differences between ITK's GaussianOperator and scipy's sampled Gaussian:

  1. Mathematical identities (kernel-independent, tolerance 1e-9)

    • SSIM(x, x) = 1 exactly for constant, random, and gradient images
    • Symmetry: SSIM(a,b) == SSIM(b,a)
  2. Closed-form analytic checks for constant inputs (tolerance 1e-9)

    • For constant inputs all variances and the covariance vanish, so $\text{SSIM}(a,b) = (2ab + C_1)/(a^2 + b^2 + C_1)$
    • Verified at (100, 150) → 0.9230923 and the textbook (0, 255) → 0.0000999900
    • Per-pixel verification across the entire output map for constant inputs (every map element matches the closed form)
  3. Input validation (exception tests)

    • Mismatched input sizes, missing inputs, non-positive sigma, non-positive dynamic range, empty ScaleWeights, multi-element ScaleWeights (MS-SSIM not yet implemented)
  4. Qualitative properties

    • Result range bounded in $[-1, 1]$
    • Strict monotonic decay of mean SSIM as additive Gaussian noise grows ($\sigma = 2 \to 8 \to 24$)
    • Strong anti-correlation for negated images (SSIM(x, 255-x) < -0.5)
  5. scikit-image cross-checks (tolerance 5e-3)

    • Gradient + 30 luminance shift → 0.9676912545
    • Gradient × 0.5 contrast change → 0.7550069937
    • The 5e-3 tolerance absorbs minor discretization differences between ITK's GaussianOperator and scipy's sampled Gaussian (the two libraries do not produce bit-identical Gaussian kernels)
  6. Code-path equivalence

    • The simplified-product fast path ($\alpha=\beta=\gamma=1$) and the general $l^\alpha c^\beta s^\gamma$ path (forced via a tiny exponent perturbation) agree to 1e-6 on the same inputs
  7. Multi-dimensional and pixel-type coverage

    • 3D and 4D variants of the identity and constant-input tests
    • Default DynamicRange correct for unsigned char, unsigned short, float
    • unsigned char identical-image identity test

Results

Local build with GCC 13.3 / Ninja / Release on Ubuntu 24.04, 48 cores:

$ cmake --build build-ssim -j48 --target ITKImageCompareGTestDriver
[4/4] Linking CXX executable bin/ITKImageCompareGTestDriver

$ ./bin/ITKImageCompareGTestDriver --gtest_filter='StructuralSimilarityImageFilter.*'
[==========] 30 tests from 1 test suite ran. (60 ms total)
[  PASSED  ] 30 tests.

$ ctest -R 'StructuralSimilarity' --output-on-failure
100% tests passed, 0 tests failed out of 30
Total Test time (real) =   0.30 sec

ITKImageCompareHeaderTest1 (the auto-generated header self-test) and the existing ITKImageCompareTestDriver also link cleanly with the new module dependencies.

pre-commit (gersemi, clang-format, kw-pre-commit, etc.) reports all checks Passed on every touched file.

Module dependency changes

Modules/Filtering/ImageCompare/itk-module.cmake adds:

  • ITKSmoothing as a COMPILE_DEPENDS (for DiscreteGaussianImageFilter)
  • ITKGoogleTest as a TEST_DEPENDS (for the new GTest driver)

Files

Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.h    | 295 ++
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.hxx  | 386 ++
Modules/Filtering/ImageCompare/test/itkStructuralSimilarityImageFilterGTest.cxx| 674 ++
Modules/Filtering/ImageCompare/wrapping/itkStructuralSimilarityImageFilter.wrap|   3 ++
Modules/Filtering/ImageCompare/itk-module.cmake                                |   2 +
Modules/Filtering/ImageCompare/test/CMakeLists.txt                             |   4 +
6 files changed, 1364 insertions(+)

Test plan

  • All 30 SSIM GTests pass locally (ctest -R StructuralSimilarity)
  • ITKImageCompareHeaderTest1 (header self-test) compiles and links
  • ITKImageCompareTestDriver (full classic test driver) compiles and links
  • pre-commit run clean on all touched files (gersemi, clang-format, kw-pre-commit)
  • CI: Linux, Windows, macOS, ARM, Pixi
  • CI: Python wrapping (itkStructuralSimilarityImageFilter.wrap provided for WRAP_ITK_REAL, 2D)

Future work (out of scope here)

  • Multi-scale SSIM (MS-SSIM, Wang 2003): the ScaleWeights API surface is in place; implementation requires per-scale Gaussian downsampling and the per-component product across scales. A length > 1 array currently throws a clear not-yet-implemented exception.

🤖 Generated with Claude Code

@github-actions github-actions bot added type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Enhancement Improvement of existing methods or implementation area:Python wrapping Python bindings for a class type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct area:Filtering Issues affecting the Filtering module labels Apr 10, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 10, 2026

Greptile Summary

This PR adds a new StructuralSimilarityImageFilter implementing the Wang et al. 2004 SSIM metric as an N-dimensional, multi-threaded composite ITK filter. The implementation is thorough: five internal DiscreteGaussianImageFilter passes feed a parallelized per-pixel combination stage, with mean SSIM averaged over the cropped interior region matching the scikit-image/MATLAB reference. All 30 GTests pass locally. Two minor correctness edge-cases are worth noting before merge.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/edge-case suggestions that do not affect the default use case.

The algorithm is correct for the default configuration (σ=1.5, 11×11 kernel, K1=0.01, K2=0.03, L from NumericTraits). The two flagged gaps — border-crop formula for even kernel widths, and no guard against K1/K2=0 NaN — are P2 edge cases that do not affect any of the 30 passing tests or the documented API. The test coverage is comprehensive and the implementation closely follows Wang 2004 and scikit-image.

itkStructuralSimilarityImageFilter.hxx lines 106–122 (K1/K2 validation) and line 240 (border-crop formula).

Important Files Changed

Filename Overview
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.h Public API and class declaration; well-structured with standard ITK macros. Documentation note: header comment still says "BeforeGenerate" though validation is in VerifyPreconditions (flagged in previous thread).
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.hxx Core implementation; simplified and general SSIM paths are correct. Two edge-case gaps: K1/K2=0 produces silent NaN for constant/zero-mean inputs, and the border-crop formula diverges from scikit-image for even MaximumKernelWidth values.
Modules/Filtering/ImageCompare/test/itkStructuralSimilarityImageFilterGTest.cxx 30 GTest cases covering identities, analytic constant-image checks, input validation, qualitative properties, scikit-image cross-checks, code-path equivalence, and multi-dimensional/pixel-type coverage. Well-designed and comprehensive.
Modules/Filtering/ImageCompare/itk-module.cmake Adds ITKSmoothing as COMPILE_DEPENDS (correct for template-only dependency) and ITKGoogleTest as TEST_DEPENDS. No issues.
Modules/Filtering/ImageCompare/test/CMakeLists.txt Adds creategoogletestdriver for the new GTest file; GTest auto-discovery handles CTest registration, confirmed by author's ctest run showing 30 tests.
Modules/Filtering/ImageCompare/wrapping/itkStructuralSimilarityImageFilter.wrap Wraps for WRAP_ITK_REAL (float/double) in 2D, consistent with SimilarityIndexImageFilter's wrapping pattern. Appropriate starting point.

Reviews (3): Last reviewed commit: "ENH: Add StructuralSimilarityImageFilter..." | Re-trigger Greptile

@blowekamp
Copy link
Copy Markdown
Member

I believe algorithms should go into remote modules first.

It may be useful to create new skill or AI tools in the Remove module template to assist with creating new modules.

I have used SSI before, I am unsure if I implemented it also with jus using composition of filters with SimpleITK or only use the scikit-image version.

@hjmjohnson
Copy link
Copy Markdown
Member Author

I believe algorithms should go into remote modules first.

It may be useful to create new skill or AI tools in the Remove module template to assist with creating new modules.

I have used SSI before, I am unsure if I implemented it also with jus using composition of filters with SimpleITK or only use the scikit-image version.

@blowekamp I was thinking both of these thoughts too (1 from tempate remote module generator & RemoteModule for new filters), this too, but I think we should have a "FutureITK" module for putting proposed additions with a set of criteria for enabling/disabling elements of the entire module. Maintaining many modules is a major pain point, so minimimizing the number of modules is important.

ENABLE_SSIM_METRIC=ON|OFF
ENABLE_COOL_FILTER=ON|OFF

Hans

@hjmjohnson hjmjohnson marked this pull request as draft April 10, 2026 13:21
@blowekamp
Copy link
Copy Markdown
Member

I believe algorithms should go into remote modules first.
It may be useful to create new skill or AI tools in the Remove module template to assist with creating new modules.
I have used SSI before, I am unsure if I implemented it also with jus using composition of filters with SimpleITK or only use the scikit-image version.

@blowekamp I was thinking both of these thoughts too (1 from tempate remote module generator & RemoteModule for new filters), this too, but I think we should have a "FutureITK" module for putting proposed additions with a set of criteria for enabling/disabling elements of the entire module. Maintaining many modules is a major pain point, so minimimizing the number of modules is important.

ENABLE_SSIM_METRIC=ON|OFF ENABLE_COOL_FILTER=ON|OFF

Hans

Yes, the remote module situation is not easy. Hopefully with AI agents and tools/skills the process can easier now.

What you are describing is similar to the "Review" module. Which is one of those optional modules that is not on for testing so it breaks. We should probably have this module enable for testing but not by default for users.

Minimizing the number of options is also important to. A header only filter, with a test is no addition to users who do not include the header. It's only an addition to testing, so then if it's not tested it won't be working.

@dzenanz
Copy link
Copy Markdown
Member

dzenanz commented Apr 10, 2026

Maintaining many modules is a major pain point, so minimimizing the number of modules is important.

I was also thinking about grouping the content of current remote modules into a much smaller set. Most remote modules have just a few filters in them, so combining them would be technically easy. What do you think about this? The main question is how to group them.

@hjmjohnson
Copy link
Copy Markdown
Member Author

Maintaining many modules is a major pain point, so minimimizing the number of modules is important.

I was also thinking about grouping the content of current remote modules into a much smaller set. Most remote modules have just a few filters in them, so combining them would be technically easy. What do you think about this? The main question is how to group them.

I would take into consideration the information in *.remote.cmake.

#-- # Grading Level Criteria Report
#-- EVALUATION DATE: 2020-03-24
#-- EVALUATORS: [Dženan Zukić, Davis Vigneault]
#--
#-- ## Compliance level 5 star (AKA ITK main modules, or remote modules that could become core modules)
#--   - [ ] Widespread community dependance
#--   - [X] Above 90% code coverage
#--   - [ ] CI dashboards and testing monitored rigorously
#--   - [X] Key API features are exposed in wrapping interface
#--   - [ ] All requirements of Levels 4,3,2,1
#--
#-- ## Compliance Level 4 star (Very high-quality code, perhaps small community dependance)
#--   - [X] Meets all ITK code style standards
#--   - [X] No external requirements beyond those needed by ITK proper
#--   - [ ] Builds and passes tests on all supported platforms within 1 month of each core tagged release
#--            - [ ] Windows Shared Library Build with Visual Studio
#--            - [ ] Mac with clang compiler
#--            - [ ] Linux with gcc compiler
#--   - [X] Active developer community dedicated to maintaining code-base
#--   - [X] 75% code coverage demonstrated for testing suite
#--   - [X] Continuous integration testing performed
#--   - [X] All requirements of Levels 3,2,1

Anything in Level 2 or below should perhaps be combined into a single AlphaCodeSpecialtyModules Remote module.

Anything in Level 3 is BetaCodeSpecialtyModules modules

Anything in Level 4 is SpecialtyModules

@dzenanz
Copy link
Copy Markdown
Member

dzenanz commented Apr 10, 2026

We are still in 6.0 beta phase. We can do this now. But we should discuss this more widely. Hans, do you want to start a new forum topic?

@hjmjohnson
Copy link
Copy Markdown
Member Author

@dzenanz I've got a lot going on today. Will try to address this weekend.

@hjmjohnson
Copy link
Copy Markdown
Member Author

The lone failing check (ITK.Linux on Azure DevOps build 15505) appears to be a pipeline-side flake rather than a real build/test issue:

  • CDash reports "All builds completed successfully" for SHA 46fabd3525 — meaning the same Linux ITK build that Azure ran successfully completed, ran its tests, and submitted the green results to CDash.
  • ✅ Every other Linux build passes:
    • Pixi-Cxx (ubuntu-22.04)
    • ARMBUILD-Ubuntu-24.04-arm
    • ARMBUILD-x86_64-rosetta ✓ (macOS but same toolchain class)
  • ✅ Every other Azure pipeline passes:
    • ITK.Windows
    • ITK.macOS
    • ITK.Linux.Python
    • ITK.macOS.Python
  • ✅ The 30 new SSIM GTests run and pass on ARMBUILD-Python (verified in the run log: tests FindPython3.cmake unavailable before CMake 3.12 #1700Add lambda function command #1735 all Passed).

The pattern (lone Azure pipeline failing, CDash green, all duplicate-coverage Linux builds green) almost always means the failure is in a post-CTest infrastructure step in the Azure pipeline (cache save, artifact upload, agent cleanup), not in the build itself. Re-triggering.

@hjmjohnson
Copy link
Copy Markdown
Member Author

/azp run ITK.Linux

Copy link
Copy Markdown
Member

@blowekamp blowekamp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comment on pipeline management details.

The implementation this feeling is going to be rather memory intensive with the number of image converted and processed to real type.

Is this a 2D only implementation?

@hjmjohnson
Copy link
Copy Markdown
Member Author

Updated diagnosis: the ITK.Linux failure was a real one in the legacy-removed Linux build (the Azure pipeline runs both a normal and an ITK_LEGACY_REMOVE=ON configuration; the normal one was green on CDash, hence the misleading "all builds completed successfully" CDash summary, but the legacy-removed one promoted a deprecation warning into an error).

The root cause:

itkStructuralSimilarityImageFilter.hxx:342:87:
warning: 'itk::ImageConstIterator<TImage>::IndexType
itk::ImageConstIterator<TImage>::GetIndex() const' is deprecated:
Please use ComputeIndex() instead, or use an iterator with index,
like ImageIteratorWithIndex! [-Wdeprecated-declarations]

  342 |   if (subInterior.GetNumberOfPixels() > 0 && subInterior.IsInside(outIt.GetIndex()))

The output iterator was a plain ImageRegionIterator, and the per-pixel loop called outIt.GetIndex() to test interior-region membership for the mean accumulator. GetIndex() on the index-less iterator is wrapped in #ifndef ITK_FUTURE_LEGACY_REMOVE (Modules/Core/Common/include/itkImageConstIterator.h:306-317) and emits a deprecation warning under ITK_LEGACY_REMOVE. CI promotes that warning to an error.

Fix (commit f4316869e7): switch the output iterator to ImageRegionIteratorWithIndex, which tracks the index natively and exposes a non-deprecated GetIndex(). One-line change: replace the using OutputIteratorType = ImageRegionIterator<OutputImageType> typedef and add the corresponding header include.

Local verification:

$ cmake -B build-ssim-legacy -S . -G Ninja \
       -DITK_LEGACY_REMOVE=ON -DITK_LEGACY_SILENT=OFF ...
$ cmake --build build-ssim-legacy -j 4 \
       --target ITKImageCompareHeaderTest1 ITKImageCompareGTestDriver
[1/4] Building CXX object .../ITKImageCompareHeaderTest1.dir/test/ITKImageCompareHeaderTest1.cxx.o
[2/4] Linking CXX executable bin/ITKImageCompareHeaderTest1
[3/4] Building CXX object .../itkStructuralSimilarityImageFilterGTest.cxx.o
[4/4] Linking CXX executable bin/ITKImageCompareGTestDriver

No deprecation warnings, clean build.

$ ./bin/ITKImageCompareGTestDriver --gtest_filter='StructuralSimilarityImageFilter.*'
[==========] 30 tests from 1 test suite ran. (333 ms total)
[  PASSED  ] 30 tests.

All 30 SSIM GTests still pass under the legacy-removed build with the new iterator.

Implements the Structural Similarity Index Measure as an N-dimensional,
multi-threaded ITK filter in the ImageCompare module.  Closes InsightSoftwareConsortium#6030,
supersedes InsightSoftwareConsortium#6031.

== Algorithm and reference sources ==

The implementation follows the canonical SSIM formulation of Wang, Bovik,
Sheikh, and Simoncelli, "Image Quality Assessment: From Error Visibility
to Structural Similarity," IEEE Trans. Image Processing 13(4), 2004.
The reference materials consulted while building this filter were:

  - Wang et al. 2004 paper (full text):
    https://www.cns.nyu.edu/pub/eero/wang03-reprint.pdf
  - Wang et al. SSIM MATLAB reference (utlive/ssim/ssim_index.m):
    https://github.com/utlive/ssim/blob/main/ssim_index.m
  - scikit-image v0.25 structural_similarity implementation:
    https://github.com/scikit-image/scikit-image/blob/v0.25.0/skimage/metrics/_structural_similarity.py
  - Wang, Simoncelli, Bovik, "Multi-Scale Structural Similarity for Image
    Quality Assessment," Asilomar 2003 (MS-SSIM, future extension):
    https://www.cns.nyu.edu/pub/eero/wang03b.pdf
  - Wikipedia SSIM article (formulas, defaults):
    https://en.wikipedia.org/wiki/Structural_similarity_index_measure

For two images x and y the filter computes local statistics by convolving
with a discrete Gaussian (sigma=1.5, 11x11 default, matching Wang et al.
and the default of skimage.metrics.structural_similarity):

  mu_x  = G_sigma * x
  mu_y  = G_sigma * y
  var_x = G_sigma * (x*x) - mu_x^2
  var_y = G_sigma * (y*y) - mu_y^2
  cov   = G_sigma * (x*y) - mu_x * mu_y

The three SSIM components are
  l(x,y) = (2*mu_x*mu_y + C1) / (mu_x^2 + mu_y^2 + C1)
  c(x,y) = (2*sigma_x*sigma_y + C2) / (var_x + var_y + C2)
  s(x,y) = (cov + C3) / (sigma_x*sigma_y + C3)
with C1 = (K1*L)^2, C2 = (K2*L)^2, C3 = C2/2, K1=0.01, K2=0.03, L the
dynamic range.  The combined SSIM is l^alpha * c^beta * s^gamma.  When
alpha=beta=gamma=1 (the default), the filter takes the simplified product
fast path
  SSIM = (2*mu_x*mu_y + C1)*(2*cov + C2) /
         ((mu_x^2 + mu_y^2 + C1)*(var_x + var_y + C2))
which is what Wang et al.'s reference distributes and what skimage uses
by default.

== Filter architecture ==

The filter is structured as a composite ImageToImageFilter:

  1. Internal sub-pipeline of five DiscreteGaussianImageFilter passes
     (mu_x, mu_y, mu_xx, mu_yy, mu_xy) reusing ITK's well-optimized,
     multi-threaded smoothing.
  2. A parallelized per-pixel combination via
     MultiThreaderBase::ParallelizeImageRegion that reads from the five
     smoothed buffers and writes the SSIM map.
  3. The mean SSIM is accumulated only over the interior region (cropped
     by half the Gaussian kernel width), matching scikit-image and the
     MATLAB reference, since pixels within the kernel half-width use
     boundary-extended values inside the convolution and are less
     reliable.

Inputs:  two images of the same template type and identical region.
Outputs:
  - a per-pixel SSIM map (TOutputImage, default Image<float, D>)
  - GetMeanSSIM() returns the scalar after Update()

Configurable runtime parameters (covering all of issue InsightSoftwareConsortium#6030):
  - GaussianSigma                      (default 1.5)
  - MaximumKernelWidth                 (default 11)
  - K1, K2 stability constants         (defaults 0.01, 0.03)
  - DynamicRange L                     (NumericTraits-derived: 1.0 for
                                         float/double, 255 for uchar,
                                         65535 for ushort, ...)
  - LuminanceExponent  alpha           (default 1.0)
  - ContrastExponent   beta            (default 1.0)
  - StructureExponent  gamma           (default 1.0)
  - ScaleWeights array                 (default {1.0} -> single-scale).
                                       Multi-element arrays reserve API
                                       space for a future MS-SSIM
                                       extension and currently throw a
                                       not-yet-implemented exception in
                                       BeforeGenerate.

== Test strategy ==

The filter ships with 30 GoogleTest assertions in
itkStructuralSimilarityImageFilterGTest.cxx, organized into four classes
of expected values that decouple correctness checks from sensitivity to
the discrete-Gaussian implementation:

  Class 1 -- mathematical identities (kernel-independent, tolerance 1e-9)
    SSIM(x, x) = 1 exactly for any image (constant, random, gradient).
    SSIM is symmetric: SSIM(a,b) == SSIM(b,a).

  Class 2 -- closed-form analytic checks for constant inputs
    Constant inputs make all variances and the covariance vanish, so
    SSIM(constant_a, constant_b) = (2*a*b + C1) / (a^2 + b^2 + C1).
    Tested at (100, 150) -> 0.9230923 and the textbook (0, 255) ->
    0.0000999900.  Verified pixel-wise across the output map (every
    map element matches the closed form).

  Class 3 -- input validation (exception tests)
    Mismatched input sizes, missing inputs, non-positive sigma,
    non-positive dynamic range, empty ScaleWeights, multi-element
    ScaleWeights (MS-SSIM not yet implemented).

  Class 4 -- qualitative properties
    Result range bounded in [-1, 1].
    Monotonic decay of mean SSIM as additive Gaussian noise grows
    (sigma = 2 -> 8 -> 24).
    Strong anti-correlation for negated images (SSIM(x, 255-x) < -0.5).

  Class 5 -- cross-checks against scikit-image (loose tolerance 5e-3)
    Two reference values were computed offline against
    skimage.metrics.structural_similarity with gaussian_weights=True,
    sigma=1.5, use_sample_covariance=False, data_range=255, win_size=11
    (i.e. the canonical Wang configuration):
      gradient + 30 luminance shift -> 0.9676912545
      gradient * 0.5  contrast      -> 0.7550069937
    The 5e-3 tolerance absorbs minor discretization differences between
    ITK's GaussianOperator and scipy's sampled Gaussian (the two
    libraries do not produce bit-identical Gaussian kernels).

  Class 6 -- code-path equivalence
    The simplified-product fast path (alpha=beta=gamma=1) and the
    general l^alpha*c^beta*s^gamma path are exercised on the same
    inputs and required to agree to 1e-6.

  Class 7 -- multi-dimensional and pixel-type coverage
    3D and 4D variants of the identity and constant-input tests.
    Default DynamicRange is correct for unsigned char (255), unsigned
    short (65535), and float (1.0).

The reference values for Class 5 were generated with the following
script (commit history; not shipped):

  import numpy as np
  from skimage.metrics import structural_similarity as ssim
  def ref(x, y, L=255.0):
      return ssim(x, y, gaussian_weights=True, sigma=1.5,
                  use_sample_covariance=False, data_range=L,
                  win_size=11, K1=0.01, K2=0.03)

== Results ==

Local build with GCC 13.3 / Ninja / Release on Ubuntu 24.04:

  $ cmake --build build-ssim -j48 --target ITKImageCompareGTestDriver
  [4/4] Linking CXX executable bin/ITKImageCompareGTestDriver

  $ ./bin/ITKImageCompareGTestDriver \\
        --gtest_filter='StructuralSimilarityImageFilter.*'
  [==========] 30 tests from 1 test suite ran. (60 ms total)
  [  PASSED  ] 30 tests.

  $ ctest -R 'StructuralSimilarity' --output-on-failure
  100% tests passed, 0 tests failed out of 30
  Total Test time (real) = 0.30 sec

ITKImageCompareHeaderTest1 (the auto-generated header self-test) and the
existing ITKImageCompareTestDriver also link cleanly with the new module
dependencies.

pre-commit (gersemi, clang-format, kw-pre-commit, etc.) reports all
checks Passed on every touched file.

== Module dependency changes ==

ITKImageCompare/itk-module.cmake gains:
  - ITKSmoothing as a COMPILE_DEPENDS  (for DiscreteGaussianImageFilter)
  - ITKGoogleTest as a TEST_DEPENDS    (for the new GTest driver)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hjmjohnson hjmjohnson force-pushed the copilot/add-structural-similarity-filter-clean branch from c4721ab to 80a2e9a Compare April 12, 2026 02:35
@hjmjohnson
Copy link
Copy Markdown
Member Author

@greptileai review this draft before I make it official

@hjmjohnson hjmjohnson marked this pull request as ready for review April 12, 2026 02:48
hjmjohnson added a commit to hjmjohnson/ITKRemoteAnalysis that referenced this pull request Apr 14, 2026
N-dimensional, multi-threaded ITK filter for the Structural Similarity
Index Measure (Wang et al., IEEE TIP 2004). Two inputs in, a per-pixel
SSIM map out, and a scalar mean SSIM available via GetMeanSSIM() after
Update().

Restructured from ITK PR #6034 (in-tree ImageCompare addition) to
standalone remote module format for ITKRemoteAnalysis category repo.

Original PR: InsightSoftwareConsortium/ITK#6034
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Filtering Issues affecting the Filtering module area:Python wrapping Python bindings for a class type:Enhancement Improvement of existing methods or implementation type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Structural similarity index measure image filter

4 participants