InsightSoftwareConsortium / InsightSoftwareConsortium/ITK

ENH: Fortify against the recurring single-axis `RecursiveGaussianImageFilter` misuse (isotropic-intent defect class)

Open
#6,721 0 comments 0 reactions 0 assignees View on GitHub
type:Enhancement type:Infrastructure
Dominant language
C++
Stars
1.7k
Forks
748
Avg merge
1d 1h
Merged PRs (30d)
64

Description

`itk::RecursiveGaussianImageFilter` smooths along **exactly one axis** — the one set by `SetDirection()`, which defaults to `0` and is silently valid if never set. Developers repeatedly reach for it wanting an *isotropic* blur, call only `SetSigma()`, and ship a filter that blurs axis 0 only. Per the maintainer this has recurred for ~15 years; #6602 (from #6575 B22) is only the most recent instance.

That instance is fixed. This issue tracks the **systemic** fortification — docs, a static matcher, an audit, and a reusable test harness — so the next instance is caught before it lands.

Why the defect is invisible in review and in CI

The correct isotropic choice is `itk::SmoothingRecursiveGaussianImageFilter`, which internally chains one `RecursiveGaussianImageFilter` per axis. The wrong choice still *looks* right: on any fixture that varies along axis 0 the blur visibly "works". It only manifests on data varying along a non-first axis — so it survives code review, passes hand inspection, and gets baked into committed baselines.

The #6602 instance, both pre-fix call sites in `Modules/Filtering/AnisotropicDiffusionLBR/include/itkStructureTensorImageFilter.hxx`:

- `:75` — noise-scale `K_sigma` smoother (unreachable branch)
- `:124` — feature-scale `K_rho` smoother (always active; the live bug)

Both did:

```cpp
using GaussianFilterType = RecursiveGaussianImageFilter<...>;
auto smoother = GaussianFilterType::New();
smoother->SetSigma(...); // no SetDirection(), no per-axis loop
```

i.e. axis-0 only. The fix swapped both to `SmoothingRecursiveGaussianImageFilter` and regenerated 16 baselines (ITKTestingData #77).

Corpus scan (re-derived on main, 2026-07-29)

```
git grep -lE "(^|[^g])RecursiveGaussianImageFilter<" -- 'Modules/*.h' 'Modules/*.hxx' 'Modules/*.cxx'
# => 63 files instantiate RecursiveGaussianImageFilter directly
# 26 of them outside test/ and example/ — the real audit surface
```

Note: a naive `grep -viE "Smoothing"` path filter reports 62, but it filters on *filename*, not on the instantiated template, so it both misses and mis-attributes files. Use the word-boundary form above.

The **majority are correct by design** — genuinely directional filters, or filters that loop `SetDirection()` over every dimension:

- `itkGradientRecursiveGaussianImageFilter.{h,hxx}` — loops `SetDirection` (canonical-correct reference)
- `itkHessianRecursiveGaussianImageFilter.{h,hxx}`, `itkLaplacianRecursiveGaussianImageFilter.{h,hxx}`, `itkGradientMagnitudeRecursiveGaussianImageFilter.{h,hxx}` — directional by design

Two previously-suspected displacement-field smoothers were **checked and are correct** — they wrap `SetSigma`/`SetDirection(dim)` in a `for (dim < ImageDimension)` loop:

- `Modules/Registration/PDEDeformable/include/itkMultiResolutionPDEDeformableRegistration.hxx:372-382`
- `Modules/Registration/VariationalRegistration/include/itkVariationalRegistrationMultiResolutionFilter.hxx:324-334`

The remaining non-test users still to be triaged include `itkMultiScaleHessianEnhancementImageFilter.h`, `itkCuberilleImageToMeshFilter.h`, `itkMultiScaleHessianBasedMeasureImageFilter.h`, `itkUnsharpMaskImageFilter.h`, `itkImageToImageMetric.h`, `itkPointSetToImageMetric.h`, `itkDefaultImageToImageMetricTraitsv4.h`, `itkJointHistogramMutualInformationImageToImageMetricv4.h`, `itkVectorImageToImageMetricTraitsv4.h`, `itkLevelSetMotionRegistrationFunction.h`, `itkImageRegistrationMethodv4.hxx`, `itkTwoImageToOneImageMetric.h`, `itkCurvesLevelSetFunction.hxx`, `itkGeodesicActiveContourLevelSetFunction.hxx`, `itkGeodesicActiveContourShapePriorLevelSetFunction.hxx`, `itkLevelSetEquationAdvectionTerm.hxx`.

Deliverable 1 — Doxygen \warning (cheapest, highest leverage — do this first)

Catch the mistake at the point of API discovery. On `itk::RecursiveGaussianImageFilter`:

```
* \warning This filter smooths along a **single** axis only — the one selected by
* SetDirection(), which defaults to 0 and is silently valid if never set. It is
* **not** an isotropic Gaussian blur. For isotropic smoothing use
* itk::SmoothingRecursiveGaussianImageFilter, which chains one
* RecursiveGaussianImageFilter per image dimension. Using this filter with only
* SetSigma() set will blur axis 0 and leave every other axis untouched.
*
* \sa SmoothingRecursiveGaussianImageFilter
```

and the reciprocal `\note` on `itk::SmoothingRecursiveGaussianImageFilter`:

```
* \note This is the isotropic (all-axes) recursive Gaussian smoother. Prefer it over
* RecursiveGaussianImageFilter unless you specifically want single-axis smoothing.
*
* \sa RecursiveGaussianImageFilter
```

Deliverable 2 — clang-query / clang-tidy matcher sketch

Flag: an object of `RecursiveGaussianImageFilter` on which `SetSigma` is called but **no** `SetDirection` is reachable on the same variable, and which is **not** inside a loop over `ImageDimension`.

Start interactively in `clang-query` to find the candidate variables:

```
set output detailed-ast
match varDecl(
hasType(hasCanonicalType(qualType(hasDeclaration(classTemplateSpecializationDecl(
hasName("::itk::SmartPointer"),
hasTemplateArgument(0, refersToType(hasDeclaration(
namedDecl(matchesName("::itk::RecursiveGaussianImageFilter"))))))))))
).bind("smootherVar")
```

then, in the same TU, the two member calls keyed to that variable:

```
match cxxMemberCallExpr(
callee(cxxMethodDecl(hasName("SetSigma"))),
on(expr(hasDescendant(declRefExpr(to(varDecl(equalsBoundNode("smootherVar")))))))
).bind("setSigmaCall")

match cxxMemberCallExpr(
callee(cxxMethodDecl(hasName("SetDirection"))),
on(expr(hasDescendant(declRefExpr(to(varDecl(equalsBoundNode("smootherVar")))))))
).bind("setDirectionCall")
```

Because "no call anywhere on this variable" is a *negative* whole-function property that AST matchers express poorly, follow the hybrid discovery model already proven by the **`itk-sizetype-filled`** skill: let clang-query emit the raw `smootherVar` / `setSigmaCall` / `setDirectionCall` bindings with source locations, then do the pairing and negation in a small Python pass:

1. group bindings by (file, enclosing function, variable);
2. drop any group that has a `setDirectionCall`;
3. drop any group whose `setSigmaCall` location is lexically inside a `for` whose condition mentions `ImageDimension` / `VDimension` / `Dimension`;
4. rank what remains.

Expect false positives on genuinely-directional single-axis uses (`itkLaplacianRecursiveGaussianImageFilter` and friends). **Rank and report — never auto-rewrite.** If the signal/noise is good enough, promote it to a `readability-`-style clang-tidy check under `Utilities/`.

Deliverable 3 — reusable "isotropic smoother responds on every axis" GTest harness

Generalize the `FeatureScaleSmoothsAlongAllAxes` tripwire added by #6602 into a fixture any filter claiming isotropic smoothing can instantiate. The idea: for each axis `d`, build an input whose only variation is along `d` (a step or an impulse plane), run the filter, and assert the output actually changed along `d`.

```cpp
// Modules/Core/Common/test/ (or a shared testing header)
template
void
ExpectIsotropicResponseOnEveryAxis(TFilterFactory makeFilter, double sigma)
{
using ImageType = typename TFilterFactory::ImageType;
constexpr unsigned int D = ImageType::ImageDimension;

for (unsigned int axis = 0; axis < D; ++axis)
{
// Input varies ONLY along `axis`: a step at the midpoint of that axis.
auto input = MakeAxisAlignedStepImage(axis);

auto filter = makeFilter(sigma);
filter->SetInput(input);
filter->Update();

// A smoother that ignores `axis` reproduces the sharp step exactly.
EXPECT_GT(MaxAbsDifference(input, filter->GetOutput()), tolerance)
<< "filter did not smooth along axis " << axis
<< " -- likely single-axis RecursiveGaussianImageFilter misuse";
}
}
```

The load-bearing property is the `for (axis)` loop: an axis-0-only smoother passes `axis == 0` and fails every other axis, which is exactly the signature of this defect class. Any 1-D-only fixture, and any 2-D fixture that happens to vary along axis 0, is blind to it.

Deliverable 4 — audit the 26 non-test direct users

Triage each into {correct-directional-by-design, correct-per-axis-loop, suspect-isotropic-intent}. For anything landing in the third bucket, fix by switching to `SmoothingRecursiveGaussianImageFilter` and add the Deliverable-3 harness for it — expect baseline regeneration, as #6602 needed 16 new baselines.

Deliverable 5 (evaluate, may be rejected) — runtime debug guard

Consider having `RecursiveGaussianImageFilter` emit a one-time debug warning when `Update()` runs on a >1-D image with a never-explicitly-set direction. **Assess before pursuing** — there are many legitimate direction-0 uses, so this is likely too noisy to enable by default. If pursued, gate it behind `SetDebug(true)` rather than making it unconditional.

Related

- #6602 — "BUG: Smooth structure tensor with an isotropic Gaussian kernel" (merged 2026-07-16) — the latest instance; its `FeatureScaleSmoothsAlongAllAxes` GTest is the reusable tripwire pattern.
- #6575 item B22 — the audit entry that surfaced #6602.
- ITKTestingData #77 — the 16 regenerated baselines for #6602.
- Correct-pattern reference: `itkGradientRecursiveGaussianImageFilter`, which loops `SetDirection()` over all axes.

Contributor guide

Open the contributing guide

Research direction

Start with the documentation for itk::RecursiveGaussianImageFilter and itk::SmoothingRecursiveGaussianImageFilter, then run the stated git grep audit over Modules/*.h, *.hxx, and *.cxx. Read the existing FeatureScaleSmoothsAlongAllAxes test and the itk-sizetype-filled clang-query skill before evaluating the matcher and reusable test harness. Done means the documented warning, ranked audit, appropriate checks, and axis-sensitive coverage are in place without auto-rewriting legitimate directional uses.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
documentation, testing, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.