InsightSoftwareConsortium / InsightSoftwareConsortium/ITK

Quirks and inconsistencies collected while porting ~60 filters (Q1-Q118, split from #6575)

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

Description

Split out of #6575 so that the bug ledger there can be driven to completion. Every **quirk** (Q1–Q118) from that issue is reproduced verbatim below; #6575 keeps the 92 suspected **bugs**.

These are explicitly **lower priority than #6575** and are not intended to be worked on until the bug ledger is closed out. Nothing here is a known-wrong-output defect — each was judged possibly intended, historical, or not worth changing, and is recorded so the analysis is not lost.

## Status

| Bucket | Count |
|---|---|
| Dispositioned or fixed (all merged) | 16 of 118 |
| Untriaged | 102 of 118 |

Already handled (these landed with the bug work in #6575, before the split):

| Quirk(s) | Where |
|---|---|
| Q25–Q38 | #6671 — displacement-field / warp / demons dispositions (DOC/STYLE), **merged** 2026-07-22 |
| Q82 | #6681 — `NrrdImageIO` warns when a NRRD space is used verbatim, **merged** 2026-07-23 |
| Q84 | #6674 — NrrdIO writes comments and key/value pairs before the fields so a LIST-form `data file:` stays last, **merged** 2026-07-17 |

## Provenance

Original context from #6575: found while porting SimpleITK's filter surface to another language, by cross-checking an independent reimplementation against the ITK sources. Verified at source level against `main` at `e46eb723a5`; exact `file:line` and analysis exist for every row. Items whose original analysis did not survive re-verification were corrected or dropped before filing.

---

These were verified the same way but may be judged intended, historical, or not worth changing — listed for the record, one line each, citations available on request (I have exact file:line and analysis for every row).

Verified quirks Q1–Q118 (table)

| # | Site | Behavior |
|---|---|---|
| ⬜ Q1 | Deconvolution family | The same `KernelZeroMagnitudeThreshold` ε is compared against \|H\| by Inverse but against the \|H\|²-carrying denominator by Tikhonov/Wiener — "regularization → 0 equals inverse" fails in the band ε ≤ \|H\| < √ε. |
| ⬜ Q2 | `DiscreteGaussianDerivativeImageFilter` | Never calls `FlipAxes()`, unlike `DerivativeImageFilter`/`GradientImageFilter` (which do) — odd-order derivatives carry the opposite sign vs. its siblings. Also the inter-axis intermediate is `Image`, so integer outputs truncate per axis pass (the local comments say "real to real"). |
| ⬜ Q3 | `ThresholdSegmentationLevelSetImageFilter` | The class doc (`.h:63-65`) states positive = inside — the opposite of the convention its own base class documents (`itkSegmentationLevelSetImageFilter.h:82-84`: negative inside). |
| ⬜ Q4 | Segmentation level-set functions | `CurvatureSpeed` is overridden to `PropagationSpeed` only by GeodesicActiveContour and ShapeDetection; Threshold/Laplacian/Canny keep the base-class constant 1 — an undocumented asymmetry across siblings. |
| ⬜ Q5 | `BinaryContourImageFilter` | Non-foreground pixels keep their original values verbatim (`.hxx:164-179`) — they are not normalized to `BackgroundValue`, so several distinct non-foreground input values all survive into the output. |
| ⬜ Q6 | `SimpleContourExtractorImageFilter` | `InputForegroundValue == InputBackgroundValue` marks every foreground pixel as contour (the neighborhood scan includes the center pixel, `.hxx:84`). |
| ⬜ Q7 | `BinaryReconstructionByErosionImageFilter` | Built on the complement via `BinaryNot`; `BackgroundValue` is consumed only as a self-consistent internal sentinel — the parameter has no effect on the output. |
| ⬜ Q8 | `AnisotropicDiffusionImageFilter` | An unstable time step warns and proceeds; the clamp is present but commented out (`.hxx:68`). |
| ⬜ Q9 | `ThresholdMaximumConnectedComponentsImageFilter` | `m_LowerBoundary` is written once in the constructor and read only by `PrintSelf` — dead configuration with no setter. |
| ⬜ Q10 | `MinMaxCurvatureFlowFunction` | `SetStencilRadius` clamps to ≥ 1 (`.hxx:43`), making the `radius == 0` early-outs (`.hxx:217-220`, `276-279`) unreachable dead code. |
| ⬜ Q11 | `ScalarImageKmeansImageFilter` | Overrides the estimator's defaults in `GenerateData` (`SetMaximumIteration(200)`, threshold 0.0, `.hxx:91-92`), invisibly to users; and `KdTreeBasedKmeansEstimator`'s loop tests the limit pre-increment after the refinement pass (`.hxx:327-347`) — `MaximumIteration = n` runs n+1 passes while `GetCurrentIteration()` reports n. |
| ⬜ Q12 | `SLICImageFilter` | Enforce-connectivity relabels an undersized component to the previously-encountered label, whatever it is (`.hxx:544-546`); there is no final relabel/compaction pass; per-pixel distances accumulate in `float`. |
| ⬜ Q13 | `BoxMean`/`BoxSigmaImageFilter` | The window crops at the image border and divides by the cropped count (`itkBoxUtilities.h:322-324`, `499-501`) — border statistics use fewer samples. A `ZeroFluxNeumannBoundaryCondition` is declared (L215/389) but never used, suggesting replication was once intended. |
| ⬜ Q14 | `BinShrinkImageFilter` | Throws when a shrink factor exceeds the axis size where `ShrinkImageFilter` silently clamps to 1 (`.hxx:289-296` vs `itkShrinkImageFilter.hxx:265-267`); integer outputs round half-up while float outputs truncate (`RoundIfInteger`, `.h:135-149`). |
| ⬜ Q15 | `AntiAliasBinaryImageFilter` | A constant input (even all-maximum) shifts to 0 under the `max − (max−min)/2` iso-value and the strict `> 0` sign test (`.hxx:109`; base `.hxx:605`) — the output is uniformly negative, i.e. "all inside". |
| ⬜ Q16 | `ReinitializeLevelSetImageFilter` | Narrow-band mode pre-fills the whole output with `±NumericTraits::max()` and only overwrites band points (`.hxx:206-225`) — far-field pixels keep ±max rather than a distance-like magnitude. |
| ⬜ Q17 | `FastMarchingUpwindGradientImageFilter` | `m_TargetValue` is overwritten at every accepted point in NoTargets mode; in Some/AllTargets modes the reached test is sticky, so every later accepted point — target or not — keeps advancing it; OneTarget reports the last reached target, not the first (`.hxx:154-227`). |
| ⬜ Q18 | `CollidingFrontsImageFilter` | `ApplyConnectivity`'s flood fill is seeded on `SeedPoints1` alone (`.hxx:114-122`), so the forced −1e-6 at every `SeedPoints2` seed (`.hxx:92-97`) is dropped from the output unless that seed is face-connected to front 1's region. |
| ⬜ Q19 | `itk::watershed::Segmenter` | Edge lists are sorted with a height-only `operator<` (`itkWatershedSegmentTable.h:73-82`); a NaN height compares "equivalent" to everything — a strict-weak-ordering violation (formal UB; benign merge-equal in libstdc++). |
| ⬜ Q20 | `itkMath.h FloatAlmostEqual` | The absolute-difference test (default `0.1 · epsilon`) returns equal **before** the signbit check (`itkMath.h:334-352`) — `0.0 == -0.0`, and near-zero pairs millions of ULPs apart compare equal, partially defeating the documented ULP semantics. |
| ⬜ Q21 | `MultiphaseDenseFiniteDifferenceImageFilter` | Computes the CFL time step then discards it: `timeStep = 0.08; // FIXME !!! After all this work, assign a constant !!! Why ??` (`.hxx:153-154`). |
| ⬜ Q22 | Chan-Vese `UseImageSpacing` | Builds `m_ScaleCoefficients` that nothing in the Chan-Vese chain reads; the PDE divides by `m_InvSpacing` from the *feature* image unconditionally while the re-distancing Maurer map inherits the *level-set* image's spacing — the flag's only live effect is `maurer->SetUseImageSpacing` (`.hxx:227`). Two images with different spacings drive the PDE and the re-distancing with different metrics. |
| ⬜ Q23 | `ScalarChanAndVeseDenseLevelSet` RMS/labeling | `m_ReinitializeCounter` defaults to 1, so the reinit branch runs every iteration, zeroes the RMS accumulator, and refills it with the re-distancing residual (`.hxx:214-248`) — `MaximumRMSError` halting compares against the wrong quantity. `CopyInputToOutput` labels `phi < 0` strictly (`.hxx:69`), so exactly-zero boundary pixels are excluded from the label. |
| ⬜ Q24 | `ObjectMorphologyImageFilter` | `BeforeThreadedGenerateData`'s fill-then-conditional-copy always reduces to `output := input` (the fill sentinel is constructed to differ from `ObjectValue`, `.hxx:87-113`); `UseBoundaryCondition` is dead configuration (installed but never enabled, `.h:145` even warns "Don't forget to set UseBoundaryCondition to true!"); boundary detection is a hard-coded 3^dim box decoupled from the kernel radius (`.hxx:130-198`) — dilation is unaffected, erosion diverges substantially from true erosion (the header caveats this). |
| ✅ Q25 | `IterativeInverseDisplacementFieldImageFilter` | the coordinate-descent probe step is axis 0's spacing on every axis |
| ✅ Q26 | `DisplacementFieldJacobianDeterminantFilter` | `SetDerivativeWeights` silently flips `UseImageSpacing` off — precedence between the two is last-writer-wins, while the class doc describes only one ordering |
| ✅ Q27 | `WarpImageFilter` | `m_OutputSize[0] == 0` alone keys "size unset"; output-grid defaults are unit/zero/identity from neither input; the field is sampled with edge replication, never out-of-domain |
| ✅ Q28 | `VectorConnectedComponentImageFilter` | zero vectors join anything under the default threshold (no special case); concept macro name typo `InputValyeTypeIsFloatingCheck` |
| ✅ Q29 | `MahalanobisDistanceMembershipFunction` | the `det < 0` exception branch is unreachable; `GetCovariance()` never reflects the singular-case substitute |
| ✅ Q30 | `FastSymmetricForcesDemonsRegistrationFilter` | its difference function is `ESMDemonsRegistrationFunction`; the identically-named `FastSymmetricForcesDemonsRegistrationFunction` class is orphaned from its own filter |
| ✅ Q31 | `ExponentialDisplacementFieldImageFilter` | "take the ceil" comment vs `+ 1.0`-then-truncate code (integral values overshoot by 1); a zero field still runs one squaring iteration |
| ✅ Q32 | `WarpVectorImageFilter` (as wired by `DiffeomorphicDemonsRegistrationFilter`) | under the nearest-neighbor-extrapolating interpolator, `IsInsideBuffer` is always true and `m_EdgePaddingValue` is dead code |
| ✅ Q33 | `ESMDemonsRegistrationFunction` | the `WarpedMoving` and `MappedMoving` gradient options coincide exactly at a zero displacement field (interior pixels, default linear interpolation, coincident grids) |
| ✅ Q34 | `LevelSetMotionRegistrationFilter/Function` | `dt = 1/maxL1` makes the applied step at the governing pixel exactly `Alpha`-independent; thresholded pixels count toward the metric while out-of-buffer pixels don't; `Halt` adds an RMS==0 clause the base can never fire |
| ✅ Q35 | `LevelSetMotionRegistrationFunction` | one-sided difference steps add index-axis spacing to a *physical* coordinate — wrong direction and step under non-identity direction cosines |
| ✅ Q36 | demons family | zero-iteration runs report `GetRMSChange() == 0` but `GetMetric() == NumericTraits::max()` in the two filters that don't override `GetRMSChange` |
| ✅ Q37 | `LevelSetMotionRegistrationFunction` smoothing | the smoothed moving image is re-quantized to the moving image's own pixel type (`u8` in → `u8` smoothed) before the gradient; and any axis shorter than 4 pixels is a hard error for this family member only |
| ✅ Q38 | `FastMarchingImageFilterBase::IsChangeWellComposed3D` | builds its neighborhood iterator over `GetRequestedRegion()` (empty, never set) where the 2-D sibling and the strict-topology check use `GetBufferedRegion()` — harmless (only `SetLocation`+`GetPixel` are used) but inconsistent (`.hxx:779` vs `:583`/`:619`) |
| ⬜ Q39 | `LabelToRGBImageFilter` | background is always fixed black (`m_BackgroundColor` zero-filled at construction); the functor comment promising a "gray pixel matching the background intensity" describes `LabelOverlayFunctor`, not this class (`itkLabelToRGBFunctor.h:93-98`) |
| ⬜ Q40 | `LabelToRGB`/`LabelOverlay` palette | the 0–255 palette constants are rescaled by `NumericTraits::max()` rather than a fixed 255 (`itkLabelToRGBFunctor.h:110-117`) — a `double` RGB output scales the palette into `[0, ≈1.8e308]` |
| ⬜ Q41 | `HessianToObjectnessMeasureImageFilter` | eigenvalues are sorted by \|λ\| with unstable `std::sort` (equal-magnitude order unspecified, `.hxx:78-79`); `alpha == 0` skips only the R_A factor while `beta == 0` (or a zero denominator) zeroes the whole measure (`.hxx:111-151`) — asymmetric parameter semantics |
| ⬜ Q42 | `ComplexToModulusImageFilter` | computes `sqrt(re*re + im*im)` in the component type (`.h:46-50`): `ComplexFloat32` overflows to inf above \|component\| ≈ 1.8e19 and flushes to 0 near denormals, where `std::hypot` would not |
| ⬜ Q43 | `RelabelLabelMapFilter` | class doc says objects are reassigned via `PushLabelObject`; the inherited implementation uses `SetLabel` + `AddLabelObject` against a counter from 0 (`itkAttributeRelabelLabelMapFilter.hxx:64-79`) |
| ⬜ Q44 | `LabelMap::HasLabel` | doc says the background label also returns true; the implementation is a plain `find(label) != end()`, which returns false for background (`.hxx:151-153`) |
| ⬜ Q45 | `StatisticsLabelMapFilter` weighted moments | adds `spacing²/12` (the pixel's own second moment) to every central-moment diagonal (`.hxx:224-228`) while `ShapeLabelMapFilter` does not — `WeightedPrincipalMoments` exceed `PrincipalMoments` by 1/12 per eigenvalue even on a constant feature image |
| ⬜ Q46 | `StatisticsLabelMapFilter` extremum indices | min/max index updates are non-strict (`v <= min`, `v >= max`, `.hxx:118-127`) — on ties the recorded index is the *last* tie in raster order |
| ⬜ Q47 | `itk::Statistics::Histogram::Initialize` | computes the bin interval and every non-final bin bound in `float` (`itkHistogram.hxx:225-236`) — a `Histogram` gets float-quantized bin edges, which `StatisticsLabelMapFilter`'s `Median` inherits |
| ⬜ Q48 | `ImageBase::IsCongruentImageGeometry` | scales the coordinate tolerance by `this->GetSpacing()[0]` only (`itkImageBase.hxx:400-405`) — the predicate is asymmetric when the two images differ in axis-0 spacing, and axes 1+ are judged against axis 0's scale |
| ⬜ Q49 | `DivideImageFilter::Div` | a near-zero divisor returns `NumericTraits::max()` regardless of the numerator's sign or magnitude (`itkArithmeticOpsFunctors.h:142-151`); through `NormalizeToConstantImageFilter` a zero-sum input sends the *entire output* to the type maximum |
| ⬜ Q50 | `VotingBinaryHoleFillingImageFilter` | the else-branch stamps `it.Set(foregroundValue)` for *any* center value other than `backgroundValue` (`.hxx:138-141`) — a third pixel value is silently promoted to foreground without counting toward `GetNumberOfPixelsChanged()`; the iterative sibling inherits this |
| ⬜ Q51 | `ExtractImageFilter` | the output origin ignores a collapsed axis's start index (and the collapsed↔retained direction coupling), so extracting slice k of an oblique volume reports slice 0's origin (`.hxx:74-85`, `:162-179`) |
| ⬜ Q52 | `CenteredVersorTransformInitializer::ComputeRotation` | uses `R = M·Fᵀ` (`.hxx:47`), which does not satisfy the covariance-aligning identity `R·Cov_f·Rᵀ = Cov_m` (that requires `Mᵀ·F`); with per-image independent eigenvector sign canonicalization the initializer can 180°-flip an axis |
| ⬜ Q53 | `CompositeTransform` | `Get/SetParameters`, `Get/SetFixedParameters`, and `UpdateTransformParameters` all flatten sub-transforms in *reverse* queue order (last-added first, `.hxx:613,665-687,703,734,847`) while `GetNthTransform(n)` indexes forward — two orderings in one class |
| ⬜ Q54 | `FFTPadImageFilter` | `SizeGreatestPrimeFactor == 1` pads each axis to an *even* size (`.hxx:64-68`) while the class doc says "1 or less … disable the extra padding" (`.h:90`) — only 0 disables |
| ⬜ Q55 | `MaskedFFTNormalizedCorrelationImageFilter` | pads FFT sizes using only factors 2/3/5 (`.hxx:528-545`) while `FFTConvolutionImageFilter` uses the backend's greatest prime factor (11 under PocketFFT) — identical inputs get different padded sizes across the two FFT-based filters |
| ⬜ Q56 | `RegistrationParameterScalesFromIndexShift` | the step scale is computed in continuous-index (voxel) units while `MaximumStepSizeInPhysicalUnits` defaults to the minimum virtual spacing (mm) — the gradient-descent learning rate divides mm by voxels, dimensionally mixed on anisotropic spacing (`.hxx:66-79`; `itkGradientDescentOptimizerv4.hxx:272`) |
| ⬜ Q57 | `RegistrationParameterScalesEstimator` `CentralRegionRadius` | consulted only by `CentralRegionSampling`, which is selected only for displacement-field/B-spline transforms (`.hxx:346-367`, `:203-211`); linear/affine transforms take Corner/Random sampling and never read it, and for displacement fields the scales estimate is radius-independent |
| ⬜ Q58 | `ObjectToObjectOptimizerBase` weights | optimizer weights within `1e-4` of 1.0 are flagged identity and never multiplied into the gradient (strict `>`, so exactly `1e-4` is also discarded, `itkObjectToObjectOptimizerBase.cxx:154-164`); the sibling scales-identity test uses a 100× looser `0.01` |
| ⬜ Q59 | v4 optimizer weights application | every v4 optimizer validates the weights-array length in the base `StartOptimization`, but the vnl-based LBFGS/LBFGSB and Amoeba optimizers then apply only *scales* and silently ignore well-sized weights (`itkSingleValuedNonLinearVnlOptimizerv4.cxx:53-58`); `LBFGS2Optimizerv4`, by contrast, does apply them |
| ⬜ Q60 | `NiftiImageIO` | `ITK_InputFilterName` is encapsulated (`:1102`) and then wiped six lines later by `SetImageIOMetadataFromNIfTI`, whose first act is `thisDic.Clear()` (`:630`) |
| ⬜ Q61 | `NiftiImageIO` | the "original file was non-orthogonal" warning is dead: stored under `ITK_sform_corrected` (`:632`), looked up under `nifti_sform_corrected` (`:2038`) |
| ⬜ Q62 | `NiftiImageIO` | `getQFormCodeFromDictionary`/`getSFormCodeFromDictionary` results are unconditionally overwritten with `NIFTI_XFORM_SCANNER_ANAT` (`:2077-2078`) — yet their `StringToInt32` still throws on a non-numeric dictionary value that would have been discarded anyway |
| ⬜ Q63 | `NiftiImageIO` | `IMG.NII` passes `CanWriteFile` (extension check lowercases) but `WriteImageInformation` compares the raw uppercase extension against lowercase spellings only and throws "Bad Nifti file name" (`:1173-1202`) |
| ⬜ Q64 | niftilib *(vendored)* | a magic-less (Analyze-7.5) header in a single `.nii` reports `nifti_type == 1`: `convert_nhdr2nim` sets ANALYZE, then `nifti_set_type_from_names` forces NIFTI1_1 whenever `fname == iname` — true for every single-file case (`nifti1_io.c:3730`, `:3453-3454`) |
| ⬜ Q65 | `GiplImageIO` | the magic number is validated only in `CanReadFile`; `ReadImageInformation` re-reads the same 4 bytes and discards them, so a file supplied via `SetImageIO` gets no magic check (`:566-584`) |
| ⬜ Q66 | `GiplImageIO` | `NDims` is a population count of non-trivial dims — a 2-D image round-trips as 3-D `[nx, ny, 1]`, and malformed dims like `[4, 0, 5, 1]` give `NDims == 2` with the 5 silently dropped (`:294-312`) |
| ⬜ Q67 | `GiplImageIO` | `image_type` is byte-swapped only in the BigEndian arm and `min`/`max` are never swapped — unobservable, since the constructor pins BigEndian and all these header fields are read then discarded (`:78`, `:325-328`, `:443-461`) |
| ⬜ Q68 | `GiplImageIO` | registers no read/write extensions (constructor omits `AddSupported*Extension`) and its real gate is a case-sensitive suffix test — `IMAGE.GIPL` is not recognized (`:1079-1090`) |
| ⬜ Q69 | `GiplImageIO` | the compressed path uses `gzopen`/`gzread` throughout, which transparently pass through a non-gzip stream — a `.gipl.gz` containing a plain GIPL file is accepted and read verbatim |
| ⬜ Q70 | `VTKImageIO::GetNextLine` | tolerates 5 consecutive empty lines and throws on the 6th, while its message says "consecutive 5 empty lines" — off by one from its own text (`:44-46`) |
| ⬜ Q71 | `VTKImageIO::GetNextLine` | tests `eof()` before using the line, so a header whose final line lacks a trailing newline throws "Premature EOF" even though `getline` delivered the content (`:50-56`) |
| ⬜ Q72 | `VTKImageIO` | the 2-D/3-D test is written twice and the first copy is dead — the surviving rule is "2-D iff `dims[2] <= 1`", so `DIMENSIONS 4 1 3` is a one-row-tall 3-D volume (`:210-221`) |
| ⬜ Q73 | `VTKImageIO` | a round-trip collapses `ComplexFloat32` to a 2-component `VectorFloat32`, and a 1-component vector image to a scalar — the legacy format cannot express either distinction (`:701-709`, `:311-329`) |
| ⬜ Q74 | `VTKImageIO` | the component count is never range-checked on either side (writer carries a `// todo this should be asserted`, `:705`) — `COLOR_SCALARS c 7` reads as a 7-component `VectorUInt8` |
| ⬜ Q75 | `ImageIOBase` ASCII read | `is >> temp` with an integer `PrintType`: a negative literal for an unsigned pixel type wraps rather than failing, and out-of-range 8-bit values narrow silently (−1 → 255, 300 → 44) (`itkImageIOBase.cxx:804-811`) |
| ⬜ Q76 | `MetaImageIO` | complex pixels cannot round-trip: `ElementNumberOfChannels == 2` is indistinguishable from a 2-vector, and the reader forces `VECTOR` for any channels > 1 (`itkMetaImageIO.cxx:241-244`, in-source `// BUG: 8732`) |
| ⬜ Q77 | MetaIO *(vendored)* | `MetaImage::CanRead` re-tests the suffix case-sensitively against `.mha`/`.mhd` while the ITK factory and `CanWriteFile` match case-insensitively — `IMG.MHA` is writable but not readable (`metaImage.cxx:1184-1199`) |
| ⬜ Q78 | MetaIO *(vendored)* | booleans test only `value[0] == 'T'\|'t'\|'1'` — `trash` parses as true, `yes` as false (`metaObject.cxx:1605-1642`) |
| ⬜ Q79 | MetaIO *(vendored)* | header-alias precedence is `M_Read`'s apply order, not file order: `Origin` > `Position` > `Offset`, `TransformMatrix` > `Rotation` > `Orientation`, `BinaryDataByteOrderMSB` > `ElementByteOrderMSB` — a trailing `ElementByteOrderMSB = False` loses to an earlier `BinaryDataByteOrderMSB = True` (`metaObject.cxx:1618-1707`) |
| ⬜ Q80 | MetaIO *(vendored)* | writes a zlib (RFC-1950) stream but reads zlib *or* gzip (`inflateInit2(…, 47)`) — the reader accepts a superset of what the writer produces (`metaUtils.cxx:808`, `:862`) |
| ⬜ Q81 | vendored gzip readers | NrrdIO's gzio and zlib's `gz_look` fall back to a byte-for-byte copy when the gzip magic is absent, while niftilib's `nifti_is_gzfile` is a pure *name* test — a genuine gzip stream named `.nii` is never inflated, and a `.nii.gz` holding raw bytes reads via the copy fallback (`gzio.c:501-519`; `nifti1_io.c:2656-2668`) — reviewed, no change (see below) |
| ✅ Q82 | `NrrdImageIO` | only `RAS`/`LAS`/`LPS` are converted to LPS; every other `space` value (scanner-xyz, right-up, …) has its geometry used verbatim through an empty `default:` with no warning (`itkNrrdImageIO.cxx:766-786`) — **fixed: #6681, merged** |
| ⬜ Q83 | NrrdIO *(vendored)* | `data file: LIST` reads filenames until EOF, so a blank line becomes an *empty* filename that later fails `fopen("")` — there is no in-band list terminator (`parseNrrd.c:1347-1400`) — reviewed, no change (see below) |
| ✅ Q84 | NrrdIO *(vendored)* | `formatNRRD_write` emits comments and key/value pairs *after* the `data file:` field, defeating the writer's own "data file must be last" ordering — latent header corruption for `LIST`-form writes with comments (`formatNRRD.c:783-857`; cf. `write.c:724-728`) — **fixed: #6674 (teem `bug-nrrd-datafile-last` regenerated into ITK NrrdIO), merged** |
| ⬜ Q85 | `TxtTransformIO` | `Parameters:` lines are parsed by vnl `read_ascii`, which stops at the first unparseable token and reports success — `Parameters: 1 2 oops 4` silently yields `[1, 2]` (`itkTxtTransformIO.cxx:183-186`) |
| ⬜ Q86 | `TxtTransformIO` | one `transform` local is reassigned per `Transform:` line and buffered parameters apply only once *both* `Parameters` and `FixedParameters` have been seen — unpaired lines bind one transform's parameters to a different transform, silently (`:168-232`) |
| ⬜ Q87 | `TxtTransformIO` | the `#Insight Transform File V1.0` header is swallowed by the `#`-comment skip and never validated; `CanReadFile` checks only the `.txt`/`.tfm` extension (`:148`, `:39-46`) |
| ⬜ Q88 | `TxtTransformIO` | an empty `FixedParameters` vector writes the line `FixedParameters: ` with a trailing space (`:238-250`, `:303-305`) |
| ⬜ Q89 | `HDF5TransformIO` | `CanWriteFile` claims `.hdf`/`.h4`/`.hdf4`/`.he4` — HDF**4** container names — though `Write` always creates an HDF5 file, and the lowercase extension table rejects `.H5`/`.HDF5` (`itkHDF5TransformIO.cxx:83-84`, `:424`) |
| ⬜ Q90 | `PNGImageIO::ReadImageInformation` is a bare `return;` on a wrong PNG signature (`:334-338`) where `Read` throws `"File is not png type: …"` for the identical `png_sig_cmp` (`:139-143`) — a caller reaching it via `SetImageIO` (bypassing `CanReadFile`) gets a fabricated success with `m_Dimensions` unset. A missing file pointer returns silently the same way (`:321-324`) |
| ⬜ Q91 | `PNGImageIO::CanWriteFile` passes `ignoreCase = false` to `HasSupportedWriteExtension` (`:497`), unlike the trait's own case-insensitive default; `.png` and `.PNG` are both registered (`:281-287`), so `foo.PNG` is claimed but `foo.PnG` is not |
| ⬜ Q92 | `m_ExpandRGBPalette`'s in-class initializer is `false` (`itkImageIOBase.h:853`) but `ImageIOBase`'s constructor calls `Reset(false)`, which unconditionally sets it `true` (`itkImageIOBase.cxx:28,45`) — `PNGImageIO` never touches the flag, so the header default is dead and every instance starts with palette expansion on |
| ⬜ Q93 | `HDF5TransformIO`'s `ReadParameters`/`ReadFixedParameters` emit `"Wrong data type for " << DataSetName << "in HDF5 File"` — no space before `in`, so the dataset name runs into the word (`itkHDF5TransformIO.cxx:171`, `:217`) |
| ⬜ Q94 | the rank-1 check in both of those readers raises `"Wrong # of dims for TransformType in HDF5 File"` (`:176`, `:222`) — `TransformType` is the one dataset the check never covers |
| ⬜ Q95 | `HDF5TransformIO::Read` loops `i < transformGroup.getNumObjs()` and then `openGroup(GetTransformName(i))` (`:297-302`); `getNumObjs` counts every *link*, so one stray dataset beside the `0`, `1`, … subgroups makes the loop ask for a group `N` that was never written and the read dies inside libhdf5 rather than reporting an extra object |
| ⬜ Q96 | `HDF5TransformIO::CanReadFile` is `h5file.exists("/TransformGroup")` (`:59-66`), i.e. `H5Lexists` — a *dataset* of that name passes, the factory hands the file over, and `Read`'s `openGroup` (`:295`) throws an `H5::Exception` instead of the factory's clean "could not create Transform IO object" |
| ⬜ Q97 | `HDF5ImageIO` stores three axis conventions in one group: `Dimension`/`Origin`/`Spacing` in ITK order, fastest-moving first (`:1151-1161`); only `VoxelData`'s dataspace is reversed (`:1170-1182`); and `Directions` writes `m_Direction[i]` as its **row** `i` (`:490-510`) while that is *column* `i` of the direction matrix — so a 4×3 2-D image writes `Dimension = [4, 3]` and a `VoxelData` of shape `[3, 4]`, and the `Directions` dataset is the matrix transpose |
| ⬜ Q98 | `HDF5ImageIO`'s `ReadScalar` (`:410`) and `ReadVector` (`:479`) both raise `"Wrong # of dims for TransformType in HDF5 File"` — the transform IO's message (Q94), verbatim, in a file that holds no transform. `ReadDirections` (`:521`) at least names the image directions |
| ⬜ Q99 | `VoxelType` is written on every `Write` (`:1163-1166`) and read on no `Read`: `m_ComponentType` is derived from `imageSet.getDataType()` (`:752-753`). A file whose `VoxelType` reads `FLOAT` over `int16` voxels loads as `Int16`; a file with no `VoxelType` at all loads fine. The class comment (`itkHDF5ImageIO.h:68-71`) says it exists to make the file "user friendly with respect to HDF5 viewers", so this is by design — worth stating, since it looks authoritative |
| ⬜ Q100 | `HDF5ImageIO::CanReadFile` is `h5file.exists("/ITKImage")` (`:647-654`) — same `H5Lexists` shape as Q96, so a *dataset* named `/ITKImage` is claimed and `ReadImageInformation`'s first `openDataSet` throws |
| ⬜ Q101 | three of `ReadImageInformation`'s meta-data branches are unreachable: `WriteScalar(unsigned long)` stores `NATIVE_UINT` tagged `isUnsignedLong` (`:322-342`), so no ITK-written dataset is ever `NATIVE_INT` + `isUnsignedLong` as `:801-805` expects; and on every LP64 platform `H5Tequal(NATIVE_LONG, NATIVE_LLONG)` and `H5Tequal(NATIVE_ULONG, NATIVE_ULLONG)` are true, so the arms at `:874-897` swallow every 64-bit integer and those at `:898-905` never run (the `isLLong`/`isULLong` tags inside the surviving arms are what actually distinguish the two) |
| ⬜ Q102 | a meta-data dataset of rank > 1 is skipped by `continue` (`:785-789`), and the `if`/`else if` chain over the fourteen `NATIVE_*` types ends in an `else` that tests only `metaDataType == strType` with no `else` of its own (`:914-922`) — so a compound, enum, array or variable-length-sequence dataset leaves no dictionary entry and raises nothing. A *subgroup* under `MetaData`, by contrast, is fatal: the loop opens every link with `openDataSet` (`:776-782`) |
| ⬜ Q103 | `HDF5ImageIO`'s constructor registers `.hdf`, `.h4`, `.hdf4`, `.he4` — HDF**4** container names — for both reading and writing alongside the HDF5 ones (`:36-43`), the same eight-name list already noted on the transform side (Q89). Here `CanWriteFile` is `HasSupportedWriteExtension(name)` with `ignoreCase = true` (`itkImageIOBase.h:788`), so `IMAGE.H5` is claimed for writing where the transform IO's case-sensitive `CanWriteFile` declines it |
| ⬜ Q104 | `JPEGImageIO::Write` throws `"JPEG Writer can only write 2-dimensional images"` before opening anything (`:459-462`), where `PNGImageIO::WriteSlice` silently writes only the first slice (B71) — the two sibling IOs disagree on whether a 3-D source is an error |
| ⬜ Q105 | `JPEGImageIO::CanReadFile` is markedly stricter than PNG's: case-sensitive extension (`:102`, `ignoreCase = false`), then the `0xFFD8` magic (`:117-130`), then a full `jpeg_read_header` parse (`:152`) — where `PNGImageIO::CanReadFile` stops at the 8-byte signature |
| ⬜ Q106 | `JPEGImageIO::CanWriteFile` passes `ignoreCase = false` (`:449`) against four registered spellings (`:308-314`) — `.jpg`, `.JPG`, `.jpeg`, `.JPEG` are claimed, `.Jpg` is not. Same shape as PNG's (Q91) |
| ⬜ Q107 | `m_UseCompression` is assigned `false` in the constructor (`:299`) and never read: `jpeg_set_quality` is called unconditionally (`:537`), so only `Quality`/`CompressionLevel` has any effect and `UseCompressionOn()` is silently inert for this IO |
| ⬜ Q108 | Two quirks of the TIFF resolution tags. (a) Spacing survives a round-trip only to `float` precision: `WriteImageInformation` stores `25.4 / m_Spacing[i]` (`:587`, `:794`) into `XRESOLUTION`, which libtiff holds as a `float` (`tif_dir.h:98`; `TIFF_RATIONAL`/`TIFF_SETGET_FLOAT`, `tif_dirinfo.c:90`) and `m_XResolution` mirrors as `float` — so a spacing of `1.0` reads back as `1.0000000150…`. (b) `TIFFReaderInternal::Initialize` reads `XRESOLUTION` with the *non*-defaulted `TIFFGetField` (`:202`) over `Clean()`'s seed of `1`, while the read guard is only `unit > 0 && m_XResolution > 0` (`:375-385`) — so a page with `RESOLUTIONUNIT = 2` (inch) and no `XRESOLUTION` tag yields a spacing of `25.4` rather than the default `1.0`. (An absent `RESOLUTIONUNIT` seeds `unit = 1`, which takes neither branch, so the plain case is safe.) |
| ⬜ Q109 | `FastChamferDistanceImageFilter` | the anti-raster sweep is `for (it.GoToEnd(), --it; !it.IsAtBegin(); --it)` (`.hxx:194`), so flat index 0 is never the *center* of the backward pass — harmless, because the pass writes to lower-index neighbours and flat index 1 still updates it. The 3-D weights are the float literals `0.92644`, `1.34065`, `1.65849` (`.hxx:38`, `:41`, `:44`) |
| ⬜ Q110 | `IsolatedConnectedImageFilter` | the bisection bounds and midpoint are typed `NumericTraits::AccumulateType` (`.hxx:147`, `:178-180`, `:243-245`), which for `unsigned int` is `unsigned int` — no widening (`itkNumericTraits.h:1027`). `guess = (upper + lower) / 2` (`.hxx:231`, `:296`) can therefore overflow for thresholds near `UINT_MAX` |
| ⬜ Q111 | `STAPLEImageFilter` | contrary to the common description of the algorithm, there is **no** `p = q = 0.99999` seeding: `W` is seeded from the rater mean (`.hxx:116`), the prior `g_t` is computed once before the loop (`:123`), per-rater convergence is `(dp)² , (dq)² ≤ 1e-14` (`:51`, `:215-220`), and the foreground test is a two-sided ε-window with `ε = 1e-10` (`:94`). Worth stating because implementations that copy the papers' seeding will not reproduce ITK |
| ⬜ Q112 | `BinaryMedianImageFilter` | the majority rule is strict `>` against `neighborhoodSize / 2` (`.hxx:121`, `:136-143`), so a tie goes to background; and the output is always exactly `ForegroundValue` or `BackgroundValue` — a pixel whose value matches neither is not passed through |
| ⬜ Q113 | `ChangeLabelImageFilter` | the change map is `std::map` (`.h:57`), so keys are quantized through the pixel type and a repeated key is last-wins (`.h:77`); `operator()` performs a single lookup (`.h:92-101`), so `a→b` together with `b→c` is **not** transitive — `a` maps to `b` |
| ⬜ Q114 | `BSplineDecompositionImageFilter` | the causal-coefficient initialization switches between an accelerated truncated sum and the exact mirror-boundary sum on `horizon < dataLength`, where `horizon = ceil(log(tol)/log(|z|))` (`.hxx:180-208`). With the default `m_Tolerance = 1e-10` (`.h:188`) that is 18 for the cubic pole, so an 18-sample line takes the exact path and a 19-sample line takes the truncated one — a discontinuity in method, of size `O(|z|^horizon)` |
| ⬜ Q115 | `PatchBasedDenoisingImageFilter` noise-model fidelity | on the first iteration the output equals the input, so GAUSSIAN's `2·(in − out)` (`.hxx:2015`) and POISSON's `(in − out)/(out + 1e-5)` (`:2049`) are both identically zero. With SimpleITK's default `NumberOfIterations = 1`, neither noise model contributes anything; RICIAN's Bessel-ratio term (`:2031-2032`) is nonzero and is unaffected |
| ⬜ Q116 | `PatchBasedDenoisingImageFilter` internals | three of a kind: `ResolveSigmaUpdate` writes `m_KernelBandwidthSigma` (`.hxx:1854`, `:1860`) *before* returning, and the caller then compares `\|sigmaUpdate\|` against the **already-updated** sigma (`:1427`); `m_MinProbability = 2.2e-306` (`:43`) is added to the entropy probability *and* to its first and second derivatives (`:1734-1738`); and on the skipped-query path the subsampler's `offset` keeps its zero initialization rather than being recomputed by `ComputeOffset` (`itkSpatialNeighborSubsampler.hxx:140`, `:147-151`) |
| ⬜ Q117 | `JoinSeriesImageFilter` size mismatch | asymmetric and undocumented. Each input's requested region is derived from the *primary* input's size (`.hxx:209-216`), so a subsequent input that is **smaller** throws `InvalidRequestedRegionError` from region propagation, while one that is **larger** is silently corner-cropped to `[0, primary_size)`. (Physical congruency is checked separately, with `coordinateTol = m_CoordinateTolerance · spacing[0]` — the *first* dimension's spacing for every axis, `itkImageToImageFilter.hxx:184-185`) |
| ⬜ Q118 | `TransformToDisplacementFieldFilter` linear fast path | the scan-line endpoints are the continuous indices `idx[0]` and `idx[0] + size[0]` — one past the last pixel — with `alpha = i / size[0]` (`.hxx:239`, `:245`, `:255-256`). This is exact rather than off-by-one, because for a linear transform `T(p) − p` is affine, so interpolating between those two endpoints reproduces the displacement at every interior pixel. Recorded because it reads like an off-by-one and is not |

Also: `LinearAnisotropicDiffusionLBRImageFilter`'s Selling decomposition bails out after 200 superbase flips by printing `"Warning: Selling's algorithm not stabilized."` to `std::cerr` and continuing with a possibly non-obtuse superbase — which can yield negative stencil weights. The 2-D branch guards obtuseness with a debug-only `assert`; the 3-D branch has no check at all (`.hxx:185-263`).

Contributor guide

Open the contributing guide

Research direction

Start with the Q1–Q118 table and the source-level file-and-line citations referenced in the issue, then compare each untriaged behavior with the relevant ITK filter documentation and tests. This is complete when every quirk has a recorded disposition; any accepted correction should become a focused follow-up issue or pull request.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
computer-vision
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.