Repository navigation
DICOM: read spacing, position and rescale of Enhanced CT slices without functional groups - #7141
Conversation
…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>
| 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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| } | ||
| double slope = gimage.GetSlope(); | ||
| double intercept = gimage.GetIntercept(); | ||
| if ( slope == 1 && intercept == 0 ) |
There was a problem hiding this comment.
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 005slope/intercept and ignores the public ones, which are display scaling (see the comment citing "Platform-Dependent Image Scaling" inGetRescaleInterceptSlopeValue). 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.
There was a problem hiding this comment.
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" ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
…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)
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 byspacingVec.empty(), butgdcm::ImageHelper::GetSpacingValuenever 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.sortDICOMFiles: ImagePositionPatient is read from the top-level element instead ofGetOriginValue, 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
DicomLoadSlicesWithoutFunctionalGroupswrites 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):
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,makeObjectTreeFromFoldermarked 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