Skip to content

BUG: Findings collected while porting ~60 filters — reported together in one issue #6575

Description

@physwkim

Following up on #6569: while porting SimpleITK's filter surface to another language, I kept a ledger of everything in the ITK sources that looked wrong or surprising. It has grown to 92 suspected bugs and 118 quirks (originally 30 and 24; follow-up batches B31–B92 and Q25–Q118 are merged into the tables below) — too many to file individually without flooding the tracker, so I am reporting them together here for triage. Happy to split any subset into standalone issues if that is more useful; I am not planning PRs for all of these, so please treat this as raw material.

Status at a glance (maintainer triage — updated 2026-07-17 by @hjmjohnson)

Bucket Count
✅ Bugs fixed and merged 76 of 92
🚀 Bugs fixed, draft PR open 1 (#6658)
🔄 Bugs fixed on a branch, no ITK PR yet 6
⬜ Bugs unclaimed (all vendored ThirdParty) 5
❌ Bugs withdrawn — investigated, not defects 4
Quirks triaged 16 of 118

Every PR opened against this ledger has merged. The completed work is collapsed below; what follows is only what still needs someone.

⬅ Outstanding work

Bugs fixed on a branch, needing an ITK PR (6)

# Site One-line symptom
🔄 B34 WarpImageFilter the same-information fast-path test omits size, so a valid larger-output configuration dies in an iterator containment check instead of taking the general path
🔄 B35 ExponentialDisplacementFieldImageFilter the field is divided by 1 << numiter — UB when numiter >= 31; unreachable by default (cap 20, clamped), but an explicit SetMaximumNumberOfIterations(>= 31) makes it live
🔄 B63 NrrdImageIO::Write metadata dispatch NRRD_-prefixed keys are matched by strncmp of the candidate's length — NRRD_space directions is swallowed by the space handler, and unmatched NRRD_ keys (including the reader's own NRRD_pixel_original_axis) are silently dropped — fix: branch bug-nrrdio-6575
🔄 B67 NrrdIO per-axis field parsers (vendored) sizes/spacings/thicknesses/axis mins/axis maxs parse dim + 1 values into a NRRD_DIM_MAX(16)-element stack array — a 16-D header with a 17th token is a stack buffer overflow — fix: teem branch bug-nrrd-parse-axis-overflow (pushed to sourceforge; regenerates into ITK NrrdIO) + ITK-side bug-nrrdio-6575
🔄 B68 NrrdIO nrrdOriginCalculate (vendored) the gotMin loop tests axis[0]->min for every axis (literal 0, not the loop variable) — origins computed from NaN mins, or usable mins ignored — fix: teem branch bug-nrrd-origin-calculate (pushed to sourceforge; regenerates into ITK NrrdIO) + ITK-side bug-nrrdio-6575
🔄 B69 NrrdIO rnParse_kinds (vendored) the none token arm assigns the axis's centering, not its kindkinds: none … clobbers a previously parsed centers: value — fix: teem branch bug-nrrd-kinds-none-centering (pushed to sourceforge; regenerates into ITK NrrdIO) + ITK-side bug-nrrdio-6575
Ledger item PR Author State
B34, B35, Q25–Q38 (displacement-field / warp / demons batch-2+3) branch bug-displacement-field-batch2-6575 @hjmjohnson PR still pending — 9 commits, tested locally
B24 (FastMarchingUpwindGradient AllTargets duplicates) #6658 (draft) @hjmjohnson Draft PR open — rebased on upstream/main, fail-before/pass-after GTest, pre-commit clean
B63, B67–B69, Q82, Q84 (NrrdIO) ITK bug-nrrdio-6575 + 4 teem branches on sourceforge @hjmjohnson ITK PR still pending — vendored fixes sent upstream to Gordon Kindlmann

Bugs unclaimed (5) — all vendored ThirdParty, better handled upstream

# Site One-line symptom
⬜ B52 niftilib nifti_read_buffer (vendored) maps every non-finite float/double/complex component to 0 on read wherever isfinite is a macro (glibc: always) — NaN/±Inf pixels silently destroyed
⬜ B64 MetaIO ElementDataFile = LIST (vendored) three unguarded paths: short slice list → success with uninitialized tail; negative file dimension → m_DimSize[-1]/m_SubQuantity[-1] OOB reads; == NDims → zero-length slice reads, still success
⬜ B65 MetaIO M_ReadElements (vendored) a LOCAL compressed header without CompressedDataSize seeks to file start and inflates its own header text over the uninitialized pixel buffer — success, garbage pixels
⬜ B66 MetaIO MET_PerformUncompression (vendored) unconditionally return true — truncated/corrupt/wrong-container streams and short output all report success
⬜ B91 vnl vnl_matlab_readhdr::read_hdr (vendored) The no-swap switch lists 7 of the 8 legal type_t encodings — 1010 (big-endian, column-wise, single-precision), which vnl's own writer emits on a big-endian host, is byte-swapped as though foreign

Quirks

102 of 118 quirks are untriaged; 16 (Q25–Q38, Q82, Q84) are fixed on the branches listed above. The full quirk table is collapsed in its own section at the bottom — none of it is completed work, so it is all outstanding.

✅ Completed — 76 merged bug fixes (table)
# Site One-line symptom
✅ B1 ProjectionImageFilter Collapsed-axis origin shift uses the axis index, unsigned; axis 0 wraps to a ~2³¹·spacing shift — fix: #6594 (merged)
✅ B2 N4BiasFieldCorrectionImageFilter::SharpenImage Degenerate bin range → 0.0/0.0 = NaN → NaN→int cast (UB) → unchecked histogram index — fix: #6589 (merged)
✅ B3 SLICImageFilter Empty cluster → 0/0 NaN centroid → NaN→integer index cast (UB) — fix: #6589 (merged)
✅ B4 WienerDeconvolutionImageFilter Pf == Pn → complex division by zero → NaN output (guard passes on inf) — fix: #6589 (merged)
✅ B6 GridImageSource Kernel loop walks only the first line per axis; origin cancels out of the pattern — fix: #6600 (merged)
✅ B7 MinMaxCurvatureFlowFunction (3-D) acos taken on a gradient rescaled to length r, not 1 — wrong polar angle for radius ≥ 2 — fixed: #6603 (merged)
✅ B8 MinMaxCurvatureFlowFunction Neighborhood scales divide by the stencil radius but only ±1 neighbors are sampled — update r² too small — fix: #6604 (merged)
✅ B9 AdaptiveHistogramEqualization Constant input → iscale == 0 → NaN everywhere — fix: #6589 (merged)
✅ B10 MultiLabelSTAPLEImageFilter Confusion-matrix increment walks into the next row on any voting tie — fix: #6579 (merged)
✅ B11 STAPLEImageFilter All-background inputs → p_denom == 0 → NaN, unguarded — fix: #6589 (merged)
✅ B12 LabelOverlapMeasuresImageFilter false_positive_error guard tests the wrong quantity — NaN reachable — fix: #6589 (merged)
✅ B13 ThresholdMaximumConnectedComponentsImageFilter Initial bisection midpoint is (upper−lower)/2; unsigned wrap when it lands below lowerfixed: #6595 (merged)
✅ B14 ScalarImageKmeansImageFilter UseNonContiguousLabels interval underflows to 0xFFFFFFFF for > 255 classes — fix: #6596 (merged)
✅ B15 LabelVotingImageFilter Undecided label max_label+1 wraps to 0 for a UInt8 image using all 256 labels — fix: #6597 (merged)
✅ B16 MaskedFFTNormalizedCorrelationImageFilter Pixel squaring runs in the input integer type — int32 overflow before the cast to real — fix: #6598 (merged)
✅ B17 IterativeDeconvolutionImageFilter Output regions clobbered before PadInput/CropOutput read them — OutputRegionMode silently ignored — fixed: #6599 (merged)
✅ B18 FastApproximateRankImageFilter::SetRank Rank forwarded to only the first Dim−1 per-axis filters — last axis always median — fix: #6580 (merged)
✅ B19 AttributeMorphologyBaseImageFilter stable_sort end iterator off by one — last raster pixel never sorted — fix: #6581 (merged)
✅ B20 BoxSigmaImageFilter Radius 0 → 0.0/0.0 → NaN for every pixel — fix: #6589 (merged)
✅ B21 LevelSetNeighborhoodExtractor Strict sign tests: a pixel exactly on the contour neutralizes its neighborhood; grid-aligned contour starves the outward march — fixed: #6601 (merged)
✅ B22 StructureTensorImageFilter (AnisotropicDiffusionLBR) Tensor smoothing K_ρ applied along axis 0 only (single-direction filter, SetDirection never called) — fix: #6602 (merged)
✅ B23 CoherenceEnhancingDiffusionImageFilter Constant input → ∞ rescale → NaN tensors → negative step count via UB cast → zero iterations by accident — fix: #6589 (merged)
✅ B25 Transform::ApplyToImageMetadata Null GetInverseTransform() (singular or non-linear transform) dereferenced without a check — fix: #6583 (merged)
✅ B26 PatchBasedDenoisingImageFilter (POISSON) static_cast<PixelValueType>(0.99999) is 0 for integer types — step size collapses to 1e-5 always — fix: #6633 (merged)
✅ B27 PatchBasedDenoisingImageFilter Update write-back casts an unclamped negative float to an unsigned pixel type — UB — fix: #6589 (merged)
✅ B28 UniformRandomSpatialNeighborSubsampler Search loops forever when the search box is exactly the query point (kernel-bandwidth path) — fix: #6605 (merged)
✅ B29 GaussianRandomSpatialNeighborSubsampler Negative Gaussian variate cast to unsigned int — UB; rejection loop relies on the wrap — fix: #6589 (merged)
✅ B30 RegionBasedLevelSetFunction Reinitialization smoothing term silently degenerates to a bare Laplacian when CurvatureWeight == 0fix: #6606 (merged)
✅ B31 IterativeInverseDisplacementFieldImageFilter smallestError carries over from the previous pixel when the mapped point starts outside the buffer — output depends on raster order — fix: #6576 (merged)
✅ B32 InverseDisplacementFieldImageFilter the field-subsampling resampler never gets the input's direction — with non-identity direction cosines the spline landmarks are not on the input lattice at all — fix: #6577 (merged)
✅ B33 VectorConfidenceConnectedImageFilter a re-estimation pass that visits zero pixels produces NaN statistics, which invert the Mahalanobis guard — the next flood includes every pixel in the image — fix: #6578 (merged)
✅ B36 FastMarchingImageFilterBase::UpdateNeighbors the neighbor loop gates on the center being strictly interior, so a border seed never propagates along the boundary-normal axis — a face seed confines the march to that face, a corner seed marches nowhere — fixed: #6613 (merged)
✅ B37 FastMarchingImageFilterBase (NoHandles) marched-alive voxels never receive a connected-component label, so the facing-neighbor label test always passes and NoHandles degenerates to Strictfixed: #6613 (merged)
✅ B38 FastMarchingImageFilterBase::Solve the zero-speed guard substitutes a finite coefficient, giving zero-speed pixels an arrival time ≈ 2⁵² instead of the old filter's ∞ — speed barriers leak — fixed: #6613 (merged)
✅ B39 ScalarToRGBColormapImageFilter / ColormapFunction NumericTraits<float/double>::min() (smallest positive) used as a "most negative" seed in two places — wrong rescale for non-positive float images, and a ≈FLT_MAX default input range — fixed: #6615 (merged)
✅ B40 MergeLabelMapFilter (KEEP) the deferred-collision deque is never cleared between inputs — with ≥ 3 inputs one LabelObject ends up aliased under two label keys — fix: #6590 (merged)
✅ B41 LabelMapContourOverlayImageFilter (SLICE_CONTOUR) the slice-radius loop tests the write cursor against m_SliceDimension where it means the read index — SliceDimension == 0 draws no contour at allfix: #6590 (merged)
✅ B42 LabelMapMaskImageFilter (Crop) an empty/zero-line object set leaves the bounding-box seeds untouched — signed overflow (UB), then a 2×2 region at index 2⁶³−1 — fix: #6590 (merged)
✅ B43 AttributeUniqueLabelMapFilter the "remove objects without lines" pass is while (it.IsAtEnd()) — inverted, never executes — fix: #6590 (merged)
✅ B44 LabelMap::PushLabelObject on a completely full label range the gap scan finds nothing, skips SetLabel, and silently overwrites the entry at the object's own label instead of throwing "map is full" — fix: #6590 (merged)
✅ B45 StatisticsLabelMapFilter weighted elongation/flatness: no eigenvalue clamp, one shared guard, no ratio > 0 check — an indefinite weighted covariance yields NaN where ShapeLabelMapFilter yields 0 — fix: #6590 (merged)
✅ B46 NormalizedCorrelationImageFilter both divisions unguarded — a constant template or a locally-constant neighborhood produces NaN — fixed: #6612 (merged)
✅ B47 TransformIOBaseTemplate::CorrectTransformPrecisionType find("float") result used unchecked — a transform-type string naming neither precision throws std::out_of_range past every ITK handler — fixed: #6608 (merged)
✅ B48 NiftiImageIO::Read rescale cast buffer sized per voxel, consumed per component — heap over-read for any multi-component image with scl_slope/scl_interfix: #6625 (merged)
✅ B49 NiftiImageIO::Read RescaleFunction is given the voxel count — only the first numElts of a multi-component/complex buffer are rescaled — fix: #6625 (merged)
✅ B50 NiftiImageIO::Read RAS↔LPS conversion never checks the 3-component requirement its own exception text names — 2/4-component vectors get a stride-3 sign flip across voxel boundaries — fix: #6625 (merged)
✅ B53 GiplImageIO::Write INT/UINT images throw "Pixel Type Unknown" only after the target file has been truncated and the header written — destroys existing data, leaves a 256-byte stub — fixed: #6611 (merged)
✅ B54 GiplImageIO::Read success test is !bad(); a short pixel block sets failbit/eofbit, never badbit — truncated file reads as success over an uninitialized tail — fixed: #6611 (merged)
✅ B55 GiplImageIO::Read (compressed) success flag is p != nullptr on the caller's always-non-null output buffer — gzread's byte count is never inspected — fixed: #6611 (merged)
✅ B56 GiplImageIO::Write header image_type is derived from the component type alone while the byte count multiplies by components — a vector image writes a scalar header plus N× the bytes, reading back as garbage — fixed: #6611 (merged)
✅ B57 VTKImageIO::Read ReadBufferAsBinary's bool (false on short read) is discarded — truncated BINARY file reads as success — fixed: #6610 (merged)
✅ B58 VTKImageIO::InternalReadImageInformation every sscanf return is discarded over uninitialized locals — an under-filled DIMENSIONS/SPACING/ORIGIN line sizes the image from indeterminate values (UB) — fixed: #6610 (merged)
✅ B59 VTKImageIO::InternalReadImageInformation attribute dispatch is an ordered substring test — SCALARS vector_field float matches the vector arm and is misparsed as a 3-component VECTORS line — fixed: #6610 (merged)
✅ B60 VTKImageIO ASCII write precision(16) is set on a throwaway header stream; pixel data is emitted through a second stream at the default 6 significant digits — silent precision loss — fixed: #6610 (merged)
✅ B61 ImageIOBase::ReadBuffer (ASCII) extraction variable declared outside the loop, stream state never checked — one bad/overflowing field latches into every later component (5 99999 7 8 as short reads [5, 32767, 32767, 32767]) — fixed: #6609 (merged)
✅ B62 VTKImageIO::CanReadFile four GetNextLine calls outside the try block — a .vtk shorter than four lines throws "Premature EOF" through the factory instead of returning false — fixed: #6610 (merged)
✅ B70 PNGImageIO::WriteSlice the file is opened "wb" — truncating it — before the component-type switch whose default: throws; writing an int16 image destroys any existing .png and leaves it zero-byte — fixed: #6614 (merged)
✅ B72 PNGImageIO::WriteSlice any component count > 4 declares 4 channels (PNG_COLOR_TYPE_RGB_ALPHA) while row pointers advance by the actual count — each row's trailing components are silently dropped — fixed: #6614 (merged)
✅ B73 HDF5ImageIO::WriteImageInformation the C-string meta-data branch calls constCstringObj->GetMetaDataObjectValue() under a guard that admits constCstringObj == nullptr — null dereference for a MetaDataObject<char *>fix: #6626 (merged)
✅ B74 HDF5ImageIO::ReadImageInformation every geometry dataset's length is taken on trust — spacing[i] / Dims[i] are indexed to numDims regardless of the file's actual dataset length, reading past the end of a heap vector before any pixel is touched — fix: #6626 (merged)
✅ B76 JPEGImageIO::WriteSlice the "prefer centimetres" density branch tags the JFIF header density_unit = 0 ("aspect ratio only") while storing real dots-per-cm values — the reader's own density_unit > 0 guard then discards them, so the spacing is lost on a round-trip through ITK — fix: #6627 (merged)
✅ B77 JPEGImageIO::Read the CMYK→RGB conversion assumes all four channels are inverted; that holds for plain CMYK, but for a YCCK source libjpeg un-inverts C/M/Y and passes K through — a white YCCK pixel decodes to black — fix: #6627 (merged)
✅ B78 TIFFImageIO::ReadVolume each kept page is written at its raw directory index while the volume depth is sized from m_SubFiles — one ignored or untagged page before a SUBFILETYPE == 0 page and the last slice is written past the end of the buffer — fix: #6636 (merged)
✅ B79 TIFFImageIO::ReadGenericImage the scanline buffer is sized from the current directory, but width/SamplesPerPixel/BitsPerSample stay pinned to directory 0 — a multi-page file whose later pages are narrower over-reads the buffer — fix: #6636 (merged)
✅ B80 TIFFImageIO::ReadImageInformation the 32-bit arm of the component-type ladder has no default:, and CanRead never constrains SampleFormat — a 32-bit SAMPLEFORMAT_VOID page is read as whatever m_ComponentType already held — fix: #6636 (merged)
✅ B81 TIFFImageIO::ReadGenericImage PHOTOMETRIC_MINISWHITE is read out un-inverted — the // check inverted comment above the plain-copy grayscale arm never grew a body — fix: #6636 (merged)
✅ B82 TIFFImageIO::GetFormat MINISBLACK/MINISWHITE map to GRAYSCALE regardless of SamplesPerPixel, so a 2-sample grey+alpha page is declared 1-component and each row keeps only its first half — fix: #6636 (merged)
✅ B83 TIFFImageIO::InternalWrite PHOTOMETRIC_RGB is written for every component count other than 1 — a 2-component image becomes an RGB-tagged file with SamplesPerPixel = 2 and no EXTRASAMPLESfix: #6636 (merged)
✅ B84 MultiLabelSTAPLEImageFilter label loop counters are typed InputPixelType but bounded by m_TotalLabelCount — a uint8 input using all 256 labels wraps the counter 255 → 0 and InitializePriorProbabilities() never terminates (reproduced as a live hang, not source analysis) — fix: #6607 (merged)
✅ B85 MultiLabelSTAPLEImageFilter::InitializeConfusionMatrixArrayFromVoting the internal seeding LabelVotingImageFilter never receives the outer filter's SetLabelForUndecidedPixels(), so the seed pass always uses automatic label selection regardless of the caller's setting — fix: #6607 (merged)
✅ B86 GiplImageIO::ReadImageInformation / CanReadFile the 256-byte header is consumed as 15 separate gzread/istream::read pairs, none of whose byte counts is checked — a truncated or non-GIPL file populates dimensions, spacing and origin from uninitialized locals (same shape as B54/B55, but no success flag is computed at all) — fix: #6628 (merged)
✅ B87 TileImageFilter A 0 in any non-last Layout axis is never validated: with a non-zero last axis it indexes an empty std::vector and runs a ~2³² iteration loop; with a zero last axis (the constructor default) it divides by zero — fix: #6630 (merged)
✅ B88 GDCMImageIO::Read (SINGLEBIT) The 1-bit expansion walks the buffer as an unpadded bitstream, but GDCM pads each row to a byte boundary — every row after the first is decoded from the wrong bits when the width is not a multiple of 8 — fix: #6639 (merged), replacing the reverted #6629
✅ B89 GDCMImageIO::InternalReadImageInformation (ultrasound/hardcopy arm) A single-valued (0028,0030) leaves the second Element slot never written — axis-0 spacing is read from indeterminate heap memory; the assert(GetLength() == 2) guard is vacuous and cannot fire even in a Debug build — fix: #6639 (merged), replacing the reverted #6629
✅ B90 LabelSetErodeImageFilter / LabelSetDilateImageFilter GenerateData only calls AllocateOutputs() and every write is gated on m_Scale[d] > 0; under UseImageSpacing a zero radius on the last axis skips erosion's only output-writing pass — the filter returns uninitialized memory — fix: #6631 (merged)
✅ B92 ImageSeriesReader::GenerateOutputInformation An ITK_ImageOrigin override array shorter than the output dimension is silently shrunk by Array::operator=, and the next loop still indexes to ImageDimension - 1 — unchecked out-of-bounds read. No in-tree ImageIO writes that key, so it is live only for an out-of-tree/custom ImageIOfix: #6632 (merged)
✅ Completed — PR-by-PR triage record (49 settled rows)

#6629 (B88/B89, GDCM) was reverted by #6638 — its single-bit decode assumed row-padded 1-bit Pixel Data, but DICOM PS3.5 packs contiguously — and is now replaced by #6639, which decodes contiguously (verified pixel-for-pixel against pydicom on a real 13×12×8 highdicom SEG file) and validates PixelSpacing multiplicity via gdcm::VM::GetNumberOfElementsFromArray.

Every @physwkim PR carries a single commit; each regression test was verified locally to fail on the pre-fix code and pass on the fix.

Ledger item PR Author State
B10 (MultiLabelSTAPLE tie increment) #6579 @physwkim Merged
B18 (FastApproximateRank last axis) #6580 @physwkim Merged
B19 (AttributeMorphologyBase sort range) #6581 @physwkim Merged
B25 (ApplyToImageMetadata null inverse) #6583 @dzenanz Merged
B31 (IterativeInverseDisplacementField stale error bar) #6576 @physwkim Merged
B32 (InverseDisplacementField subsampler direction) #6577 @physwkim Merged; adjacent geometry gaps split to #6582, closed by #6588
B32 follow-up (lattice centering, output-direction API, subsampling guards) #6588 @hjmjohnson Merged — closed #6582
B2, B3, B4, B9, B11, B12, B20, B23, B27, B29 (NaN / UB-cast guards) #6589 @hjmjohnson Merged
B40–B45 (LabelMap / label-statistics correctness) #6590 @hjmjohnson Merged
B1 (ProjectionImageFilter collapsed-axis origin) #6594 @physwkim Merged — baselines uploaded
B13 (ThresholdMaximumConnectedComponents bisection seed) #6595 @physwkim Merged
B14 (ScalarImageKmeans non-contiguous label interval) #6596 @physwkim Merged
B15 (LabelVoting / MultiLabelSTAPLE undecided-label wrap) #6597 @physwkim Merged
B16 (MaskedFFTNormalizedCorrelation integer squaring) #6598 @physwkim Merged
B17 (IterativeDeconvolution ignores OutputRegionMode) #6599 @physwkim Merged
B6 (GridImageSource ignores the image origin) #6600 @physwkim Merged
B7 (MinMaxCurvatureFlow 3-D polar angle) #6603 @physwkim Merged
B8 (CurvatureFlowFunction derivative scale vs radius) #6604 @physwkim Merged
B21 (LevelSetNeighborhoodExtractor zero contour) #6601 @physwkim Merged
B22 (StructureTensor single-axis smoothing) #6602 @physwkim Merged — the 16 AnisotropicDiffusionLBR baselines were regenerated and uploaded
B28 (UniformRandomSpatialNeighborSubsampler infinite Search) #6605 @physwkim Merged
B30 (RegionBasedLevelSetFunction reinitialization term) #6606 @physwkim Merged
B36, B37, B38 (FastMarchingImageFilterBase boundary / topology / zero speed) #6613 @physwkim Merged
B39 (colormap NumericTraits::min as a most-negative seed) #6615 @physwkim Merged
B46 (NormalizedCorrelationImageFilter unguarded divisions) #6612 @physwkim Merged
B47 (TransformIOBase CorrectTransformPrecisionType) #6608 @physwkim Merged
B53–B56 (GiplImageIO write truncation, short-read detection, vector header) #6611 @physwkim Merged
B57–B60, B62 (VTKImageIO short read, sscanf, attribute dispatch, ASCII precision, CanReadFile) #6610 @physwkim Merged
B61 (ImageIOBase::ReadBuffer ASCII stream state) #6609 @physwkim Merged
B70, B72 (PNGImageIO truncates the file before validating) #6614 @physwkim Merged
B71 (PNGImageIO writes only the first slice) @physwkim Withdrawn — not a defect, the contract is documented and tested
B84, B85 (MultiLabelSTAPLE counter wrap, undecided label not propagated) #6607 @physwkim Merged
B48–B50 (NIfTI multi-component rescale over-read) #6625 @physwkim Merged
B73, B74 (HDF5 null metadata cast, unchecked geometry lengths) #6626 @physwkim Merged
B76, B77 (JPEG centimetre density tag, YCCK conversion) #6627 @physwkim Merged
B86 (GIPL header read in one checked call) #6628 @physwkim Merged
B88, B89 (GDCM single-bit packing, single-valued PixelSpacing) #6629 (reverted by #6638), replaced by #6639 @physwkim, @hjmjohnson Merged#6639 decodes 1-bit Pixel Data contiguously per PS3.5 (including the trailing partial byte, which the pre-#6629 code dropped) and counts PixelSpacing values with GDCM's own multiplicity counter, closing the trailing-backslash gap. Verified against pydicom on a real multi-frame SEG file.
B87 (TileImageFilter zero Layout axis) #6630 @physwkim Merged
B90 (LabelSet erode/dilate uninitialized output) #6631 @physwkim Merged
B92 (ImageSeriesReader origin-override out-of-bounds) #6632 @physwkim Merged
B26 (PatchBasedDenoising POISSON step collapse) #6633 @physwkim Merged
B78–B83 (TIFF volume packing, per-page geometry, SampleFormat, MINISWHITE, grey+alpha, 2-component write) #6636 @physwkim Merged
B39 family (NumericTraits::min as a most-negative seed, 5 further sites) #6634 @physwkim Merged
B39 family (entropy threshold calculators) #6635 @physwkim Merged — expected constants and baselines updated
B51 (NIfTI 2-D vector rank), B71 (PNG first slice), B75 (HDF5 memory type) @physwkim Withdrawn — not defects, see the bug table
B5 (CannySegmentationLevelSetFunction speed image) @physwkim Withdrawn — not a defect, see comment below
B33 (ConfidenceConnected empty re-estimation) #6578 @physwkim Merged
Precursors #6569 (JHMI marginals + derivative) @physwkim Merged
#6572 (ScaleLogarithmicTransform Jacobian) @physwkim Merged
❌ Withdrawn — 4 items investigated and found not to be defects (table)
# Site One-line symptom
❌ B5 CannySegmentationLevelSetFunction Propagation weight 0 → advection generated from a never-allocated speed image — not a defect; withdrawn (UpdateOutputInformation resets the empty requested region)
❌ B51 NiftiImageIO spatial-dimension inference for intent-vector images never consults dim[1] — a 2-D vector image one row tall reads back as 1-D — not a defect; withdrawn: the write side collapses trailing degenerate dimensions too, so a 2-D 5x1 vector image and a 1-D 5 vector image produce byte-identical dim[]; the rank is lost at write time and no read-side change can recover it
❌ B71 PNGImageIO::WriteSlice height comes from GetDimensions(1) alone, no axis beyond it is consulted — a 3-D image writes only its first slice, with no error — not a defect; withdrawn: writing the first slice of an image of 3 or more dimensions is the documented, tested contract (Modules/IO/PNG/test/itkPNGImageIOTest.cxx:152-160)
❌ B75 HDF5ImageIO::Read the file datatype is passed as H5Dread's memory type, making the conversion the identity — a VoxelData stored STD_I16BE is copied without a byte swap and read back byte-swapped, silently — not a defect; withdrawn: ReadImageInformation() requires an exact H5Tequal match against a NATIVE_* predtype and throws otherwise, and it is what populates m_VoxelDataSet — so Read() is unreachable with a foreign-endian on-disk type (verified: writing STD_I16BE throws during ReadImageInformation())
How these were found, and what was checked before filing

Each filter was reimplemented from the .hxx sources and the two implementations compared on synthetic inputs; every divergence was traced back to the ITK code path that caused it. Before filing, every item was re-checked against main at e46eb723a5: the cited lines re-read in context, declared types and constructor defaults verified, and each claimed consequence re-derived from the quoted code. Items whose original analysis did not survive that re-check were corrected or dropped — some of my own earlier conclusions turned out to be wrong, so I would not be surprised if a few of the remaining ones are too. Corrections welcome.

One item is flagged inline as source-analysis only (B28: non-termination identified from the loop structure, not run to a live hang).

Reproduced quirks and inconsistencies

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<OutputPixelType>, 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<PixelType>::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<double>::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<output RGB component>::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<double> 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<TOutput>::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) — fix: branch bug-nrrdio-6575
⬜ 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) — fix: teem branch bug-nrrd-datafile-last (pushed to sourceforge; regenerates into ITK NrrdIO) + ITK-side bug-nrrdio-6575
⬜ 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 — HDF4 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 — HDF4 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<InputPixelType>::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<InputPixelType, OutputPixelType> (.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(
⬜ 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).

AI assistance
  • Tool: Claude Code (claude-opus-4-8)
  • Role: found the defects while cross-checking an independent reimplementation of these filters against this source; performed the pre-filing re-verification pass (re-reading every cited line on main at e46eb723a5, checking declared types and constructor defaults, re-deriving each consequence)
  • Contribution: the inventory above
  • Testing: source-level verification against e46eb723a5; behavioral claims cross-checked against the independent reimplementation's pinned tests. B28 is source analysis only (not run to a live hang) and is labeled as such inline.
  • Items whose original analysis did not survive re-verification were corrected or dropped before filing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions