MD.ai
How-To Guides

Convert DICOM

Working with DICOM converted from JPEG/PNG

When jpeg or png images are uploaded to an MD.ai DICOM dataset, it gets wrapped as a DICOM file with the appropriate DICOM tags. Optionally, these images can be specified using a filename schema that defines hierarchical relationships (exam/series/image). When this dataset is exported, the files are DICOM, not the original jpeg/png images. The original jpeg/png image filenames can be found in the ImageComments tag.

If you would like to use the converted DICOM directly, care must be taken to use the correct color space, since JPEG uses YCbCr, which is specified by DICOM in the PhotometricInterpretation tag with a value of YBR_FULL_422 (or variants). Usually, we want to normalize to the RGB color space when working with machine learning code. The following example code ensures that the resulting pixel array from color DICOM (such as those converted from JPEG/PNG) is converted to RGB, by reading the SamplesPerPixel and PhotometricInterpretation tags and calling the pydicom utility function convert_color_space if necessary:

import pydicom
from pydicom.pixel_data_handlers.util import convert_color_space
 
ds = pydicom.dcmread('example.dcm')
 
try:
    is_color = ds.SamplesPerPixel == 3
except Exception:
    is_color = False
 
try:
    color_space = ds.PhotometricInterpretation
except Exception:
    color_space = ""
 
arr = ds.pixel_array
if is_color and color_space != "RGB":
    arr = convert_color_space(arr, color_space, "RGB")

Convert DICOM to JPEG/PNG

Converts a single dicom file to an 8 bit format

# Code modified from Ian Pan https://storage.googleapis.com/kaggle-forum-message-attachments/1010629/17014/convert_to_jpeg_for_kaggle.py
 
def convert_ct_dicom_to_8bit(dicom_file, windows = [[350,40],[1500,-500],[120,70]], imsize=(256.,256.), should_remove_padding = True):
    '''
    Given a DICOM file, window specifications, and image size, return the
    image as a Numpy array scaled to [0,255] of the specified size.
 
    Parameters
    ----------
    dicom_file: str
        filename that ends in .dcm
    windows: list of lists of ints
        list of window width and window level values
    imsize: tuplet of float
        desired output image size
    should_remove_padding: bool
        if True will remove extra rows/columns of zeroes around the image
    '''
    array = apply_slope_intercept(dicom_file)
 
    if should_remove_padding:
        array = remove_padding(array)
 
    # different width, level for each RGB channel
    image = apply_windows(array, windows)
    # resize
    image = resize(image, imsize)
 
    return image

Save as jpg or png

    image = convert_ct_dicom_to_8bit(dicom_file, windows = [[350,40],[1500,-500],[120,70]], imsize=(256.,256.), should_remove_padding = True)
    im = Image.fromarray(image)
    im.save(dicom_file[-4:] + '.jpg')
    # or
    im.save(dicom_file[-4:] + '.png')

Supporting functions

import pandas as pd
import numpy as np
import pydicom
from scipy.ndimage.interpolation import zoom
 
# Applies slope and intercept from DICOM tags
def apply_slope_intercept(dicom_file):
    array = dicom_file.pixel_array.copy()
    try:
        slope = float(dicom_file.RescaleSlope)
        intercept = float(dicom_file.RescaleIntercept)
    except Exception:
        slope = 1
        intercept = 0
    if slope != 1 or intercept != 0:
        array = array * slope
        array = array + intercept
    return array
 
# Removes zeroes around image
def remove_padding(array):
    array = array.copy()
    nonzeros = np.nonzero(array)
    x1 = np.min(nonzeros[0]) ; x2 = np.max(nonzeros[0])
    y1 = np.min(nonzeros[1]) ; y2 = np.max(nonzeros[1])
    return array[x1:x2,y1:y2]
 
# Apply different W/L settings to different channels to take advantage of RGB structure
def apply_windows(array, windows):
    layers = []
    for values in windows:
        if len(value) >= 2:
            ww = values[0]
            wl = values[1]
            layers.append(np.expand_dims(window(array, WL=wl, WW=ww), axis=3))
    if len(layers) == 0:
        return np.expand_dims(window(array, WL=350, WW=40), axis=3)
    else:
        return np.concatenate(layers, axis=3)
 
# Resize
def resize(image, imsize):
    rat = max(imsize) / np.max(image.shape[1:])
    return zoom(image, [1.,rat,rat,1.], prefilter=False, order=1)

Common window width and window level for CT exams

TargetWWWL
brain8040
subdural21575
stroke_1832
stroke_24040
temporal_bone2800600
neck_soft_tissue37540
lung1500-500
emphysema800-800
mediastinum40040
pulmonary_embolism700100
abdomen35040
liver12070
kidney70050
bone2500480

Convert JPG, PNG to DICOM

Converts a single jpg or png file to dicom format

import os
import subprocess
import tempfile
import uuid
from PIL import Image
from pydicom.uid import generate_uid
 
 
def from_jpeg(jpeg_fp, dicom_tags={}):
    """Converts JPEG to DICOM as secondary capture, with minimal DICOM tags.
    If JPEG mode is RGBA or CMYK, we must first convert to RGB since these photometric
    interpretations have been retired in the DICOM standard:
    http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.3.html.
    Returns: path to DICOM tempfile
    """
    # If JPEG is RGBA/CMYK mode, convert to RGB mode first.
    im = Image.open(jpeg_fp)
    if im.mode in ("RGBA", "CMYK"):
        im2 = im.convert("RGB")
        im2.save(jpeg_fp)
        im2.close()
    im.close()
 
    dicom_fp = os.path.join(tempfile.gettempdir(), f"{str(uuid.uuid4())}.dcm")
    try:
        cmd = ["img2dcm", jpeg_fp, dicom_fp]
        for key, value in dicom_tags.items():
            cmd.extend(["-k", f"{key}={value}"])
        subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
    except Exception:
        return None
    return dicom_fp
 
 
def from_png(png_fp, dicom_tags={}):
    """Converts PNG to DICOM as secondary capture, with minimal DICOM tags.
    Returns: path to DICOM tempfile
    """
    jpeg_fp = os.path.join(tempfile.gettempdir(), f"{str(uuid.uuid4())}.jpg")
    try:
        cmd = ["convert", png_fp, jpeg_fp]
        subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
    except Exception:
        return None
    return from_jpeg(jpeg_fp, dicom_tags=dicom_tags)

Convert CT nifti to DICOM

Use the convert_ct function in common_utils to convert nifti files to DICOM for uploading to MD.ai. You'll need to choose your input and output directories. Optionally, you can change the plane or default window/level settings. You'll also need a sample dicom which you can download from here

results = mdai.common_utils.convert_ct(
    input_dir=None,
    output_dir=None,
    input_ext=".nii.gz",
    plane="axial",
    sample_dicom_fp=os.path.join(os.path.dirname(""), "./sample_dicom.dcm"),
    window_center=40,
    window_width=350,
)

This function will write the converted DICOM files to the output directory and will give each image from a nifti file the same Study and Series UIDs. Use the CLI tool to upload the newly created DICOM images into your project

Convert DICOM to nifti

Use the dicom2nifiti library from icometrix

!pip install dicom2nifti
import dicom2nifti
dicom2nifti.convert_directory(dicom_directory, output_folder, compression=True, reorient=True)

This will convert the dicom files from the dicom_directory to the output_folder.

Convert MR .mha files to DICOM

Use the function provided below to convert MR modality .mha files to DICOM for uploading to MD.ai. You'll need to provide the filepath to the MHA file. Optionally, you can change the plane or default window/level settings. You'll also need a sample dicom file which you can download from here. This method was provided by Ramon Correa, one of our summer 2022 interns.

import os
import pydicom as pyd
from pydicom import uid
from medpy.io import load
import numpy as np
 
def convert_mha_file(
    filepath,
    output_dir='./processed_images',
    plane="axial",
    sample_dicom_fp='./sample_dicom.dcm',
    window_center=400,
    window_width=1000
):
    """Takes path to an mha file converts and saves as series of dicom files.
    filepath: str path to an mha file.
    output_dir:str root directory where images should be stored.
    plane: Acquisition plane  of the original image.
    sample_dicom_fp: Path to dicom image to use as reference
    window_center: Dicom Parameter  used for windowing
    window_width: Dicom Parameter used for windowing
    """
    input_ext = '.mha'
    voxel_arr,header = load(filepath)
 
    pixdim = header.get_voxel_spacing()
 
    # Image coordinates -> World coordinates
    if plane == "axial":
        slice_axis = 2
        plane_axes = [0, 1]
    elif plane == "coronal":
        slice_axis = 1
        plane_axes = [0, 2]
    elif plane == "sagittal":
        slice_axis = 0
        plane_axes = [1, 2]
    thickness = pixdim[slice_axis]
    spacing = [pixdim[plane_axes[1]], pixdim[plane_axes[0]]]
 
    # generate DICOM UIDs (StudyInstanceUID and SeriesInstanceUID)
    study_uid = pyd.uid.generate_uid(prefix=None)
    series_uid = pyd.uid.generate_uid(prefix=None)
    # randomized patient ID
 
    patient_id = str(uid.uuid.uuid4())
    patient_name = patient_id
 
    scale_slope = "1"
    scale_intercept = "0"
    # create base directory
    base_dir =  os.path.join( output_dir,
    os.path.basename(filepath).replace(input_ext,"")
    )
    os.makedirs(base_dir,exist_ok=True)
    for slice_index in range(voxel_arr.shape[-1]):
        # generate SOPInstanceUID
        instance_uid = pyd.uid.generate_uid(prefix=None)
 
        loc = slice_index * thickness
 
        ds = pyd.dcmread(sample_dicom_fp)
 
        # slice and set PixelData tag
        axes = [slice(None)] * 3
        axes[slice_axis] = slice_index
        arr = voxel_arr[:,:,slice_index].T.astype(np.int16)
        ds[0x7fe00010].value = arr.tobytes()
 
        # modify tags
        # using code from original nifti2dcm
        # - UIDs are created by pydicom.uid.generate_uid at each level above
        # - image position is calculated by combination of slice index and slice thickness
        # - slice location is set to the value of image position along z-axis
        # - Rows/Columns determined by array shape
        # - we set slope/intercept to 1/0 since we're directly converting from PNG pixel values
        ds[0x00080018].value = instance_uid  # SOPInstanceUID
        ds[0x00100010].value = patient_name
        ds[0x00100020].value = patient_id
        ds[0x0020000d].value = study_uid  # StudyInstanceUID
        ds[0x0020000e].value = series_uid  # SeriesInstanceUID
        ds[0x0008103e].value = ""  # Series Description
        ds[0x00200011].value = "1"  # Series Number
        ds[0x00200012].value = str(slice_index + 1)  # Acquisition Number
        ds[0x00200013].value = str(slice_index + 1)  # Instance Number
        ds[0x00201041].value = str(loc)  # Slice Location
        ds[0x00280010].value = arr.shape[0]  # Rows
        ds[0x00280011].value = arr.shape[1]  # Columns
        ds[0x00280030].value = spacing  # Pixel Spacing
        ds[0x00281050].value = str(window_center)  # Window Center
        ds[0x00281051].value = str(window_width)  # Window Width
        ds[0x00281052].value = str(scale_intercept)  # Rescale Intercept
        ds[0x00281053].value = str(scale_slope)  # Rescale Slope
        ds.Modality = "MR"
 
        # Image Position (Patient)
        # Image Orientation (Patient)
        if plane == "axial":
            ds[0x00200032].value = ["0", "0", str(loc)]
            ds[0x00200037].value = ["1", "0", "0", "0", "1", "0"]
        elif plane == "coronal":
            ds[0x00200032].value = ["0", str(loc), "0"]
            ds[0x00200037].value = ["1", "0", "0", "0", "0", "1"]
        elif plane == "sagittal":
            ds[0x00200032].value = [str(loc), "0", "0"]
            ds[0x00200037].value = ["0", "1", "0", "0", "0", "1"]
 
        # add new tags
        # see tag info e.g., from https://dicom.innolitics.com/ciods/nm-image/nm-reconstruction/00180050
        # Slice Thickness
        ds[0x00180050] = pyd.dataelem.DataElement(0x00180050, "DS", str(thickness))
        ds.SeriesDescription = f"MR {plane}"
 
        dicom_fp =  os.path.join( output_dir,
        os.path.basename(filepath).replace(input_ext,""),
        "{:03}.dcm".format(slice_index + 1),
        )
        dcm_base,_ = os.path.split(dicom_fp)
        os.makedirs(dcm_base,exist_ok=True)
        pyd.dcmwrite(dicom_fp,ds)

Convert Annotations to DICOM SR

With the util class dicom_utils.SrExport(), all annotations will be converted, capturing the label's and their parent's name in SR format (The specific Code Value of each annotation will be arbitrary). An SR file will be created for each annotator in each annotated study. The SR's DICOM data will be consistent with the study it references. The study and annotation information comes from the inputted "Annotation Json" and "Metadata Json" (These jsons have to be referencing the same dataset(s) for it to work).

The labels will be ordered from exam level to series to image. Each image level label will have a "Referenced Image" section preceding the label to indicate it's source. Series labels will have "Series UID: xxx" under them for better referencing as well.

import mdai
from glob import glob
 
# Define your personal token and project id
personal_token = 'a1s2d3f4g4h5h59797kllh8vk'     # put your token here
project_id = 'LFdpnJGv'
 
# Create an mdai client
mdai_client = mdai.Client(domain='public.md.ai', access_token=personal_token)
 
# Download the annotation data only (all label groups)
p = mdai_client.project(project_id, path='.',  annotations_only=True)
 
# Download only the DICOM metadata
p = mdai_client.download_dicom_metadata(project_id, format ='json', path='.')
 
# Use glob to find the downloaded json files (or get them manually)
annotation_file = glob('*annotations*.json')[0]
metadata_file = glob('*dicom_metadata*.json')[0]
 
# Use the SrExport class to export the annotations to DICOM SR format
 
exporter = mdai.dicom_utils.SrExport(annotation_json=annotation_file, metadata_json=metadata_file, output_dir='out_folder')

With the util class dicom_utils.SegExport(), only local annotations will be exported. This export process converts the annotation data into a binary mask, and creates the relevant segmentation DICOM data to export a DICOM Segmentation file. A Segmentation file will be created for each annotator in each annotated series. Additionally, if combine_label_groups is False, a different file will be created for each label group. The file's DICOM headers will be consistent with the original DICOM's. The study and annotation information comes from the inputted "Annotation Json" and "Metadata Json" (These jsons have to be referencing the same dataset(s) for it to work).

The segmentation frames are grouped together by their labels and within those groups, they are ordered by their source's frame number.

import mdai
from glob import glob
 
# Define your personal token and project id
personal_token = 'a1s2d3f4g4h5h59797kllh8vk'     # put your token here
project_id = 'LFdpnJGv'
 
# Create an mdai client
mdai_client = mdai.Client(domain='public.md.ai', access_token=personal_token)
 
# Download the annotation data only (all label groups)
p = mdai_client.project(project_id, path='.',  annotations_only=True)
 
# Download only the DICOM metadata
p = mdai_client.download_dicom_metadata(project_id, format ='json', path='.')
 
# Use glob to find the downloaded json files (or get them manually)
annotation_file = glob('*annotations*.json')[0]
metadata_file = glob('*dicom_metadata*.json')[0]
 
# Use the SegExport class to export the annotations to DICOM SEG format
 
exporter = mdai.dicom_utils.SegExport(annotation_json=annotation_file, metadata_json=metadata_file, combine_label_groups=True, output_dir='out_folder')

Convert Freeform Annotations to DICOM SEG (Legacy)

Instructions can be found here.

Support functions

Read DICOM UIDs and tags from your original files

Use this code on your original data to create a dataframe of DICOM tags. These are sample tags, you can add or remove values, as needed.

from pathlib import Path
import pydicom
 
images_path = Path('MY_PATH')
filenames = list(images_path.glob('**/*.dcm'))
info = []
 
for f in filenames:
    d = pydicom.dcmread(str(f),stop_before_pixels=True)
    info.append({'fn':str(f),
        'StudyInstanceUID':d.StudyInstanceUID,
        'SeriesInstanceUID':d.SeriesInstanceUID,
        'SOPInstanceUID':d.SOPInstanceUID,
        'description':d.SeriesDescription if 'SeriesDescription' in d else "",
        'name':d.SequenceName if 'SequenceName' in d else "",
        'Modality':d.Modality if 'Modality' in d else "",
        'ContrastAgent':d.ContrastBolusAgent if 'ContrastBolusAgent' in d else "",
        'ScanOptions':d.ScanOptions if 'ScanOptions' in d else "",
        'WW':d.WindowWidth if 'WindowWidth' in d else "",
        'WC':d.WindowCenter if 'WindowCenter' in d else "",
        'ImageType' :d.ImageType if 'ImageType' in d else "",
        'PixelSpacing' :d.PixelSpacing if 'PixelSpacing' in d else "",
        'SliceThickness':d.SliceThickness if 'SliceThickness' in d else "",
        'PhotometricInterpretation':d.PhotometricInterpretation if 'PhotometricInterpretation' in d else ""
                  })
df = pd.DataFrame(info)