Skip to content
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ByteArray } from 'dicom-parser';
import { metaData, type Types } from '@cornerstonejs/core';
import type { Types } from '@cornerstonejs/core';
import { fetchLUTForInstance } from './fetchLUTForInstance';

function convertLUTto8Bit(lut: number[], shift: number) {
const numEntries = lut.length;
Expand All @@ -12,84 +13,46 @@ function convertLUTto8Bit(lut: number[], shift: number) {
return cleanedLUT;
}

function fetchPaletteData(imageFrame, color, fallback) {
const data = imageFrame[`${color}PaletteColorLookupTableData`];
if (data) {
return Promise.resolve(data);
}

const result = metaData.get('imagePixelModule', imageFrame.imageId);

if (result && typeof result.then === 'function') {
return result.then((module) =>
module ? module[`${color}PaletteColorLookupTableData`] : fallback
);
} else {
return Promise.resolve(
result ? result[`${color}PaletteColorLookupTableData`] : fallback
);
}
}

/**
* Convert pixel data with PALETTE COLOR Photometric Interpretation to RGBA
* Convert pixel data with PALETTE COLOR Photometric Interpretation to RGB/RGBA
* Note: Palette bulkdata must be loaded on the imageFrame before calling this function
*
* @param imageFrame - The ImageFrame to convert
* @param imageFrame - The ImageFrame to convert (must have palette data loaded)
* @param colorBuffer - The buffer to write the converted pixel data to
* @param useRGBA - Whether to output RGBA (true) or RGB (false)
* @returns
*/
export default function (
export default function convertPaletteColor(
imageFrame: Types.IImageFrame,
colorBuffer: ByteArray,
useRGBA: boolean
): void {
const numPixels = imageFrame.columns * imageFrame.rows;
const pixelData = imageFrame.pixelData;

Promise.all([
fetchPaletteData(imageFrame, 'red', null),
fetchPaletteData(imageFrame, 'green', null),
fetchPaletteData(imageFrame, 'blue', null),
]).then(([rData, gData, bData]) => {
if (!rData || !gData || !bData) {
throw new Error(
'The image does not have a complete color palette. R, G, and B palette data are required.'
);
}
const rData = imageFrame.redPaletteColorLookupTableData;
const gData = imageFrame.greenPaletteColorLookupTableData;
const bData = imageFrame.bluePaletteColorLookupTableData;

const len = rData.length;
let palIndex = 0;
let bufferIndex = 0;

const start = imageFrame.redPaletteColorLookupTableDescriptor[1];
const shift =
imageFrame.redPaletteColorLookupTableDescriptor[2] === 8 ? 0 : 8;

const rDataCleaned = convertLUTto8Bit(rData, shift);
const gDataCleaned = convertLUTto8Bit(gData, shift);
const bDataCleaned = convertLUTto8Bit(bData, shift);

if (useRGBA) {
for (let i = 0; i < numPixels; ++i) {
let value = pixelData[palIndex++];

if (value < start) {
value = 0;
} else if (value > start + len - 1) {
value = len - 1;
} else {
value -= start;
}

colorBuffer[bufferIndex++] = rDataCleaned[value];
colorBuffer[bufferIndex++] = gDataCleaned[value];
colorBuffer[bufferIndex++] = bDataCleaned[value];
colorBuffer[bufferIndex++] = 255;
}
if (!rData || !gData || !bData) {
throw new Error(
'The image does not have a complete color palette. R, G, and B palette data are required.'
);
}

return;
}
const len = rData.length;
let palIndex = 0;
let bufferIndex = 0;

const start = imageFrame.redPaletteColorLookupTableDescriptor[1];
const shift =
imageFrame.redPaletteColorLookupTableDescriptor[2] === 8 ? 0 : 8;

const rDataCleaned = convertLUTto8Bit(rData, shift);
const gDataCleaned = convertLUTto8Bit(gData, shift);
const bDataCleaned = convertLUTto8Bit(bData, shift);

if (useRGBA) {
for (let i = 0; i < numPixels; ++i) {
let value = pixelData[palIndex++];

Expand All @@ -104,6 +67,48 @@ export default function (
colorBuffer[bufferIndex++] = rDataCleaned[value];
colorBuffer[bufferIndex++] = gDataCleaned[value];
colorBuffer[bufferIndex++] = bDataCleaned[value];
colorBuffer[bufferIndex++] = 255;
}

return;
}

for (let i = 0; i < numPixels; ++i) {
let value = pixelData[palIndex++];

if (value < start) {
value = 0;
} else if (value > start + len - 1) {
value = len - 1;
} else {
value -= start;
}
});

colorBuffer[bufferIndex++] = rDataCleaned[value];
colorBuffer[bufferIndex++] = gDataCleaned[value];
colorBuffer[bufferIndex++] = bDataCleaned[value];
}
}

/**
* Convert pixel data with PALETTE COLOR Photometric Interpretation to RGB/RGBA
* with automatic fetching of palette color lookup tables if not already loaded.
* This is useful for users who want to convert palette color frames without
* manually fetching the palette data first.
*
* @param imageFrame - The ImageFrame to convert
* @param colorBuffer - The buffer to write the converted pixel data to
* @param useRGBA - Whether to output RGBA (true) or RGB (false)
* @returns Promise that resolves when conversion is complete
*/
export async function convertPaletteColorWithFetch(
imageFrame: Types.IImageFrame,
colorBuffer: ByteArray,
useRGBA: boolean
): Promise<void> {
// Fetch LUT data if needed (palette, modality, VOI)
await fetchLUTForInstance(imageFrame);

// Call the synchronous conversion function
convertPaletteColor(imageFrame, colorBuffer, useRGBA);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Types } from '@cornerstonejs/core';
import { metaData } from '@cornerstonejs/core';
import { fetchPaletteData } from './fetchPaletteData';

/**
* Fetches all necessary LUT data (palette, modality, VOI) for an image instance if not already loaded.
* This function only returns a promise if there is actual LUT data to fetch from metadata.
* TODO : Extend this to modality and VOI LUT fetching as needed.
*
* @param imageFrame - The ImageFrame to fetch LUT data for
* @returns Promise that resolves when all LUT data is fetched, or void if no fetching needed
*/
export async function fetchLUTForInstance(
imageFrame: Types.IImageFrame
): Promise<void> | null {
const fetchPromises = [];

// Check if palette color LUTs need to be fetched
const needsPaletteLUT =
!imageFrame.redPaletteColorLookupTableData ||
!imageFrame.greenPaletteColorLookupTableData ||
!imageFrame.bluePaletteColorLookupTableData;

if (needsPaletteLUT) {
// Fetch palette data if not already loaded
const palettePromises = Promise.all([
fetchPaletteData(imageFrame, 'red', null),
fetchPaletteData(imageFrame, 'green', null),
fetchPaletteData(imageFrame, 'blue', null),
]).then(([redData, greenData, blueData]) => {
// Attach palette data to imageFrame
imageFrame.redPaletteColorLookupTableData = redData;
imageFrame.greenPaletteColorLookupTableData = greenData;
imageFrame.bluePaletteColorLookupTableData = blueData;
});

fetchPromises.push(palettePromises);
}

// Only await if there are promises to fetch
if (fetchPromises.length > 0) {
await Promise.all(fetchPromises);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Types } from '@cornerstonejs/core';
import { metaData } from '@cornerstonejs/core';

/**
* Fetches the palette color lookup table data for a given color (red, green, or blue) from the image frame.
* If the data is not present on the image frame, it attempts to fetch it from the metadata store.
* @param imageFrame - The image frame containing palette information
* @param color - The color channel to fetch ('red', 'green', or 'blue')
* @param fallback - Value to return if palette data is not found
* @returns Promise resolving to the palette color lookup table data or fallback value
*/
export function fetchPaletteData(
imageFrame: Types.IImageFrame,
color: 'red' | 'green' | 'blue',
fallback
) {
const data = imageFrame[`${color}PaletteColorLookupTableData`];
if (data) {
return Promise.resolve(data);
}

const result = metaData.get('imagePixelModule', imageFrame.imageId);

if (result && typeof result.then === 'function') {
return result.then((module) =>
module ? module[`${color}PaletteColorLookupTableData`] : fallback
);
} else {
return Promise.resolve(
result ? result[`${color}PaletteColorLookupTableData`] : fallback
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import { default as convertRGBColorByPlane } from './convertRGBColorByPlane';
import { default as convertYBRFullByPixel } from './convertYBRFullByPixel';
import { default as convertYBRFullByPlane } from './convertYBRFullByPlane';
import { default as convertYBRFull422ByPixel } from './convertYBRFull422ByPixel';
import { default as convertPALETTECOLOR } from './convertPALETTECOLOR';
import {
default as convertPaletteColor,
convertPaletteColorWithFetch,
} from './convertPALETTECOLOR';
import { fetchLUTForInstance } from './fetchLUTForInstance';

export {
convertRGBColorByPixel,
convertRGBColorByPlane,
convertYBRFullByPixel,
convertYBRFullByPlane,
convertYBRFull422ByPixel,
convertPALETTECOLOR,
convertPaletteColor,
convertPaletteColorWithFetch,
fetchLUTForInstance,
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
convertYBRFullByPixel,
convertYBRFull422ByPixel,
convertYBRFullByPlane,
convertPALETTECOLOR,
convertPaletteColor,
} from './colorSpaceConverters/index';

function convertRGB(imageFrame, colorBuffer, useRGBA) {
Expand Down Expand Up @@ -39,7 +39,7 @@ export default function convertColorSpace(imageFrame, colorBuffer, useRGBA) {
// for those cases.
convertRGBColorByPixel(imageFrame.pixelData, colorBuffer, useRGBA);
} else if (imageFrame.photometricInterpretation === 'PALETTE COLOR') {
convertPALETTECOLOR(imageFrame, colorBuffer, useRGBA);
convertPaletteColor(imageFrame, colorBuffer, useRGBA);
} else if (imageFrame.photometricInterpretation === 'YBR_FULL_422') {
convertYBRFull422ByPixel(imageFrame.pixelData, colorBuffer, useRGBA);
} else if (imageFrame.photometricInterpretation === 'YBR_FULL') {
Expand Down
21 changes: 20 additions & 1 deletion packages/dicomImageLoader/src/imageLoader/createImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import isColorImageFn from '../shared/isColorImage';
import removeAFromRGBA from './removeAFromRGBA';
import isModalityLUTForDisplay from './isModalityLutForDisplay';
import setPixelDataType from './setPixelDataType';
import { fetchPaletteData } from './colorSpaceConverters/fetchPaletteData';

let lastImageIdDrawn = '';

function createImage(
async function createImage(
imageId: string,
pixelData: ByteArray,
transferSyntax: string,
Expand Down Expand Up @@ -51,6 +52,16 @@ function createImage(

options.allowFloatRendering = canRenderFloatTextures();

let redData, greenData, blueData;
// For PALETTE COLOR images, ensure palette bulkdata is loaded before decoding
if (imageFrame.photometricInterpretation === 'PALETTE COLOR') {
[redData, greenData, blueData] = await Promise.all([
fetchPaletteData(imageFrame, 'red', null),
fetchPaletteData(imageFrame, 'green', null),
fetchPaletteData(imageFrame, 'blue', null),
]);
}

// Get the scaling parameters from the metadata
if (options.preScale.enabled) {
const scalingParameters = getScalingParameters(metaData, imageId);
Expand Down Expand Up @@ -164,6 +175,14 @@ function createImage(
metaData.get(MetadataModules.CALIBRATION, imageId) || {};
const { rows, columns } = imageFrame;

// For PALETTE COLOR images, assign palette bulkdata after decoding
// to avoid copying unnecessary memory to/from the worker
if (imageFrame.photometricInterpretation === 'PALETTE COLOR') {
imageFrame.redPaletteColorLookupTableData = redData;
imageFrame.greenPaletteColorLookupTableData = greenData;
imageFrame.bluePaletteColorLookupTableData = blueData;
}

if (isColorImage) {
if (isColorConversionRequired(imageFrame)) {
canvas.height = imageFrame.rows;
Expand Down
9 changes: 6 additions & 3 deletions packages/dicomImageLoader/src/imageLoader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
convertRGBColorByPlane,
convertYBRFullByPixel,
convertYBRFullByPlane,
convertPALETTECOLOR,
convertPaletteColor,
convertPaletteColorWithFetch,
} from './colorSpaceConverters/index';

import { default as wadouri } from './wadouri/index';
Expand All @@ -29,7 +30,8 @@ const cornerstoneDICOMImageLoader = {
convertRGBColorByPlane,
convertYBRFullByPixel,
convertYBRFullByPlane,
convertPALETTECOLOR,
convertPaletteColor,
convertPaletteColorWithFetch,
wadouri,
wadors,
init,
Expand All @@ -54,7 +56,8 @@ export {
convertRGBColorByPlane,
convertYBRFullByPixel,
convertYBRFullByPlane,
convertPALETTECOLOR,
convertPaletteColorWithFetch,
convertPaletteColor,
wadouri,
wadors,
init,
Expand Down
9 changes: 6 additions & 3 deletions packages/dicomImageLoader/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
convertRGBColorByPlane,
convertYBRFullByPixel,
convertYBRFullByPlane,
convertPALETTECOLOR,
convertPaletteColor,
convertPaletteColorWithFetch,
} from './imageLoader/colorSpaceConverters/index';

import { default as wadouri } from './imageLoader/wadouri/index';
Expand Down Expand Up @@ -32,7 +33,8 @@ const cornerstoneDICOMImageLoader = {
convertRGBColorByPlane,
convertYBRFullByPixel,
convertYBRFullByPlane,
convertPALETTECOLOR,
convertPaletteColor,
convertPaletteColorWithFetch,
wadouri,
wadors,
init,
Expand All @@ -57,7 +59,8 @@ export {
convertRGBColorByPlane,
convertYBRFullByPixel,
convertYBRFullByPlane,
convertPALETTECOLOR,
convertPaletteColor,
convertPaletteColorWithFetch,
wadouri,
wadors,
init,
Expand Down
Loading
Loading