Skip to content

DICOM: read spacing, position and rescale of Enhanced CT slices without functional groups - #7141

Merged
Grantim merged 3 commits into
masterfrom
agent/mlib-dicom-enhanced-no-functional-groups
Oct 15, 2026
Merged

Grantim merged 3 commits into
masterfrom
agent/mlib-dicom-enhanced-no-functional-groups

Conversation

@Grantim

@Grantim Grantim commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Single-frame CBCT slices tagged Enhanced CT Image Storage but without the functional group sequences (OnDemand3D CD exports) loaded with 1 mm voxels instead of the PixelSpacing value, in mirrored z order, and as raw stored values without RescaleIntercept. GDCM reads spacing, origin and rescale of Enhanced SOP classes only from the functional groups and returns defaults when they are absent.

  • loadSingleFile: the PixelSpacing fallback was guarded by spacingVec.empty(), but gdcm::ImageHelper::GetSpacingValue never returns an empty vector, so the fallback has not run since Better check for missing spacing in DICOMs #5477. It now runs when gdcm reports spacing 1 and PixelSpacing is present, and reads PixelSpacing in DICOM order: row spacing (y) first.
  • loadSingleFile: the same fallback for RescaleSlope/RescaleIntercept when gdcm reports the identity.
  • Both fallbacks run only for Enhanced and Legacy Converted Enhanced CT/MR/PET, the classes gdcm reads only from the functional groups. Every other SOP class keeps gdcm's values.
  • sortDICOMFiles: ImagePositionPatient is read from the top-level element instead of GetOriginValue, which ignores it for Enhanced SOP classes. For other SOP classes the values are the same, except Secondary Capture series with ImagePositionPatient, which gdcm also ignored and which are now sorted by z.

New test DicomLoadSlicesWithoutFunctionalGroups writes the same 3 slices tagged CT Image Storage and Enhanced CT Image Storage, and both must load the same way.

A CBCT series exported by OnDemand3D (556 slices 740x740):

before after same files retagged as CT
voxel size 1 mm 0.27 mm 0.27 mm
values 0..7798 -1000..6798 -1000..6798
xf.b.z +75.06 -74.79 -74.79

After the fix the volume is voxel-for-voxel identical to the retagged copy (same data hash).

The same export also has a 256x256 color thumbnail (YBR_FULL) from another series, I0000001.dcm, which made Open Folder fail with "Unsupported DICOM folder: unsupported photometric interpretation". Since #4168, makeObjectTreeFromFolder marked a folder as DICOM by the first DICOM file it found and kept that file's status, so one unsupported file listed first rejected the whole folder. Now the first supported file decides, and the unsupported status is reported only if no file in the folder is supported. The DICOM loader already skips unsupported files. Test: DicomFolderWithUnsupportedFile.

Generated with Claude Code

…ut functional groups

GDCM reads spacing, origin and rescale of Enhanced SOP classes only from the
functional group sequences and returns defaults (spacing 1, zero origin,
identity rescale) when they are absent, as in single-frame Enhanced CT slices
exported by OnDemand3D. Such slices loaded with 1 mm voxels, mirrored z and
no rescale.

- fall back to the top-level PixelSpacing when gdcm reports spacing 1
  (the spacingVec.empty() check never fired: GetSpacingValue is never empty)
- fall back to the top-level RescaleSlope/RescaleIntercept when gdcm reports identity
- read ImagePositionPatient directly when sorting slices

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since isDicomFile reports unsupported files (#4168), the folder scan marked a
folder as DICOM by its first DICOM file and kept that file's status, so one
unsupported file listed first (e.g. a color thumbnail of another series)
rejected the whole folder. Now the first supported file decides, and the
unsupported status is reported only if no file is supported; the loader
already skips unsupported files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread source/MRVoxels/MRDicom.cpp Outdated
const double* spacing = gimage.GetSpacing();
if ( spacingVec.empty() && ds.FindDataElement( gdcm::Keywords::PixelSpacing::GetTag() ) )
// gdcm returns spacing 1 when it cannot find the spacing, e.g. it reads Enhanced SOP classes only from the functional groups,
// while single-frame Enhanced CT slices without them (OnDemand3D) have it in the top-level element as classic CT

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PixelSpacing is row spacing \ column spacing, so GetValue( 0 ) is the y spacing. gdcm swaps the pair before returning it (std::swap( sp[0], sp[1] ) in gdcmImageHelper.cxx, GetSpacingValue), so gimage.GetSpacing()[0] in the else branch is x, but the fallback below assigns GetValue( 0 ) to voxelSize.x: x and y are swapped for non-square pixels.

This fallback was dead since #5477, so nothing caught it, and the new test uses 0.5\0.5. Suggest x = GetValue( 1 ), y = GetValue( 0 ), and a non-square spacing in DicomLoadSlicesWithoutFunctionalGroups (e.g. 0.5\0.25).

@Grantim Grantim Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 76e1d84: x = GetValue( 1 ), y = GetValue( 0 ), and DicomLoadSlicesWithoutFunctionalGroups uses 0.5\0.25 again. Its CT Image Storage variant goes through gdcm, so both variants must agree on x and y.

Comment thread source/MRVoxels/MRDicom.cpp Outdated
}
double slope = gimage.GetSlope();
double intercept = gimage.GetIntercept();
if ( slope == 1 && intercept == 0 )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback also fires for SOP classes where gdcm skips the public rescale on purpose, not only for Enhanced ones without functional groups:

  • MRImageStorage: for Philips files gdcm uses the private (2005,xx09/0a) Philips MR Imaging DD 005 slope/intercept and ignores the public ones, which are display scaling (see the comment citing "Platform-Dependent Image Scaling" in GetRescaleInterceptSlopeValue). If the private values are the identity, this code now applies the public ones.
  • every class gdcm does not rescale at all (US, XA, DX, NM, ...): the top-level tags are now applied.

Suggest limiting it (and probably the spacing fallback above) to the classes gdcm reads only from the functional groups, i.e. the Enhanced / Legacy Converted Enhanced CT/MR/PET list at the top of GetRescaleInterceptSlopeValue, or at least excluding MRImageStorage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 76e1d84: isEnhanced() limits both fallbacks (spacing and rescale) to Enhanced and Legacy Converted Enhanced CT/MR/PET. Every other SOP class keeps gdcm's values.

UniqueTemporaryFolder tmpFolder;
const std::string ct = "1.2.840.10008.5.1.4.1.1.2";
const std::vector<uint16_t> pixels = { 500, 1500, 2500, 3500 };
writeDicomSlice( tmpFolder / "a.dcm", ct, 1, 0, 2, 2, pixels, "YBR_FULL" );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relies on a.dcm being listed first, but Directory order is filesystem-defined: alphabetical on NTFS, hash order on ext4. On Linux the thumbnail may come after a supported slice, and then the test passes without reaching the new code path. It never fails falsely, but its coverage is platform-dependent; e.g. writing several unsupported files would make it hit the path regardless of order.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the coverage depends on the filesystem. More unsupported files would only raise the odds on hash-ordered directories, not guarantee the path, because the listing order can't be forced. NTFS lists entries alphabetically, so the Windows CI jobs always hit it; I'd keep the test as is.

…elSpacing x/y

Review fixes:
- the PixelSpacing and RescaleSlope/RescaleIntercept fallbacks now run only for
  Enhanced and Legacy Converted Enhanced CT/MR/PET, which gdcm reads only from
  the functional groups; other SOP classes keep gdcm's values (Philips MR
  private rescale, no rescale for US/XA/DX/NM)
- PixelSpacing lists the row spacing (y) first; the test uses non-square pixels

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Grantim
Grantim merged commit 1702bd9 into master Oct 15, 2026
59 checks passed
@Grantim
Grantim deleted the agent/mlib-dicom-enhanced-no-functional-groups branch October 15, 2026 10:11
gorbovskoy-evg pushed a commit that referenced this pull request Oct 15, 2026
…ut functional groups (#7141)

* DICOM: read spacing, position and rescale of Enhanced CT slices without functional groups

GDCM reads spacing, origin and rescale of Enhanced SOP classes only from the
functional group sequences and returns defaults (spacing 1, zero origin,
identity rescale) when they are absent, as in single-frame Enhanced CT slices
exported by OnDemand3D. Such slices loaded with 1 mm voxels, mirrored z and
no rescale.

- fall back to the top-level PixelSpacing when gdcm reports spacing 1
  (the spacingVec.empty() check never fired: GetSpacingValue is never empty)
- fall back to the top-level RescaleSlope/RescaleIntercept when gdcm reports identity
- read ImagePositionPatient directly when sorting slices

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Open DICOM folders whose first DICOM file is unsupported

Since isDicomFile reports unsupported files (#4168), the folder scan marked a
folder as DICOM by its first DICOM file and kept that file's status, so one
unsupported file listed first (e.g. a color thumbnail of another series)
rejected the whole folder. Now the first supported file decides, and the
unsupported status is reported only if no file is supported; the loader
already skips unsupported files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* DICOM: limit the top-level fallbacks to Enhanced SOP classes, fix PixelSpacing x/y

Review fixes:
- the PixelSpacing and RescaleSlope/RescaleIntercept fallbacks now run only for
  Enhanced and Legacy Converted Enhanced CT/MR/PET, which gdcm reads only from
  the functional groups; other SOP classes keep gdcm's values (Philips MR
  private rescale, no rescale for US/XA/DX/NM)
- PixelSpacing lists the row spacing (y) first; the test uses non-square pixels

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1702bd9)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants