Skip to content

Commit

Permalink
fix(color): Fix RGB inconsistencies (#829)
Browse files Browse the repository at this point in the history
* Fix RGB inconsistencies

* Add color space conversion check

* refactor(comments) : refactor comments in code

* refactor(code): change requirements YBR_FULL_422

* fix(color): fix bug in color example

* fix bug in image loader example

* Refactoring function

* fix: correct typo in color space conversion function

* refactor: Refactor color space conversion functions

---------

Co-authored-by: Alireza <[email protected]>
  • Loading branch information
rodrigobasilio2022 and sedghi authored May 28, 2024
1 parent 76cc2c2 commit 2fc22bc
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 92 deletions.
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) {
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';
}
}

0 comments on commit 2fc22bc

Please sign in to comment.