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

Re-compressing data results in bright image #1637

Closed
shensmobile opened this issue Apr 19, 2022 · 10 comments
Closed

Re-compressing data results in bright image #1637

shensmobile opened this issue Apr 19, 2022 · 10 comments

Comments

@shensmobile
Copy link

Describe the issue

I have a DCM that I unfortunately cannot share, but I will do my best to describe the scenario. The file is compressed as a 16 bit JPEG2000. The issue I'm having is that I want to decompress the image, do some actions to the pixels, and then shove it back into the DCM. However, if I extract the image and then immediately recompress and put it back into the .dcm, the image is bad. The blacks become white, while everything else stays the same. It's not inverted, it's more like the values have over-flowed.

Original DCM
image

New DCM
image

When I print(img), I get the following:

[[-2048 -2048 -2048 ... -2048 -2048 -2048]
[-2048 -2048 -2048 ... -2048 -2048 -2048]
[-2048 -2048 -2048 ... -2048 -2048 -2048]
...
[-2048 -2048 -2048 ... -2048 -2048 -2048]
[-2048 -2048 -2048 ... -2048 -2048 -2048]
[-2048 -2048 -2048 ... -2048 -2048 -2048]]

min, max = -2065, 1171

Steps to reproduce

I am using the following code to extract the pixel data and then put it back into the same file:

from pydicom import dcmread
from pydicom.encaps import encapsulate
from PIL import Image
from io import BytesIO

ds = dcmread("images/bad_ct.dcm")
img = ds.pixel_array

frame_data = []
pil_img = Image.fromarray(img, "I;16")
with BytesIO() as output:
     pil_img.save(output, format="jpeg2000")
     frame_data.append(output.getvalue())

ds.PixelData = encapsulate(frame_data)
ds.file_meta.TransferSyntaxUID = "1.2.840.10008.1.2.4.90"
ds['PixelData'].is_undefined_length = True
ds.is_decompressed = False

ds.save_as("test.dcm")

Info about the file:

1.2.840.10008.1.2.4.91
(0028, 0002) Samples per Pixel US: 1
(0028, 0004) Photometric Interpretation CS: 'MONOCHROME2'
(0028, 0010) Rows US: 512
(0028, 0011) Columns US: 512
(0028, 0030) Pixel Spacing DS: [0.892, 0.892]
(0028, 0100) Bits Allocated US: 16
(0028, 0101) Bits Stored US: 16
(0028, 0102) High Bit US: 15
(0028, 0103) Pixel Representation US: 1
(0028, 1050) Window Center DS: '70.0'
(0028, 1051) Window Width DS: '500.0'
(0028, 1052) Rescale Intercept DS: '0.0'
(0028, 1053) Rescale Slope DS: '1.0'
(0028, 2110) Lossy Image Compression CS: '01'
(0028, 2112) Lossy Image Compression Ratio DS: '8.0'
(0028, 2114) Lossy Image Compression Method CS: 'ISO_15444_1'
(7fe0, 0010) Pixel Data OB: Array of 65286 elements

Using Pydicom 2.3.0.

@shensmobile
Copy link
Author

Also, just did a sanity check between bad_ct.dcm and test.dcm, ds.PixelRepresentation = 0 for test.dcm. However, even if I force ds.PixelRepresentation = 1 and confirm with print(ds), when I invoke ds.save_as("test.dcm"), the resultant file has a pixel representation of 0. What is going on?

@scaramallion
Copy link
Member

Your problem lies with Pillow, not with pydicom. Pillow will treat signed image data as unsigned when compressing J2K, which will give you a non-conformant result.

@shensmobile
Copy link
Author

shensmobile commented Apr 19, 2022

Do you have a recommendation of an alternative method of saving 16 bit signed data to J2K and shunting it into frame_data?

Also, is PIL encoding the J2K the reason that I can't change PixelRepresentation?

@scaramallion
Copy link
Member

scaramallion commented Apr 19, 2022

Also, is PIL encoding the J2K the reason that I can't change PixelRepresentation?

No, that should work. Could you show us your code?

Do you have a recommendation of an alternative method of saving 16 bit signed data to J2K and shunting it into frame_data?

There's nothing that's particularly straight-forward, but GDCM is probably your best bet.

@shensmobile
Copy link
Author

shensmobile commented Apr 19, 2022

The code I'm using:

from pydicom import dcmread
from pydicom.encaps import encapsulate
from PIL import Image
from io import BytesIO

ds = dcmread("images/bad_ct.dcm")
img = ds.pixel_array

frame_data = []
pil_img = Image.fromarray(img, "I;16")
with BytesIO() as output:
     pil_img.save(output, format="jpeg2000")
     frame_data.append(output.getvalue())

ds.PixelData = encapsulate(frame_data)
ds.file_meta.TransferSyntaxUID = "1.2.840.10008.1.2.4.90"
ds['PixelData'].is_undefined_length = True
ds.is_decompressed = False
ds.PixelRepresentation = 1

ds.save_as("test.dcm")
print(ds.PixelRepresentation)

The printout confirms that it's set to "1" but when I open test.dcm, it's set to 0

@scaramallion
Copy link
Member

scaramallion commented Apr 19, 2022

I can't reproduce with

from pydicom import dcmread
from pydicom.data import get_testdata_file

ds = get_testdata_file("CT_small.dcm", read=True)
assert ds.PixelRepresentation == 1
ds.PixelRepresentation = 0
assert ds.PixelRepresentation == 0
ds.save_as("test.dcm")

ns = dcmread("test.dcm")
assert ns.PixelRepresentation == 0
ns.PixelRepresentation = 1
assert ns.PixelRepresentation == 1
ns.save_as("test2.dcm")

es = dcmread("test2.dcm")
assert es.PixelRepresentation == 1

Could you double check that you're looking at the correct test.dcm file?

@shensmobile
Copy link
Author

shensmobile commented Apr 19, 2022

That's so bizarre. If I open the file using PyDicom, PixelRepresentation is indeed 1. However, my DICOM viewer (microdicomviewer) shows it as 0. That's strange.

I checked in Mango as well, the image does have the correct Pixel Representation of 1. Unfortuantely, the image is still all white.

I will try compressing with GDCM. Hopefully I can do it programmatically in Python in memory. Thank you for your help!

@scaramallion is there any reason why re-casting the int16 to uint16 and then saving it using PIL still results in a file that doesn't look quite right? Does the JPEG byte values get mapped directly to the window/level of the image? That's essentially what I'm seeing, if I move my level up by +32767, it looks like the original file.

Edit 2: Sorry if it seems like I'm rambling here, is it possible that they've directly mapped JPEG pixel values to HU? If so, could I manually set the slope/intercept to re-adjust the recasted uint16 (+32767) automatically for the final viewer?

Edit 3: setting ds.RescaleIntercept = -32767 worked. Woohoo :)

@scaramallion
Copy link
Member

scaramallion commented Apr 20, 2022

You have to be careful with converting to unsigned then compressing using Pillow because the J2K file will have a flag that it's unsigned while the DICOM dataset will indicate its signed. Some (non-conformant) implementations may not correctly interpret this.

It's possible that this is what you're seeing with your viewer and it's claimed Pixel Representation of 0 when it should be 1.

@shensmobile
Copy link
Author

Sorry, I forgot to add that I also had to manually set ds.PixelRepresentation=0. You're right that it is now unsigned. I'm hoping that I don't have to run into this scenario too often. Hopefully we get an easy way to write signed int16 to J2K through PIL soon!

@darcymason
Copy link
Member

Closing as appears not to be pydicom related.

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

No branches or pull requests

3 participants