I am converting hand X-rays in DICOM format to PNG format. The code below does this:
import os
import cv2
import pydicom
import numpy as np
from PIL import Image
inputdir = "P:/BoneDataset/DICOM-File/0-RefinedDICOM/"
outdir = 'P:/BoneDataset/DICOM-File/1-ConvertedPics/'
test_list = [f for f in os.listdir(inputdir)]
for f in test_list[:10]:
ds = pydicom.read_file(inputdir + f) # read dicom image
img = ds.pixel_array # get image array
scaled_img = (np.maximum(img,0) / img.max()) * 255.0
img = scaled_img.astype(np.uint8)
cv2.imwrite(outdir + f.replace('.dcm','.png'),img)
The image below shows some of the result (Input (DICOM) --> Output (PNG)):
As you can see, I want the output images to look the same as the input X-ray, however, I get black and white output. Is this because of image threshold issue or something wrong with the file?
EDIT 1:
I tried the code suggested below, however it returns the same output as before, not as the same image as the input.
I've anonymized the DICOM files, hence you can find the DICOM dataset files HERE. Please use these DICOM files.
EDIT 2:
Trying the code edited update's code below works 100%. Also, using cv2.bitwise_not()
also works after the line ds= img.pixel_array
. Like this (Trying this works but the suggested answer works too):
test_list = [f for f in os.listdir(inputdir)]
for f in test_list[:10]:
ds = pydicom.read_file(inputdir + f) # read dicom image
img = ds.pixel_array # get image array
img = cv2.bitwise_not(img)
img = lin_stretch_img(img, 1, 99) # Apply "linear stretching"
cv2.imwrite(outdir + f.replace('.dcm','.png'),img)
As commented, scaling the maximum may not be enough.
We may try "linear stretching", where some low percentile goes to 0, high percentile goes to 255, and levels in between are transformed linearly.
Additional option:
Since many DICOM images have black margins, and white labels, we may want to ignore the minimum and maximum values, when computing the percentiles.
There is no guarantee that the result is going to be the same as the "the input X-ray", but it is probably not going to be black and white.
For testing, I downloaded CASE 1 DICOM samples from here.
Code sample:
import cv2
import pydicom
import numpy as np
def lin_stretch_img(img, low_prc, high_prc, do_ignore_minmax=True):
"""
Apply linear "stretch" - low_prc percentile goes to 0,
and high_prc percentile goes to 255.
The result is clipped to [0, 255] and converted to np.uint8
Additional feature:
When computing high and low percentiles, ignore the minimum and maximum intensities (assumed to be outliers).
"""
# For ignoring the outliers, replace them with the median value
if do_ignore_minmax:
tmp_img = img.copy()
med = np.median(img) # Compute median
tmp_img[img == img.min()] = med
tmp_img[img == img.max()] = med
else:
tmp_img = img
lo, hi = np.percentile(tmp_img, (low_prc, high_prc)) # Example: 1% - Low percentile, 99% - High percentile
if lo == hi:
return np.full(img.shape, 128, np.uint8) # Protection: return gray image if lo = hi.
stretch_img = (img.astype(float) - lo) * (255/(hi-lo)) # Linear stretch: lo goes to 0, hi to 255.
stretch_img = stretch_img.clip(0, 255).astype(np.uint8) # Clip range to [0, 255] and convert to uint8
return stretch_img
# https://www.visus.com/fileadmin/content/pictures/Downloads/JiveX_DICOME_Viewer/case1.zip
ds = pydicom.read_file('case1_008.dcm') # read dicom image
img = ds.pixel_array # get image array
img = lin_stretch_img(img, 1, 99) # Apply "linear stretching" (lower percentile 1 goes to 0, and percentile 99 to 255).
cv2.imwrite('case1_008.png', img)
Output of img = lin_stretch_img(img, 0.01, 99.99)
(may give better result):
The polarity of the sample DICOM images is inverted.
The minimum value is intended to be displayed as white, and the maximum as black.
For correcting the polarity, we may execute img = 255-img
(after converting to uint8
).
Checking if the polarity is inverted:
According to the documentation, if Photometric Interpretation equals 'MONOCHROME1', then the polarity is inverted ('MONOCHROME2' is not inverted).
MONOCHROME1
Pixel data represent a single monochrome image plane. The minimum sample
value is intended to be displayed as white after any VOI gray scale transformations have been performed. See PS3.4. This value may be used only when Samples per Pixel (0028,0002) has a value of 1. May be used for pixel data in a Native (uncompressed) or Encapsulated (compressed) format.
Inverting polarity if Photometric Interpretation is 'MONOCHROME1':
if ds[0x0028, 0x0004].value == 'MONOCHROME1':
img = 255-img
The documentation also says we have to apply it "after VOI gray scale transformations".
Applying "VOI gray scale transformations" is described here:
img = apply_voi_lut(img, ds, index=0)
Updated code sample:
import cv2
import pydicom
from pydicom.pixel_data_handlers.util import apply_voi_lut
import numpy as np
def lin_stretch_img(img, low_prc, high_prc, do_ignore_minmax=True):
"""
Apply linear "stretch" - low_prc percentile goes to 0,
and high_prc percentile goes to 255.
The result is clipped to [0, 255] and converted to np.uint8
Additional feature:
When computing high and low percentiles, ignore the minimum and maximum intensities (assumed to be outliers).
"""
# For ignoring the outliers, replace them with the median value
if do_ignore_minmax:
tmp_img = img.copy()
med = np.median(img) # Compute median
tmp_img[img == img.min()] = med
tmp_img[img == img.max()] = med
else:
tmp_img = img
lo, hi = np.percentile(tmp_img, (low_prc, high_prc)) # Example: 1% - Low percentile, 99% - High percentile
if lo == hi:
return np.full(img.shape, 128, np.uint8) # Protection: return gray image if lo = hi.
stretch_img = (img.astype(float) - lo) * (255/(hi-lo)) # Linear stretch: lo goes to 0, hi to 255.
stretch_img = stretch_img.clip(0, 255).astype(np.uint8) # Clip range to [0, 255] and convert to uint8
return stretch_img
# https://www.visus.com/fileadmin/content/pictures/Downloads/JiveX_DICOME_Viewer/case1.zip
ds = pydicom.read_file('1.2.392.200036.9125.9.0.152034855.3288075520.2287343482.dcm') # read dicom image
img = ds.pixel_array # get image array
# https://pydicom.github.io/pydicom/stable/old/working_with_pixel_data.html#voi-lut-or-windowing-operation
# Apply "VOI gray scale transformations":
img = apply_voi_lut(img, ds, index=0)
img = lin_stretch_img(img, 0.1, 99.9) # Apply "linear stretching" (lower percentile 0.1 goes to 0, and percentile 99.9 to 255).
# https://dicom.innolitics.com/ciods/rt-dose/image-pixel/00280004
if ds[0x0028, 0x0004].value == 'MONOCHROME1':
img = 255-img # Invert polarity if Photometric Interpretation is 'MONOCHROME1'
cv2.imwrite('1.2.392.200036.9125.9.0.152034855.3288075520.2287343482.png', img)