Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(color): Fix RGB inconsistencies #829

Merged
merged 12 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
setTitleAndDescription,
} from '../../../../utils/demo/helpers';

import { cache } from '@cornerstonejs/core';

// This is for debugging purposes
console.warn(
'Click on index.ts to open source code for this example --------->'
Expand All @@ -45,6 +47,7 @@ addToggleButtonToToolbar({
defaultToggle: false,
onClick(toggle) {
toggle ? setUseCPURendering(true) : setUseCPURendering(false);
cache.purgeCache();
},
});

Expand All @@ -53,6 +56,7 @@ addToggleButtonToToolbar({
defaultToggle: false,
onClick(toggle) {
toggle ? setPreferSizeOverAccuracy(true) : setPreferSizeOverAccuracy(false);
cache.purgeCache();
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ 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 = external.cornerstone.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
*
Expand All @@ -27,55 +49,51 @@ export default function (
): void {
const numPixels = imageFrame.columns * imageFrame.rows;
const pixelData = imageFrame.pixelData;
let rData = imageFrame.redPaletteColorLookupTableData;

if (!rData) {
// request from metadata provider since it might grab it from bulkdataURI
rData = external.cornerstone.metaData.get(
'imagePixelModule',
imageFrame.imageId
)?.redPaletteColorLookupTableData;
}

let gData = imageFrame.greenPaletteColorLookupTableData;

if (!gData) {
gData = external.cornerstone.metaData.get(
'imagePixelModule',
imageFrame.imageId
)?.greenPaletteColorLookupTableData;
}

let bData = imageFrame.bluePaletteColorLookupTableData;

if (!bData) {
bData = external.cornerstone.metaData.get(
'imagePixelModule',
imageFrame.imageId
)?.bluePaletteColorLookupTableData;
}

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 len = imageFrame.redPaletteColorLookupTableData.length;

let palIndex = 0;

let bufferIndex = 0;
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 start = imageFrame.redPaletteColorLookupTableDescriptor[1];
const shift =
imageFrame.redPaletteColorLookupTableDescriptor[2] === 8 ? 0 : 8;
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;
}

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

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

Expand All @@ -90,25 +108,6 @@ 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];
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ export default function (
useRGBA: boolean
): void {
if (imageFrame === undefined) {
throw new Error('decodeRGB: rgbBuffer must not be undefined');
throw new Error('decodeRGB: rgbBuffer must be defined');
}
if (imageFrame.length % 3 !== 0) {
throw new Error('decodeRGB: rgbBuffer length must be divisible by 3');
throw new Error(
`decodeRGB: rgbBuffer length ${imageFrame.length} must be divisible by 3`
);
}

const numPixels = imageFrame.length / 3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ export default function (
useRGBA: boolean
): void {
if (imageFrame === undefined) {
throw new Error('decodeRGB: rgbBuffer must not be undefined');
throw new Error('decodeRGB: rgbBuffer must be defined');
}
if (imageFrame.length % 3 !== 0) {
throw new Error('decodeRGB: rgbBuffer length must be divisible by 3');
throw new Error(
`decodeRGB: rgbBuffer length ${imageFrame.length} must be divisible by 3`
);
}

const numPixels = imageFrame.length / 3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ export default function (
useRGBA: boolean
): void {
if (imageFrame === undefined) {
throw new Error('decodeRGB: ybrBuffer must not be undefined');
throw new Error('convertYBRFull422ByPixel: ybrBuffer must be defined');
}
if (imageFrame.length % 2 !== 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment here that the length is divisible by 2 because for 4 pixels (a 2x2 array), there are 4 y and 2 b and 2 r values, so 4+2+2 / 4 = 2 bytes per pixel

throw new Error('decodeRGB: ybrBuffer length must be divisble by 2');
throw new Error(
`convertYBRFull422ByPixel: ybrBuffer length ${imageFrame.length} must be divisible by 2`
);
}

const numPixels = imageFrame.length / 2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ export default function (
useRGBA: boolean
): void {
if (imageFrame === undefined) {
throw new Error('decodeRGB: ybrBuffer must not be undefined');
throw new Error('convertYBRFullByPixel: ybrBuffer must be defined');
}
if (imageFrame.length % 3 !== 0) {
throw new Error('decodeRGB: ybrBuffer length must be divisble by 3');
throw new Error(
`convertYBRFullByPixel: ybrBuffer length ${imageFrame.length} must be divisible by 3`
);
}

const numPixels = imageFrame.length / 3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ export default function (
useRGBA: boolean
): void {
if (imageFrame === undefined) {
throw new Error('decodeRGB: ybrBuffer must not be undefined');
throw new Error('convertYBRFullByPlane: ybrBuffer must be defined');
}
if (imageFrame.length % 3 !== 0) {
throw new Error('decodeRGB: ybrBuffer length must be divisble by 3');
throw new Error(
`convertYBRFullByPlane: ybrBuffer length ${imageFrame.length} must be divisible by 3`
);
}

const numPixels = imageFrame.length / 3;
Expand Down
32 changes: 15 additions & 17 deletions packages/dicomImageLoader/src/imageLoader/createImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,13 @@ import {
PixelDataTypedArray,
} from '../types';
import convertColorSpace from './convertColorSpace';
import isColorConversionRequired from './isColorConversionRequired';
import decodeImageFrame from './decodeImageFrame';
import getImageFrame from './getImageFrame';
import getScalingParameters from './getScalingParameters';
import { getOptions } from './internal/options';
import isColorImageFn from '../shared/isColorImage';

/**
* When using typical decompressors to decompress compressed color images,
* the resulting output is in RGB or RGBA format. Additionally, these images
* are in planar configuration 0, meaning they are arranged by plane rather
* than by color. Consequently, the images only require a transformation from
* RGBA to RGB without needing to use the photometric interpretation to convert
* to RGB or adjust the planar configuration.
*/
const TRANSFER_SYNTAX_USING_PHOTOMETRIC_COLOR = {
'1.2.840.10008.1.2.1': 'application/octet-stream',
'1.2.840.10008.1.2': 'application/octet-stream',
'1.2.840.10008.1.2.2': 'application/octet-stream',
'1.2.840.10008.1.2.5': 'image/dicom-rle',
};

let lastImageIdDrawn = '';

function isModalityLUTForDisplay(sopClassUid: string): boolean {
Expand Down Expand Up @@ -156,6 +142,18 @@ function createImage(
? true
: options.useNativeDataType || decodeConfig.use16BitDataType;

// Remove any property of the `imageFrame` that cannot be transferred to the worker,
// such as promises and functions.
// This is necessary because the `imageFrame` object is passed to the worker.
Object.keys(imageFrame).forEach((key) => {
if (
typeof imageFrame[key] === 'function' ||
imageFrame[key] instanceof Promise
) {
delete imageFrame[key];
}
});

const decodePromise = decodeImageFrame(
imageFrame,
transferSyntax,
Expand Down Expand Up @@ -243,10 +241,10 @@ function createImage(
cornerstone.metaData.get(MetadataModules.SOP_COMMON, imageId) || {};
const calibrationModule =
cornerstone.metaData.get(MetadataModules.CALIBRATION, imageId) || {};
const { rows, columns } = imageFrame;

if (isColorImage) {
const { rows, columns } = imageFrame;
if (TRANSFER_SYNTAX_USING_PHOTOMETRIC_COLOR[transferSyntax]) {
if (isColorConversionRequired(imageFrame, useRGBA)) {
canvas.height = imageFrame.rows;
canvas.width = imageFrame.columns;
const context = canvas.getContext('2d');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* This function checks color space conversion data requirements before
* applying them. This function was created to solve problems like the one
* discussed in here https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533/17
* In this case, Orthanc server converts the pixel data from YBR to RGB, but maintain
* the photometricInterpretation dicom tag in YBR
* @param imageFrame
* @param RGBA
* @returns
*/
export default function isColorConversionRequired(imageFrame, RGBA) {
if (imageFrame === undefined) {
return false;
}
const { rows, columns, photometricInterpretation, pixelDataLength } =
imageFrame;

if (photometricInterpretation.endsWith('420')) {
return (
pixelDataLength !==
(3 * Math.ceil(columns / 2) + Math.floor(columns / 2)) * rows
);
} else if (photometricInterpretation.endsWith('422')) {
return (
pixelDataLength !==
(3 * Math.ceil(columns / 2) + Math.floor(columns / 2)) *
Math.ceil(rows / 2) +
Math.floor(rows / 2) * columns
);
} else {
return photometricInterpretation !== 'RGB';
}
}