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; + } + } +}