Commit aa5fd89
ENH: Add StructuralSimilarityImageFilter for SSIM image quality
Implements the Structural Similarity Index Measure as an N-dimensional,
multi-threaded ITK filter in the ImageCompare module. Closes #6030,
supersedes #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 #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>1 parent 8483b54 commit aa5fd89
6 files changed
Lines changed: 1392 additions & 0 deletions
File tree
- Modules/Filtering/ImageCompare
- include
- test
- wrapping
Lines changed: 299 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
0 commit comments