Repository navigation
dcmjpeg: fix precision loss for MR images with small RescaleSlope in dcmcjpeg - #160
Open
MichaRotstein wants to merge 1 commit into
Open
MichaRotstein wants to merge 1 commit into
MichaRotstein wants to merge 1 commit into
Conversation
DicomImage sizes its internal pixel buffer as an integer type based on the physical (post-Rescale) value range. For MR quantitative maps with a small RescaleSlope/Intercept (e.g. ADC maps, physical range ~0-4), this truncates the full original bit depth (e.g. 12-bit, 3242 unique values) down to a handful of integer levels before dcmjpeg's own windowing/compression logic ever runs, severely posterizing the output. Fix encodeMonochromeImage() to set CIF_IgnoreModalityTransformation for the default auto-fit window mode when RescaleSlope/Intercept are present and no Modality LUT Sequence competes (which takes precedence per DICOM). This keeps DicomImage's pixel values in the raw stored domain at full bit depth. The window-derived output scale is then composed with the dataset's original RescaleSlope/Intercept via exact linear-function composition so the output Rescale tags still convert to the correct physical units. Also fixes a resulting Window Center/Width domain mismatch: these tags are defined in the physical domain, but with the Modality transform bypassed, the encoder's voiOffset/voiFactor operate in the raw domain. correctVOIWindows() now converts WC/WW to the raw domain first before applying them, only in the default output mode (the composed Rescale tags already restore the physical domain in +rm mode). Verified via round-trip compression/decompression of a real MR ADC image: unique pixel values preserved (3242 -> 3257 vs ~100 before the fix), correct WindowCenter/Width in both default and modality-rescale- preserving modes, mean/max physical reconstruction error of 0.0013/0.016 in the rescale-preserving mode. Standard CT images (RescaleSlope=1, RescaleIntercept=-1024) produce mathematically identical output to the unmodified code. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Some MR quantitative maps (e.g. ADC) use DICOM tags whose combination triggers severe precision loss when compressed to JPEG Extended (Process 2&4) with dcmcjpeg's default settings. The tags involved, with sample values from a reproduction file, are:
(0028,0100)Bits Allocated: 16(0028,0101)Bits Stored: 12(0028,0102)High Bit: 11(0028,0103)Pixel Representation: 0 (unsigned) — stored pixel values range 0-4038 (3242 unique values)(0028,1052)Rescale Intercept: -0.022939345002(0028,1053)Rescale Slope: 0.0009384566625 — these two together map the 0-4038 stored range to a physical range of about -0.02 to 3.77(0028,1050)Window Center: 1.46(0028,1051)Window Width: 2.94 — defined in the same small physical range produced by the Rescale tags aboveNo
(0028,3000)Modality LUT Sequence is present — Rescale Slope/Intercept is the active transform for these images.Compressing such a file to JPEG Extended (Process 2&4) with dcmcjpeg's default settings destroys almost all of this precision: only about 100 unique pixel values survive instead of 3242.
A fully anonymized copy of the reproduction file (all patient/institution-identifying tags removed; pixel data and clinically relevant tags unchanged) is attached here for anyone who wants to reproduce this directly:
RIm000000291224092026695Image1541_anon.dcm
Root cause
Before dcmjpeg touches the pixel data,
DicomImageapplies the Modality Rescale transform internally and stores the result in an integer buffer sized from the physical (post-rescale) value range (DiMonoModality/rangeToBits()indcmimgle). With a physical range of about 0-4, that buffer can only hold 3-4 distinct values, so the original 12-bit dynamic range is gone before compression even starts.This isn't a rounding issue — it's a storage-type issue, so rounding mode doesn't matter. The fix is to stop
DicomImagefrom applying the rescale internally for this code path, and apply it ourselves afterward.Fix
Two files changed:
dcmjpeg/libsrc/djcodece.cc(the fix logic) anddcmjpeg/include/dcmtk/dcmjpeg/djcodece.h(one helper's signature), scoped entirely toDJCodecEncoder::encodeMonochromeImage()andcorrectVOIWindows().windowType==0), whenRescaleSlope/RescaleInterceptare present and noModalityLUTSequencecompetes (a LUT correctly takes precedence per DICOM, matchingDiMonoModality::checkRescaling()), we setCIF_IgnoreModalityTransformation.DicomImagethen keeps raw stored pixel values at full bit depth instead of its truncated buffer.correctVOIWindows()converts WC/WW to raw (raw = (physical - origIntercept) / origSlope) before applying them — only in the default output mode. In+rmmode (preserve original Rescale tags), the composed output already maps to physical, so WC/WW are left as-is.When none of this applies —
windowType != 0, no Rescale tags, or a Modality LUT Sequence instead — behavior is unchanged from the current code.Verification
Round-tripped a sample MR ADC file (compress, decompress, compare to original via pydicom) using:
+ee(--encode-extended) selects the JPEG Extended (Process 2&4) transfer syntax this fix targets;dcmcjpeg's default transfer syntax is JPEG Lossless (+e1), which is unaffected by this bug. Results:+ee(default)+ee +rm(preserve Rescale)Before the fix, only about 100 unique values survived the round-trip — roughly 97% of the dynamic range lost.
Visual demonstration
Rendered each output's decompressed pixel data at its own Window Center/Width (i.e. how a viewer would display it by default) for a direct visual comparison of the sample MR ADC file above, compressed to JPEG Extended (Process 2&4):
Before the fix (103 unique pixel values) — severe posterization/banding:
After the fix (3273 unique pixel values) — correct smooth gradient:
Also confirmed: standard CT (RescaleSlope=1, RescaleIntercept=-1024) produces mathematically identical output to the unmodified code, so typical CT/MR files are unaffected.
Built and tested against current
master(dcmjpeg module compiles cleanly with this change applied).Known limitations / areas for reviewer attention
SharedFunctionalGroupsSequencefor per-frame Rescale (not top-level tags) is not specifically exercised by this fix; it safely falls back to the original unmodified behavior since the top-level tag lookup simply fails.Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com