From 2e15ad4de23cf62e40c8601e86eee629b5ecfb4b Mon Sep 17 00:00:00 2001 From: Adrien Bustany Date: Fri, 14 Aug 2026 14:15:46 +0200 Subject: [PATCH] dicomImageLoader: ignore windowing/VOI tags when decoding color images Section PS3.3 C.11.2.1.2.2 of the DICOM standard states: The Window Center (0028,1050), Window Width (0028,1051) and VOI LUT Function (0028,1056) shall be used only for Images with Photometric Interpretation (0028,0004) Values of MONOCHROME1 and MONOCHROME2. They have no meaning for other Images. Having those tags is still legal, they should just be ignored when rendering the image. --- .../src/imageLoader/createImage.ts | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/dicomImageLoader/src/imageLoader/createImage.ts b/packages/dicomImageLoader/src/imageLoader/createImage.ts index 1b33cc4db2..79db7cef6c 100644 --- a/packages/dicomImageLoader/src/imageLoader/createImage.ts +++ b/packages/dicomImageLoader/src/imageLoader/createImage.ts @@ -33,14 +33,6 @@ async function createImage( // in cs3d const useRGBA = options.useRGBA; - // always preScale the pixel array unless it is asked not to - options.preScale = { - enabled: - options.preScale && options.preScale.enabled !== undefined - ? options.preScale.enabled - : true, - }; - if (!pixelData?.length) { return Promise.reject(new Error('The pixel data is missing')); } @@ -50,6 +42,18 @@ async function createImage( const imageFrame = getImageFrame(imageId); imageFrame.decodeLevel = options.decodeLevel; + const isColorImage = isColorImageFn(imageFrame.photometricInterpretation); + + // always preScale the pixel array unless it is asked not to, or if the image + // is not grayscale (DICOM PS3.3 C.11.2.1.2.2). + options.preScale = { + enabled: + !isColorImage && + (options.preScale && options.preScale.enabled !== undefined + ? options.preScale.enabled + : true), + }; + options.allowFloatRendering = canRenderFloatTextures(); let redData, greenData, blueData; @@ -115,8 +119,6 @@ async function createImage( taskDecodeConfig ); - const isColorImage = isColorImageFn(imageFrame.photometricInterpretation); - return new Promise( (resolve, reject) => { // eslint-disable-next-line complexity @@ -390,12 +392,9 @@ async function createImage( columns: imageFrame.columns, height: imageFrame.rows, preScale: imageFrame.preScale, - intercept: modalityLutModule.rescaleIntercept - ? modalityLutModule.rescaleIntercept - : 0, - slope: modalityLutModule.rescaleSlope - ? modalityLutModule.rescaleSlope - : 1, + // Ignore rescale/intercept if the image is not monochrome (DICOM PS3.3 C.11.2.1.2.2) + intercept: isColorImage ? 0 : modalityLutModule.rescaleIntercept || 0, + slope: isColorImage ? 1 : modalityLutModule.rescaleSlope || 1, invert: imageFrame.photometricInterpretation === 'MONOCHROME1', minPixelValue: imageFrame.smallestPixelValue, maxPixelValue: imageFrame.largestPixelValue, @@ -411,11 +410,15 @@ async function createImage( windowWidth: voiLutModule.windowWidth ? voiLutModule.windowWidth[0] : undefined, + // Ignore the VOI descriptors if the image is not monochrome (DICOM + // PS3.3 C.11.2.1.2.2). StackViewport reads voiLUTSequence off the + // image to derive the initial VOI range, so a stale grayscale LUT + // would put it nowhere near the [0, 255] range of the color samples. voiLUTFunction: - (voiLutModule.voiLUTFunction?.length && + !isColorImage ? ((voiLutModule.voiLUTFunction?.length && voiLutModule.voiLUTFunction[0]) || voiLutModule.voiLutFunction || - undefined, + undefined) : undefined, decodeTimeInMS: imageFrame.decodeTimeInMS, floatPixelData: undefined, imageFrame, @@ -477,8 +480,10 @@ async function createImage( }; } - // Modality LUT + // Modality LUT, skipped for non-monochrome images for the same reason + // the rescale slope/intercept is skipped above. if ( + !isColorImage && modalityLutModule.modalityLUTSequence && modalityLutModule.modalityLUTSequence.length > 0 && isModalityLUTForDisplay(sopCommonModule.sopClassUID) @@ -486,8 +491,9 @@ async function createImage( image.modalityLUT = modalityLutModule.modalityLUTSequence[0]; } - // VOI LUT + // VOI LUT, also skipped for non-monochrome images. if ( + !isColorImage && voiLutModule.voiLUTSequence && voiLutModule.voiLUTSequence.length > 0 ) {