Skip to content

dcmjpeg: fix precision loss for MR images with small RescaleSlope in dcmcjpeg - #160

Open
MichaRotstein wants to merge 1 commit into
DCMTK:masterfrom
MichaRotstein:fix/dcmjpeg-small-rescale-slope-precision
Open

MichaRotstein wants to merge 1 commit into
DCMTK:masterfrom
MichaRotstein:fix/dcmjpeg-small-rescale-slope-precision

Conversation

@MichaRotstein

@MichaRotstein MichaRotstein commented Oct 2, 2026 •

Copy link
Copy Markdown

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 above

No (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, DicomImage applies the Modality Rescale transform internally and stores the result in an integer buffer sized from the physical (post-rescale) value range (DiMonoModality/rangeToBits() in dcmimgle). 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 DicomImage from applying the rescale internally for this code path, and apply it ourselves afterward.

Fix

Two files changed: dcmjpeg/libsrc/djcodece.cc (the fix logic) and dcmjpeg/include/dcmtk/dcmjpeg/djcodece.h (one helper's signature), scoped entirely to DJCodecEncoder::encodeMonochromeImage() and correctVOIWindows().

  1. For dcmcjpeg's default "auto-fit" mode (windowType==0), when RescaleSlope/RescaleIntercept are present and no ModalityLUTSequence competes (a LUT correctly takes precedence per DICOM, matching DiMonoModality::checkRescaling()), we set CIF_IgnoreModalityTransformation. DicomImage then keeps raw stored pixel values at full bit depth instead of its truncated buffer.
  2. Windowing and compression then operate in this raw domain.
  3. Before writing the output Rescale Slope/Intercept, we compose the original transform with the window-derived scale:
    rescaleSlope     = origSlope * newSlope
    rescaleIntercept = origSlope * newIntercept + origIntercept
    
    This is exact linear composition, so the output still decodes to the same physical units.
  4. Window Center/Width are defined in the physical domain, but windowing now happens in the raw domain. correctVOIWindows() converts WC/WW to raw (raw = (physical - origIntercept) / origSlope) before applying them — only in the default output mode. In +rm mode (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:

dcmcjpeg +ee input.dcm output.dcm         # default mode (windowType==0, no Rescale tags in output)
dcmcjpeg +ee +rm input.dcm output.dcm     # +rm = --rescale-map, preserve Modality Rescale Slope/Intercept

+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:

Original +ee (default) +ee +rm (preserve Rescale)
Unique pixel values 3242 3257 3257
RescaleSlope/Intercept 0.000938 / -0.0229 1.0 / 0.0 (raw domain by design) 0.000938 / -0.0229 (recomposed, matches original)
WindowCenter/Width 1.46 / 2.94 1580.19 / 3132.8 (raw domain, correct) 1.46 / 2.94 (unchanged, correct)
Mean / max physical error - n/a 0.0013 / 0.016

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:

Unpatched dcmcjpeg output - posterized

After the fix (3273 unique pixel values) — correct smooth gradient:

Patched dcmcjpeg output - correct

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

  • Enhanced/multiframe DICOM using SharedFunctionalGroupsSequence for 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.
  • VOI LUT Sequence interaction with this change has not been directly tested.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

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>
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.

1 participant