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

Conversation

rodrigobasilio2022
Copy link
Contributor

Context

This PR aims to fix the problem with RGB inconsistencies in loading specific color dicom images

Changes & Results

Testing

Checklist

PR

  • [] My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

Public Documentation Updates

  • [] The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • [] "OS:
  • [] "Node version:
  • [] "Browser:

@netlify
Copy link

netlify bot commented Oct 14, 2023

Deploy Preview for cornerstone-3d-docs ready!

Name Link
🔨 Latest commit bf9a8c2
🔍 Latest deploy log https://app.netlify.com/sites/cornerstone-3d-docs/deploys/6655f6995682bd00084701ae
😎 Deploy Preview https://deploy-preview-829--cornerstone-3d-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@sedghi
Copy link
Member

sedghi commented Oct 16, 2023

This part of the code is very tricky to touch. Please explain the logic behind your addition

@rodrigobasilio2022
Copy link
Contributor Author

rodrigobasilio2022 commented Oct 16, 2023

This part of the code is very tricky to touch. Please explain the logic behind your addition

As stated in the lines 21-28 of the same file, the purpose of the code is to transform from RGBA -> RGB, as the main job was already been done by the decompressors. So if the data is already in RGB (3 bytes per voxel), then there is no need to go further in the code. Also the code in here

if (imageFrame === undefined) {
throw new Error('decodeRGB: ybrBuffer must not be undefined');
}
if (imageFrame.length % 2 !== 0) {
throw new Error('decodeRGB: ybrBuffer length must be divisble by 3');
}
didnt make much sense (wrong divisor indication, function name and check as it didnt take into account the number of elements...)

@sedghi
Copy link
Member

sedghi commented Oct 16, 2023

I'm pretty certain this breaks a lot of other color images. It is not just RGBA -> RGB, it also handles color space conversions

if (TRANSFER_SYNTAX_USING_PHOTOMETRIC_COLOR[transferSyntax]) {
          canvas.height = imageFrame.rows;
          canvas.width = imageFrame.columns;
          const context = canvas.getContext('2d');
          const imageData = context.createImageData(
            imageFrame.columns,
            imageFrame.rows
          );
          convertColorSpace(imageFrame, imageData.data, useRGBA);
          imageFrame.imageData = imageData;
          imageFrame.pixelData = imageData.data;
          imageFrame.pixelDataLength = imageData.data.length;
        }

@rodrigobasilio2022
Copy link
Contributor Author

I'm pretty certain this breaks a lot of other color images. It is not just RGBA -> RGB, it also handles color space conversions

if (TRANSFER_SYNTAX_USING_PHOTOMETRIC_COLOR[transferSyntax]) {
          canvas.height = imageFrame.rows;
          canvas.width = imageFrame.columns;
          const context = canvas.getContext('2d');
          const imageData = context.createImageData(
            imageFrame.columns,
            imageFrame.rows
          );
          convertColorSpace(imageFrame, imageData.data, useRGBA);
          imageFrame.imageData = imageData;
          imageFrame.pixelData = imageData.data;
          imageFrame.pixelDataLength = imageData.data.length;
        }

I see. But we need to make sure which conversions were completely handled by decompression. This particular color space, ybr full 422 was completely handled by the decompression and doesn't need any further processing

@sedghi
Copy link
Member

sedghi commented Oct 16, 2023

Yeah i know exactly what you are talking about. I tried to find the correct condition, but everytime I tried something it failed something else. @wayfarer3130 maybe he has an idea of what is the best way to identify if something comes out of the decompresser as RGB

@rodrigobasilio2022
Copy link
Contributor Author

Yeah i know exactly what you are talking about. I tried to find the correct condition, but everytime I tried something it failed something else. @wayfarer3130 maybe he has an idea of what is the best way to identify if something comes out of the decompresser as RGB

What if for the moment we get the requirements for each conversion and only allow if the input is correct: for example ybr_full_422 expects two bytes data. What if we check that and if the requirements does match we allow proceed and if not we return the data as is? The only extepcting two byte data is the YBR_FULL_422, all others expects three byte data

@rodrigobasilio2022
Copy link
Contributor Author

Adde a new function that checks the requirements for color space convertions. It convers only one color space conversion for now, the YBR_FULL_422, as it tests if the voxel byte size is equal to 2.

@rodrigobasilio2022
Copy link
Contributor Author

Changed as suggested

@sedghi
Copy link
Member

sedghi commented Oct 17, 2023

Testing on https://www.cornerstonejs.org/live-examples/dicomimageloaderwadouri

These two are broken which is not in main right now

CleanShot 2023-10-17 at 11 37 33

@rodrigobasilio2022
Copy link
Contributor Author

Fixed the issue!!!

* @param RGBA
* @returns
*/
export default function isColorConversionRequirementsFulfilled(
Copy link
Member

Choose a reason for hiding this comment

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

@wayfarer3130 Can you please look at this function? Thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will limit the checks to only YBR_FULL_422

Copy link
Collaborator

Choose a reason for hiding this comment

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

Limitting the checks to the types ending in _420 or _422 is sufficient, but I would also add an override to allow forcing the issue to return RGB if a flag is set and it starts with YBR - that will then automagically include things like:
YBR_ICT, YBR_RCT, YBR_PARTIAL_422
etc.

There are a few other retired PMI values such as HSV and CMYK, but I doubt you will ever see those.

@rodrigobasilio2022
Copy link
Contributor Author

Changed as suggested

@sedghi
Copy link
Member

sedghi commented Feb 15, 2024

I don't think we need this after this PR can someone confirm? OHIF/Viewers#3883

@@ -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

return false;
}
const { rows, columns } = imageFrame;
if (imageFrame.photometricInterpretation === 'YBR_FULL_422') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can check endsWith 422 or endsWIth 420 to support all the test values we can check.

Copy link
Member

@sedghi sedghi Mar 20, 2024

Choose a reason for hiding this comment

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

as we talked to Bill
the formula should be edited to

for ybr_Full_422

ybr y ybr y ybr .... (ybr or y)
y   y  y  y  y ........ y
..
..
..
(last row)
ybr y ybr y ybr y ... (ybr or y)
OR 
y   y  y   y  y   y 

-> 3 * ceil(cols/2) + florr(cols/2) * ceil(rows/2) + cols * floor(rows/2)


number of ybr y ybr y ybr y elements

ybr full 420

ybr y ybr y ybr y
ybr y ybr y ybr y

for ybr_420
(3 * ceilt(cols/2) + floor (col/2)) * rows

if it is /3 -> convert as RGB

Copy link
Collaborator

Choose a reason for hiding this comment

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

as we talked to Bill the formula should be edited to

for ybr_Full_422

ybr y ybr y ybr .... (ybr or y)
y   y  y  y  y ........ y
..
..
..
(last row)
ybr y ybr y ybr y ... (ybr or y)
OR 
y   y  y   y  y   y 

-> 3 * ceil(cols/2) + florr(cols/2) * ceil(rows/2) + cols * floor(rows/2)

That is:
(3 * ceil(cols/2) + floor(cols/2)) * ceil(rows/2) + cols * floor(rows/2)

Note the parenthesis around the first expression. That breaks down into:
There are ceil(rows/2) YBR containing rows (think about 1,2,3 rows, having 1 1 and 2 YBR containing rows)
There are floor(rows/2) NON YBR containing rows. Those have length cols as they are just y containing.
For each YBR row, there are ceil(cols/2) YBR elements - think about hte same thing as the rows, but for columns - the first, third, fifth etc column has YBR. Each YBR has 3 elements (Y b and r)
Then, for each YBR row, there are floor(cols/2) Y element only.

const { rows, columns } = imageFrame;
if (imageFrame.photometricInterpretation === 'YBR_FULL_422') {
return imageFrame.pixelDataLength === 2 * rows * columns && rows % 2 === 0;
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add a settable flag allowing coercion to RGB if the response is uncompressed, and the original is YBR of some flavour. That flag could go in cornerstone settings, and maybe be triggered based on the URL prefix (eg url.startsWith), or a regular expression match.

Copy link
Member

Choose a reason for hiding this comment

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

Can you please add more to this? I don't understand

Copy link
Collaborator

Choose a reason for hiding this comment

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

If the photometric interpretation is YBR_FULL_422, BUT the length is 3rowscolumns, then assume it is the configurable value of either RGB or YBR_FULL. That is invalid data, but is common enough that we see it regularly as a "bug" fix request.
To decide whether RGB or YBR_FULL, check the datasource configuration - something like:
UncompressedColorEncoding=YBR_FULL | RGB (default is RGB)
and perhaps
ForceColorEncoding=AllUncompressed | All | WrongLength (default is WrongLength)

Then, have the above function return the PMI to use for the given image.

const {ForceColorEncoding = WrongLength, UncompressedColorEncoding = RGB} = dataSourceConfiguration;

if( pixel length === 3 * rows * columns && pmi ==='YBR_FULL_422 && forceColorEncoding===WrongLength ) {
return UncompressedColorEncoding

indicating that this image needs decompression handling, in the specified color encoding.

@wayfarer3130
Copy link
Collaborator

I don't think we need this after this PR can someone confirm? OHIF/Viewers#3883

Unfortunately, the back end can still decide to reply in uncompressed, and they can still be invalid, so yes, I think this is a good idea still. It is technically wrong, but practically is the right thing to do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants