Skip to content

BUG: Keep the Pixel Spacing row/column swap when spacing is incomplete - #7066

Merged
hjmjohnson merged 1 commit into
InsightSoftwareConsortium:mainfrom
pieper:dcmtk-pixel-spacing-order
Nov 1, 2026
Merged

hjmjohnson merged 1 commit into
InsightSoftwareConsortium:mainfrom
pieper:dcmtk-pixel-spacing-order

Conversation

@pieper

@pieper pieper commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

DCMTKFileReader::GetSpacing swaps Pixel Spacing into the index order it returns, then overwrites that with the unswapped values when it cannot find all three. Series lacking Spacing Between Slices (0018,0088) read with in-plane axes transposed.

Measured behavior

Single-frame CT, Rows 4, Columns 8, PixelSpacing 1\3, so the correct ITK spacing is [3, 1, …]. Read through 3D Slicer 5.12.4, which exposes both back ends:

ImageIO without (0018,0088) with (0018,0088)
DCMTKImageIO [1, 3, 2] — swapped [3, 1, 2]
GDCMImageIO [3, 1, 2] [3, 1, 2]

DCMTKImageIO::ReadImageInformation does not check the EXIT_FAILURE that GetSpacing returns on that path. Square pixels hide the effect, which is why it has gone unnoticed.

Why the swap is correct

DICOM PS3.3 Section 10.7.1.3:

The first Value is the row spacing in mm, that is the spacing between the centers of adjacent rows, or vertical spacing. The second Value is the column spacing in mm, that is the spacing between the centers of adjacent columns, or horizontal spacing.

Other implementations agree, several of them with a comment saying so:

implementation code
dicom3tools libsrc/src/dctool/pixposn.cc — "PixelSpacing Row/Col is space BETWEEN rows/cols respectively - hence row pixels of size PixelSpacingRow along ImageOrientationPatientCol"
PixelMed com/pixelmed/geometry/GeometryOfSlice.java — row*columnArray[i]*voxelSpacing[0]/*between rows*/ + column*rowArray[i]*voxelSpacing[1]/*between cols*/
GDCM gdcmImageHelper.cxx — sp.push_back(at.GetValue(1)); sp.push_back(at.GetValue(0));, and std::swap(sp[0], sp[1]) on the general path
DCMTK dcmimgle/libsrc/diimage.cc — PixelHeight from value 1, PixelWidth from value 2
highdicom src/highdicom/spatial.py — spacing_between_rows / spacing_between_columns, each paired with the matching index direction
nibabel nicom/dicomwrappers.py — np.fliplr(iop) with voxel_sizes = (ps[0], ps[1], zs)
dcmqi libsrc/Itk2DicomConverter.cpp — writes spacing[1] \ spacing[0] from an ITK image

The two comments describing (0028,0030) as "physical X,Y spacing" are corrected in the same commit; they state the value order the opposite way round and are what the removed assignments were consistent with.

Test

itkDCMTKImageIOGTest.cxx adds a GoogleTest driver to this module. DCMTKImageIO.InPlaneSpacingOrderWithoutSpacingBetweenSlices writes a single-frame CT with PixelSpacing 1\3 and no (0018,0088), reads it back, and checks the spacing is [3, 1]. It writes its own input with DCMTK, already in TEST_DEPENDS, so no new ExternalData is needed.

Not built locally. ITKIODCMTK is EXCLUDE_FROM_DEFAULT and configure-ci does not set Module_ITKIODCMTK, so the Pixi jobs appear not to build this module — a reviewer with the module enabled would be confirming something CI does not. The behavior in the table above was measured against a built ITK, and the fix itself is a deletion of three assignments.

AI assistance
  • Tool: Claude Code (claude-opus-5)
  • Role: root-cause analysis and drafting
  • Contribution: isolated the fall-through in GetSpacing by differential testing of the two ImageIO back ends with and without (0018,0088), then surveyed how other DICOM implementations order the attribute
  • Reviewed before committing; test compilation not verified locally

Related: pydicom/pydicom#2416 — the same confusion in an example, found in the same survey.

@github-actions github-actions Bot added type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct area:IO Issues affecting the IO module labels Nov 1, 2026
@greptile-apps

This comment was marked as resolved.

Comment thread Modules/IO/DCMTK/test/CMakeLists.txt Resolved
Pixel Spacing (0028,0030) is row spacing then column spacing (DICOM PS3.3
Section 10.7.1.3), the reverse of the index order DCMTKFileReader::GetSpacing
returns, so it swaps the two values.

When fewer than three spacings were found, the function overwrote spacing[0]
and spacing[1] with the unswapped values before returning EXIT_FAILURE.
Spacing Between Slices (0018,0088) is absent from most single-frame series and
DCMTKImageIO does not check the return value, so those series were read with
their in-plane axes transposed. Square pixels hide the effect.

Values that were found are already stored in the returned order, and the rest
keep the 1.0 they were initialized with.
@pieper
pieper force-pushed the dcmtk-pixel-spacing-order branch from 09a1e7c to 0f9f9b4 Compare November 1, 2026 16:39
@hjmjohnson
hjmjohnson merged commit adea54b into InsightSoftwareConsortium:main Nov 1, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:IO Issues affecting the IO module type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants