Use case on kidney CT data from KiTS19 challenge
- To convert a DICOM file from grayscale to RGB, you should modify several DICOM tags other than just the pixel data.
- Full source code is available at the end of this post.
DICOM (Digital Imaging and Communications in Medicine) is the international standard for storing and transmitting medical images, and DICOM files are mostly viewed using PACS (picture archiving and communication systems) in hospitals.
While radiologic images such as X-ray, CT, and MR, are grayscale, you may want to edit the images in RGB, e.g., for annotation or image processing. The edited images should be provided in DICOM format to be interpreted by doctors in clinical practice.
This post aims to provide how to convert a grayscale DICOM file to an RGB DICOM file in Python, mainly using the Pydicom package.
Please note: this post does not cover basic how-to’s on handling DICOM data. For those unfamiliar, please check Pydicom documentation.
Here, we use public CT data from the KiTS19 (2019 Kidney Tumor Segmentation Challenge) challenge, which can be download here.
Suppose you developed a model that finds kidney cancer on abdominal CT. We will draw a RED bounding box around the tumor and save the image in DICOM format.
First, load the DICOM file with pydicom
, and you can get pixel data in Hounsfield units (HU) as follows:
import pydicomds = pydicom.dcmread(INPUT_DICOM_PATH)
img = ds.pixel_array # dtype = uint16
img = img.astype(float)
img = img*ds.RescaleSlope + ds.RescaleIntercept
Since Hounsfield units range from about -1000 to over 2000, the contrast and brightness of the image can be adjusted using an appropriate CT window (e.g., width: 400, level: 50 for abdominal CT).
def apply_ct_window(img, window):
# window = (window width, window level)
R = (img-window[1]+0.5*window[0])/window[0]
R[R<0] = 0
R[R>1] = 1
return Rdisplay_img = apply_ct_window(img, [400,50])
Let’s draw a red bounding box around the tumor. Here, I used PIL
library to do so, but you can do whatever you want to. Just make sure to convert the image from grayscale to RGB before any changes and to get and an ndarray
(not pillow image) after editing.
import numpy as np# for this particular example
top, left, bottom, right = [211,99,291,158]
thickness = 4img_bbox = Image.fromarray((255*display_img).astype('uint8'))
img_bbox = img_bbox.convert('RGB')draw = ImageDraw.Draw(img_bbox)
for i in range(thickness):
draw.rectangle(
[left + i, top + i, right - i, bottom - i],
outline=(255,0,0)
)
del drawimg_bbox = np.asarray(img_bbox)
We now have a processed image. Let’s save it in DICOM format.
To overwrite the pixel data, we simply need to convert our new image from an array to bytes. But is that it? Let’s save the file and check on a DICOM viewer (here we used RadiAnt DICOM viewer).
ds.PixelData = img_bbox.tobytes()
ds.save_as(OUTPUT_DICOM_PATH)
Something went wrong. What should we do?
Grayscale and RGB images not only have different raw pixel data but also have several different properties including the number of channels. Therefore, DICOM metadata other than the pixel data should also be modified appropriately.
Grayscale to RGB
Let’s look at some DICOM tags from the original DICOM file that are related to grayscale/RGB format.
(0028, 0004) Photometric Interpretation CS: 'MONOCHROME2'
(0028, 0002) Samples per Pixel US: 1
(0028, 0100) Bits Allocated US: 16
(0028, 0101) Bits Stored US: 12
(0028, 0102) High Bit US: 11
CT data are originally in 2-byte unsigned integer (uint16) and with 1 channel. So, we have to change the DICOM tags according to RGB format with an additional tag ‘Planar Configuration’, which is required when “Samples per Pixel” >1.
ds.PhotometricInterpretation = 'RGB'
ds.SamplesPerPixel = 3
ds.BitsAllocated = 8
ds.BitsStored = 8
ds.HighBit = 7
ds.add_new(0x00280006, 'US', 0)
Now, let’s save the file again and check on the DICOM viewer.
What else could go wrong?
Fortunately, rather intuitive (if you think about structures of grayscale and RGB images) modifications on DICOM tags worked for our example. However, additional changes (in my opinion, hard to think of) may be required regarding Transfer Syntax.
ds.file_meta...
(0002, 0010) Transfer Syntax UID UI: Explicit VR Little Endian
...
If our file were encoded in Big Endian, it would look like this on DICOM viewer.
RGB values appear to be mixed up between adjacent pixels (probably something to do with byte ordering, but not sure exactly why). So, we have to keep the transfer syntax in Little Endian. (Explicit/implicit does not matter in converting to RGB)
ds.is_little_endian = True
ds.fix_meta_info()
Converting a grayscale DICOM file to an RGB DICOM file is sometimes required to provide processed medical images that can be actually used in hospitals. To do so, you must modify DICOM metadata — including photometric interpretation, samples per pixel, and bits —as well as pixel data.