BUG: Fix NRRD measurement-frame corruption; refactor reserved-field dispatch#6088
BUG: Fix NRRD measurement-frame corruption; refactor reserved-field dispatch#6088hjmjohnson wants to merge 4 commits intoInsightSoftwareConsortium:mainfrom
Conversation
|
| Filename | Overview |
|---|---|
| Modules/IO/NRRD/src/itkNrrdImageIO.cxx | Core bug fix: replaces the old 666666-sentinel corruption with a proper dimension check + exception in WriteMeasurementFrame; refactors the per-field if-ladder into a clean dispatch table — logic and validation are correct. |
| Modules/IO/NRRD/include/itkNrrdImageIO.h | Documentation-only: adds a doxygen paragraph explaining teem's automatic NRRD version selection (NRRD0001–NRRD0006). No behavioral changes. |
| Modules/IO/NRRD/test/itkNrrdMetaDataTest.cxx | Two new regression subtests added (NRRD0005 auto-bump and throw-on-malformed-frame); a minor Windows portability issue with an open ifstream during RemoveFile. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[NrrdImageIO::Write - MetaDataDictionary loop] --> B{Key starts with 'NRRD_' prefix?}
B -- Yes --> C[Extract keyField after prefix]
C --> D[TryDispatchNrrdReservedField]
D --> E{Match in kNrrdReservedFields dispatch table?}
E -- Per-axis field --> F[Parse index via sscanf, call perAxis handler, stamp nrrd->axis]
E -- Scalar field --> G{Field == measurement frame?}
G -- Yes --> H{Frame dims == nrrd->spaceDim?}
H -- No --> I[itkGenericExceptionMacro throw ExceptionObject]
H -- Yes --> J[Copy frame to nrrd->measurementFrame]
G -- No --> K[WriteOldMin / WriteOldMax / WriteSpace / WriteContent / etc.]
E -- No match --> L[Key silently consumed, not forwarded to custom-pair]
B -- No --> M[Serialize value to string, nrrdKeyValueAdd as := pair]
Reviews (2): Last reviewed commit: "BUG: Throw on malformed NRRD measurement..." | Re-trigger Greptile
…sentinel The previous WriteMeasurementFrame path (pre-existing behavior preserved verbatim through the earlier dispatch-table refactor) filled missing coefficients with a magic sentinel value of 666666 whenever the user- supplied measurement frame had fewer rows or columns than the image's space dimension. Since every coefficient is finite, teem's nrrd__FieldInteresting check treats the frame as fully specified and writes the literal 666666 to the on-disk header, silently producing a corrupted orientation matrix rather than signalling an error. Validate the supplied frame up front: if fewer rows or narrower columns than nrrd->spaceDim, issue an ITK warning and leave nrrd->measurementFrame unset (the default AIR_NAN initialization makes the frame "uninteresting" to teem and it is omitted from the header). A dimension-matched frame is copied unchanged as before. itkNrrdMetaDataTest gets a regression subtest that writes a 2x2 frame on a 3-D image and asserts neither "measurement frame:" nor the literal "666666" appears in the output header. Greptile P1 on InsightSoftwareConsortium#6088.
3628cb0 to
d5e3ea7
Compare
Replace the iterator-style loop over NrrdImageIO::Write()'s MetaDataDictionary keys with a C++11 range-based for, renaming the loop variable from the iterator `keyIt` to the string reference `metaKey`. All `*keyIt` and `keyIt->c_str()` uses are updated to `metaKey` and `metaKey.c_str()` mechanically. Pure rename; no behavior change. Split out as its own commit to keep the reviewer's attention free for the dispatch-table refactor that lands on top -- the `keyIt` -> `metaKey` churn is mechanical and easy to verify in isolation.
Replace the open-coded if-ladder inside NrrdImageIO::Write() with a uniform dispatch table keyed by the teem nrrdField enum. Each reserved field (thicknesses, centers, kinds, labels, units, old_min, old_max, space, content, measurement_frame) is handled by a dedicated small function; the per-axis vs. scalar distinction is expressed once by the handler signature instead of being re-implemented at every call site. No behavior change. The existing itkNrrdMetaDataTest and the full 42 NRRD tests continue to pass. This refactor is a prerequisite for the follow-up commits that add bare-key promotion (issue InsightSoftwareConsortium#5461), bulk per-axis syntax, locale-safe numeric coercion for Python-wrapped clients, automatic NRRD0005 version bump, and NRRD_type round-trip (issue InsightSoftwareConsortium#5462).
…sentinel The previous WriteMeasurementFrame path (pre-existing behavior preserved verbatim through the earlier dispatch-table refactor) filled missing coefficients with a magic sentinel value of 666666 whenever the user- supplied measurement frame had fewer rows or columns than the image's space dimension. Since every coefficient is finite, teem's nrrd__FieldInteresting check treats the frame as fully specified and writes the literal 666666 to the on-disk header, silently producing a corrupted orientation matrix rather than signalling an error. Validate the supplied frame up front: if fewer rows or narrower columns than nrrd->spaceDim, issue an ITK warning and leave nrrd->measurementFrame unset (the default AIR_NAN initialization makes the frame "uninteresting" to teem and it is omitted from the header). A dimension-matched frame is copied unchanged as before. itkNrrdMetaDataTest gets a regression subtest that writes a 2x2 frame on a 3-D image and asserts neither "measurement frame:" nor the literal "666666" appears in the output header. Greptile P1 on InsightSoftwareConsortium#6088.
8a0d8c1 to
b88a682
Compare
|
@greptileai review this draft before I make it official |
teem's nrrdSave auto-selects the minimum NRRD magic version based on the fields set on the Nrrd struct (nrrd__FormatNRRD_whichVersion in NrrdIO's formatNRRD.c): setting a measurement frame forces NRRD0005, thicknesses/space/space-dimension/sample-units force NRRD0004, and so on. This behavior was previously undocumented in the ITK wrapper, leading users to assume ITK always writes NRRD0004 (see Discourse https://discourse.itk.org/t/6666). Document the auto-bump contract in the NrrdImageIO class doxygen and add an itkNrrdMetaDataTest subtest that writes a 3x3 measurement frame via the documented "NRRD_measurement frame" key and verifies the on-disk magic is NRRD0005 and the header contains a standard "measurement frame:" field (not a custom ":=" entry). No C++ behavior change.
The previous WriteMeasurementFrame path (pre-existing behavior preserved verbatim through the earlier dispatch-table refactor) filled missing coefficients with a magic sentinel value of 666666 whenever the user- supplied measurement frame had fewer rows or columns than the image's space dimension. Since every coefficient was finite, teem's nrrd__FieldInteresting check treated the frame as fully specified and wrote the literal 666666 to the on-disk header, silently producing a corrupted orientation matrix rather than signalling an error. Validate the supplied frame up front: if fewer rows or narrower columns than nrrd->spaceDim, throw an itk::ExceptionObject describing the mismatch. A dimension-matched frame is copied unchanged as before. Reject-and-throw is consistent with ITK's policy of not silently masking user errors. itkNrrdMetaDataTest gets a regression subtest that writes a 2x2 frame on a 3-D image and asserts the writer throws; if any partial file was produced it also asserts the literal "666666" does not leak into the header.
b88a682 to
8123e48
Compare
Fixes a silent on-disk corruption in
NrrdImageIO::Writeand refactors the reserved-field routing. Documents the teem-driven NRRD version auto-bump that users on Discourse #6666 and in #5461 did not realise was already available.Does not attempt to paper over user-supplied metadata in non-documented forms. Use
NRRD_<field>per the (now-extended) class doxygen; non-conforming metadata either throws or falls through to NRRD's documented:=custom-pair escape hatch.Commit sequence
NrrdImageIO::Write()'s metadata loop to a range-based for (mechanicalkeyIt->→metaKey.rename, 27 / 27 lines).nrrdFieldenum. No behavior change.itkNrrdMetaDataTestsubtest that writes a measurement frame via the documentedNRRD_measurement framekey and asserts the on-disk magic isNRRD0005. Addresses the confusion behind Discourse #6666.WriteMeasurementFramenow throwsitk::ExceptionObjectwhen the user-supplied frame's dimensions do not matchnrrd->spaceDim, instead of padding missing entries with the magic sentinel 666666 (which teem faithfully wrote to the header, silently corrupting orientation data). Regression test added.Relation to #5461 and SimpleITK/SimpleITK#2395
Neither is fixed here, by design:
Write to NRRD doesn't convert all metadata keys to NRRD keys correctly #5461 — reporter used bare metadata keys like
contentandthicknessesinstead of the documentedNRRD_content/NRRD_thicknesses[N]form. The docs were corrected in DOC: Add information to NRRD and GDCM IOs. #5472; with those docs in place, the bare-key round-trip is user error. Non-NRRD_*keys go through NRRD's documented:=custom-pair escape hatch, which is the standard's own answer for "anything not a reserved field." No code change in this PR is required for that contract to hold.setting NRRD metadata fails due to encoding information as string SimpleITK/SimpleITK#2395 — the Python binding stores numeric metadata as strings, which the ITK
ExposeMetaData<double>then fails to coerce, leaving a zero. That is a Python-binding encoding bug in SimpleITK, not a NRRD writer bug. Fixing it inside the C++ writer would require type-coercion machinery that accommodates a specific wrapper's encoding choice — maintenance burden for the core codebase in service of a third-party workaround. The upstream SimpleITK issue is the correct venue.Test plan
ctest -R Nrrd— 33/33 pass locally (Linux, clang 19 via conda-forge).itkNrrdMetaDataTestextended with two new subtests: NRRD0005 auto-bump fromNRRD_measurement frame; throw-on-malformed-measurement-frame regression.