From 90f7fc1d8c902325d794c973349379a233669c56 Mon Sep 17 00:00:00 2001 From: Adnane Belmadiaf Date: Tue, 18 Aug 2026 01:39:13 +0200 Subject: [PATCH] fix(dicomImageLoader): normalize stored pixel bits Mask unsigned pixels to Bits Stored before range calculation. Use High Bit to locate the stored field and preserve signed values. Fix rendering when unused upper bits contain nonzero data. Fixes #781 --- packages/core/src/types/IImageFrame.ts | 1 + .../normalizeStoredPixelData.spec.ts | 59 +++++++++++++++++++ .../src/decodeImageFrameWorker.js | 16 +---- .../src/imageLoader/getImageFrame.ts | 1 + .../src/shared/normalizeStoredPixelData.ts | 43 ++++++++++++++ 5 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 packages/dicomImageLoader/src/__tests__/normalizeStoredPixelData.spec.ts create mode 100644 packages/dicomImageLoader/src/shared/normalizeStoredPixelData.ts diff --git a/packages/core/src/types/IImageFrame.ts b/packages/core/src/types/IImageFrame.ts index 22c6a4ff72..7d94d97743 100644 --- a/packages/core/src/types/IImageFrame.ts +++ b/packages/core/src/types/IImageFrame.ts @@ -9,6 +9,7 @@ interface ImageFrame { columns: number; bitsAllocated: number; bitsStored: number; + highBit?: number; pixelRepresentation: number; smallestPixelValue: number; largestPixelValue: number; diff --git a/packages/dicomImageLoader/src/__tests__/normalizeStoredPixelData.spec.ts b/packages/dicomImageLoader/src/__tests__/normalizeStoredPixelData.spec.ts new file mode 100644 index 0000000000..97ce40d337 --- /dev/null +++ b/packages/dicomImageLoader/src/__tests__/normalizeStoredPixelData.spec.ts @@ -0,0 +1,59 @@ +import type { Types } from '@cornerstonejs/core'; +import normalizeStoredPixelData from '../shared/normalizeStoredPixelData'; + +function createImageFrame( + pixelData: Types.PixelDataTypedArray, + overrides: Partial = {} +): Types.IImageFrame { + return { + bitsAllocated: 16, + bitsStored: 14, + highBit: 13, + pixelRepresentation: 0, + pixelData, + ...overrides, + } as Types.IImageFrame; +} + +describe('normalizeStoredPixelData', () => { + it('removes unused upper bits from unsigned pixels', () => { + const imageFrame = createImageFrame(new Uint16Array([32768, 41249, 65343])); + + normalizeStoredPixelData(imageFrame); + + expect(imageFrame.pixelData).toEqual(new Uint16Array([0, 8481, 16191])); + }); + + it('sign extends signed stored pixels', () => { + const imageFrame = createImageFrame( + new Int16Array([0x3fff, 0x2000, 0x1fff]), + { pixelRepresentation: 1 } + ); + + normalizeStoredPixelData(imageFrame); + + expect(imageFrame.pixelData).toEqual(new Int16Array([-1, -8192, 8191])); + }); + + it('uses HighBit to extract a shifted stored field', () => { + const imageFrame = createImageFrame(new Uint16Array([0xfff8, 0x4000]), { + bitsStored: 12, + highBit: 14, + }); + + normalizeStoredPixelData(imageFrame); + + expect(imageFrame.pixelData).toEqual(new Uint16Array([4095, 2048])); + }); + + it('leaves pixels unchanged when all allocated bits are stored', () => { + const imageFrame = createImageFrame(new Uint16Array([0, 41249, 65535]), { + bitsStored: 16, + highBit: 15, + }); + + normalizeStoredPixelData(imageFrame); + + expect(imageFrame.pixelData).toEqual(new Uint16Array([0, 41249, 65535])); + }); +}); diff --git a/packages/dicomImageLoader/src/decodeImageFrameWorker.js b/packages/dicomImageLoader/src/decodeImageFrameWorker.js index 4e84cd6a84..6add190e14 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.js +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.js @@ -21,6 +21,7 @@ import getPixelDataTypeFromMinMax, { validatePixelDataType, } from './shared/getPixelDataTypeFromMinMax'; import isColorImage from './shared/isColorImage'; +import normalizeStoredPixelData from './shared/normalizeStoredPixelData'; const imageUtils = { bilinear, @@ -41,20 +42,7 @@ export function postProcessDecodedPixels( start, decodeConfig ) { - const shouldShift = - imageFrame.pixelRepresentation !== undefined && - imageFrame.pixelRepresentation === 1; - - const shift = - shouldShift && imageFrame.bitsStored !== undefined - ? 32 - imageFrame.bitsStored - : undefined; - - if (shouldShift && shift !== undefined) { - for (let i = 0; i < imageFrame.pixelData.length; i++) { - imageFrame.pixelData[i] = (imageFrame.pixelData[i] << shift) >> shift; - } - } + normalizeStoredPixelData(imageFrame); // Cache the pixelData reference quickly incase we want to set a targetBuffer _and_ scale. let pixelDataArray = imageFrame.pixelData; diff --git a/packages/dicomImageLoader/src/imageLoader/getImageFrame.ts b/packages/dicomImageLoader/src/imageLoader/getImageFrame.ts index 3c5e3f95f8..5aca9d11f9 100644 --- a/packages/dicomImageLoader/src/imageLoader/getImageFrame.ts +++ b/packages/dicomImageLoader/src/imageLoader/getImageFrame.ts @@ -16,6 +16,7 @@ function getImageFrame(imageId: string): Types.IImageFrame { columns: imagePixelModule.columns, bitsAllocated: imagePixelModule.bitsAllocated, bitsStored: imagePixelModule.bitsStored, + highBit: imagePixelModule.highBit, pixelRepresentation: imagePixelModule.pixelRepresentation, // 0 = unsigned, smallestPixelValue: imagePixelModule.smallestPixelValue, largestPixelValue: imagePixelModule.largestPixelValue, diff --git a/packages/dicomImageLoader/src/shared/normalizeStoredPixelData.ts b/packages/dicomImageLoader/src/shared/normalizeStoredPixelData.ts new file mode 100644 index 0000000000..7ca2ab03f4 --- /dev/null +++ b/packages/dicomImageLoader/src/shared/normalizeStoredPixelData.ts @@ -0,0 +1,43 @@ +import type { Types } from '@cornerstonejs/core'; + +/** + * Removes bits outside the stored pixel field and sign extends signed values. + */ +export default function normalizeStoredPixelData( + imageFrame: Types.IImageFrame +): void { + const { + bitsAllocated, + bitsStored, + highBit = bitsStored - 1, + pixelData, + pixelRepresentation, + } = imageFrame; + const lowBit = highBit - bitsStored + 1; + + if ( + !pixelData || + bitsStored <= 0 || + bitsStored >= 32 || + bitsStored > bitsAllocated || + lowBit < 0 || + highBit >= bitsAllocated || + (bitsStored === bitsAllocated && lowBit === 0) + ) { + return; + } + + const valueRange = 2 ** bitsStored; + const mask = valueRange - 1; + const signBit = valueRange / 2; + const isSigned = pixelRepresentation === 1; + + for (let i = 0; i < pixelData.length; i++) { + const storedValue = (pixelData[i] >>> lowBit) & mask; + pixelData[i] = storedValue; + + if (isSigned && storedValue >= signBit) { + pixelData[i] -= valueRange; + } + } +}