How to convert grayscale DICOM file to RGB DICOM file with Python

Use case on kidney CT data from KiTS19 challenge

Jae Won Choi, MD
Analytics Vidhya
4 min readFeb 23, 2021

--

TL;DR

  • 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.

Introduction

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.

Load DICOM file and work with pixel data

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 R
display_img = apply_ct_window(img, [400,50])
Case 26, Image 174 from the KiTS19 dataset

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 = 4
img_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 draw
img_bbox = np.asarray(img_bbox)
CT image converted to RGB, with red bounding box on the tumor

We now have a processed image. Let’s save it in DICOM format.

Save the new pixel data

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)
Oh no…

Something went wrong. What should we do?

Not just pixel data, but also DICOM tags

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.

Ta-da!

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.

The red line turns in to stripes of RGB

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()

Full Source Code

Full source code

Conclusion

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.

--

--