ENH: Add StructuralSimilarityImageFilter for SSIM image quality#6034
Conversation
|
| 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
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.hxx
Show resolved
Hide resolved
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.hxx
Show resolved
Hide resolved
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.hxx
Show resolved
Hide resolved
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.h
Show resolved
Hide resolved
|
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 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. |
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,1Anything 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 |
|
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? |
|
@dzenanz I've got a lot going on today. Will try to address this weekend. |
|
The lone failing check (
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. |
|
/azp run ITK.Linux |
blowekamp
left a comment
There was a problem hiding this comment.
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?
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.hxx
Show resolved
Hide resolved
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.hxx
Outdated
Show resolved
Hide resolved
Modules/Filtering/ImageCompare/include/itkStructuralSimilarityImageFilter.h
Show resolved
Hide resolved
|
Updated diagnosis: the The root cause: The output iterator was a plain Fix (commit Local verification: No deprecation warnings, clean build. All 30 SSIM GTests still pass under the legacy-removed build with the new iterator. |
f431686 to
c4721ab
Compare
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>
c4721ab to
80a2e9a
Compare
|
@greptileai review this draft before I make it official |
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
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()afterUpdate().Resources consulted
The implementation and tests follow the canonical SSIM formulation. Sources used while building this PR:
utlive/ssim/ssim_index.m) —mu = filter2(window, ...),sigma_xy = filter2(window, x*y) - mu_x*mu_y, the simplified-product fast path withK1=0.01,K2=0.03,L=255defaults._structural_similarity.py— recommended configuration (gaussian_weights=True,sigma=1.5,use_sample_covariance=False), thecropby(win_size-1)/2border before averaging the SSIM map, thecov_normdistinction between sample and population covariance.ScaleWeightsarray) is reserved here for a follow-up 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$ ):
The three SSIM components (with$C_1 = (K_1 L)^2$ , $C_2 = (K_2 L)^2$ , $C_3 = C_2/2$ ):
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:
Architecture
Composite ImageToImageFilter:
DiscreteGaussianImageFilterpasses (forMultiThreaderBase::ParallelizeImageRegionreads from the five smoothed buffers and writes the SSIM map.API (covers all of #6030)
GaussianSigma1.5MaximumKernelWidth11K1/K20.01/0.03DynamicRange1.0for float/double,255foruchar,65535forushort, etc.LuminanceExponent1.0ContrastExponent1.0StructureExponent1.0ScaleWeights{1.0}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_similaritywithgaussian_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
GaussianOperatorand scipy's sampled Gaussian:Mathematical identities (kernel-independent, tolerance
1e-9)SSIM(x, x) = 1exactly for constant, random, and gradient imagesSSIM(a,b) == SSIM(b,a)Closed-form analytic checks for constant inputs (tolerance
1e-9)(100, 150) → 0.9230923and the textbook(0, 255) → 0.0000999900Input validation (exception tests)
ScaleWeights, multi-elementScaleWeights(MS-SSIM not yet implemented)Qualitative properties
SSIM(x, 255-x) < -0.5)scikit-image cross-checks (tolerance
5e-3)0.96769125450.7550069937GaussianOperatorand scipy's sampled Gaussian (the two libraries do not produce bit-identical Gaussian kernels)Code-path equivalence
1e-6on the same inputsMulti-dimensional and pixel-type coverage
DynamicRangecorrect forunsigned char,unsigned short,floatunsigned charidentical-image identity testResults
Local build with GCC 13.3 / Ninja / Release on Ubuntu 24.04, 48 cores:
ITKImageCompareHeaderTest1(the auto-generated header self-test) and the existingITKImageCompareTestDriveralso 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.cmakeadds:ITKSmoothingas aCOMPILE_DEPENDS(forDiscreteGaussianImageFilter)ITKGoogleTestas aTEST_DEPENDS(for the new GTest driver)Files
Test plan
ctest -R StructuralSimilarity)ITKImageCompareHeaderTest1(header self-test) compiles and linksITKImageCompareTestDriver(full classic test driver) compiles and linkspre-commit runclean on all touched files (gersemi, clang-format, kw-pre-commit)itkStructuralSimilarityImageFilter.wrapprovided forWRAP_ITK_REAL, 2D)Future work (out of scope here)
ScaleWeightsAPI 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