|
a |
|
b/data_conversion/dicom_to_nifti.py |
|
|
1 |
''' |
|
|
2 |
Copyright (c) Microsoft Corporation. All rights reserved. |
|
|
3 |
Licensed under the MIT License. |
|
|
4 |
''' |
|
|
5 |
|
|
|
6 |
''' |
|
|
7 |
This code does the following: |
|
|
8 |
(1) converts PET DICOM images in units Bq/ml to decay-corrected SUV and saved as 3D NIFTI files |
|
|
9 |
(2) converts CT DICOM images to NIFTI |
|
|
10 |
(3) converts DICOM RTSTRUCT images to NIFTI (using rt-utils) |
|
|
11 |
''' |
|
|
12 |
#%% |
|
|
13 |
import SimpleITK as sitk |
|
|
14 |
from pydicom import dcmread, FileDataset |
|
|
15 |
from rt_utils import RTStructBuilder, RTStruct |
|
|
16 |
import numpy as np |
|
|
17 |
import dateutil |
|
|
18 |
import pandas as pd |
|
|
19 |
import os |
|
|
20 |
import time |
|
|
21 |
|
|
|
22 |
#%% |
|
|
23 |
''' |
|
|
24 |
Script to convert PET and CT dicom series to niftii files. Works under |
|
|
25 |
the assumption that the rescale slope and intercept in the PET dicom |
|
|
26 |
series map image intensities to Bq/mL. Saved PET files will have image |
|
|
27 |
intensities of SUVbw, and saved CT files will have HU units. |
|
|
28 |
|
|
|
29 |
''' |
|
|
30 |
def bqml_to_suv(dcm_file: FileDataset) -> float: |
|
|
31 |
|
|
|
32 |
# Calculates the SUV conversion factor from Bq/mL to SUVbw using |
|
|
33 |
# the dicom header information in one of the images from a dicom series |
|
|
34 |
|
|
|
35 |
nuclide_dose = dcm_file[0x054, 0x0016][0][0x0018, 0x1074].value # Total injected dose (Bq) |
|
|
36 |
weight = dcm_file[0x0010, 0x1030].value # Patient weight (Kg) |
|
|
37 |
half_life = float(dcm_file[0x054, 0x0016][0][0x0018, 0x1075].value) # Radionuclide half life (s) |
|
|
38 |
|
|
|
39 |
parse = lambda x: dateutil.parser.parse(x) |
|
|
40 |
|
|
|
41 |
series_time = str(dcm_file[0x0008, 0x00031].value) # Series start time (hh:mm:ss) |
|
|
42 |
series_date = str(dcm_file[0x0008, 0x00021].value) # Series start date (yyy:mm:dd) |
|
|
43 |
series_datetime_str = series_date + ' ' + series_time |
|
|
44 |
series_dt = parse(series_datetime_str) |
|
|
45 |
|
|
|
46 |
nuclide_time = str(dcm_file[0x054, 0x0016][0][0x0018, 0x1072].value) # Radionuclide time of injection (hh:mm:ss) |
|
|
47 |
nuclide_datetime_str = series_date + ' ' + nuclide_time |
|
|
48 |
nuclide_dt = parse(nuclide_datetime_str) |
|
|
49 |
|
|
|
50 |
delta_time = (series_dt - nuclide_dt).total_seconds() |
|
|
51 |
decay_correction = 2 ** (-1 * delta_time/half_life) |
|
|
52 |
suv_factor = (weight * 1000) / (decay_correction * nuclide_dose) |
|
|
53 |
|
|
|
54 |
return(suv_factor) |
|
|
55 |
|
|
|
56 |
def get_filtered_roi_list(rois): |
|
|
57 |
filtered_rois = [] |
|
|
58 |
for roi in rois: |
|
|
59 |
if roi.endswith('PETEdge'): |
|
|
60 |
filtered_rois.append(roi) |
|
|
61 |
else: |
|
|
62 |
pass |
|
|
63 |
return filtered_rois |
|
|
64 |
|
|
|
65 |
|
|
|
66 |
def load_merge_masks(rtstruct: RTStruct) -> np.ndarray: |
|
|
67 |
''' |
|
|
68 |
Load and merge masks from a dicom RTStruct. All of the |
|
|
69 |
masks in the RTStruct will be merged. Add an extra line |
|
|
70 |
of code if you want to filter for/out certain masks. |
|
|
71 |
''' |
|
|
72 |
rois = rtstruct.get_roi_names() |
|
|
73 |
rois = get_filtered_roi_list(rois) |
|
|
74 |
masks = [] |
|
|
75 |
for roi in rois: |
|
|
76 |
print(roi) |
|
|
77 |
mask_3d = rtstruct.get_roi_mask_by_name(roi).astype(int) |
|
|
78 |
masks.append(mask_3d) |
|
|
79 |
|
|
|
80 |
final_mask = sum(masks) # sums element-wise |
|
|
81 |
final_mask = np.where(final_mask>=1, 1, 0) |
|
|
82 |
# Reorient the mask to line up with the reference image |
|
|
83 |
final_mask = np.moveaxis(final_mask, [0, 1, 2], [1, 2, 0]) |
|
|
84 |
|
|
|
85 |
return final_mask |
|
|
86 |
|
|
|
87 |
############################################################################################ |
|
|
88 |
######## Update the three variables below with the locations of your choice ############## |
|
|
89 |
############################################################################################ |
|
|
90 |
save_dir_ct = '' # path to directory where your new CT files in NIFTI format will be written |
|
|
91 |
save_dir_pt = '' # path to directory where your new PET files in NIFTI format will be written |
|
|
92 |
save_dir_gt = '' # path to directory where your new GT files in NIFTI format will be written |
|
|
93 |
############################################################################################ |
|
|
94 |
############################################################################################ |
|
|
95 |
############################################################################################ |
|
|
96 |
|
|
|
97 |
cases = pd.read_csv('dicom_ctpt_to_nifti_conversion_file.csv') |
|
|
98 |
cases = list(cases.itertuples(index=False, name=None)) |
|
|
99 |
structs = pd.read_csv('dicom_rtstruct_to_nifti_conversion_file.csv') |
|
|
100 |
structs = list(structs.itertuples(index=False, name=None)) |
|
|
101 |
# Execution |
|
|
102 |
start = time.time() |
|
|
103 |
|
|
|
104 |
for case in cases: |
|
|
105 |
patient_id, ct_folder, pet_folder, convert = case |
|
|
106 |
if convert=='N': |
|
|
107 |
continue |
|
|
108 |
print(f'Converting patient Id: {patient_id}') |
|
|
109 |
|
|
|
110 |
# Convert CT series |
|
|
111 |
ct_reader = sitk.ImageSeriesReader() |
|
|
112 |
ct_series_names = ct_reader.GetGDCMSeriesFileNames(ct_folder) |
|
|
113 |
ct_reader.SetFileNames(ct_series_names) |
|
|
114 |
ct = ct_reader.Execute() |
|
|
115 |
sitk.WriteImage(ct, os.path.join(save_dir_ct, f"{patient_id}_0000.nii.gz"), imageIO='NiftiImageIO') |
|
|
116 |
print('Saved nifti CT') |
|
|
117 |
|
|
|
118 |
# Convert PET series |
|
|
119 |
pet_reader = sitk.ImageSeriesReader() |
|
|
120 |
pet_series_names = pet_reader.GetGDCMSeriesFileNames(pet_folder) |
|
|
121 |
pet_reader.SetFileNames(pet_series_names) |
|
|
122 |
pet = pet_reader.Execute() |
|
|
123 |
|
|
|
124 |
pet_img = dcmread(pet_series_names[0]) # read one of the images for header info |
|
|
125 |
suv_factor = bqml_to_suv(pet_img) |
|
|
126 |
pet = sitk.Multiply(pet, suv_factor) |
|
|
127 |
sitk.WriteImage(pet, os.path.join(save_dir_pt, f"{patient_id}_0001.nii.gz"), imageIO='NiftiImageIO') |
|
|
128 |
print('Saved nifti PET') |
|
|
129 |
|
|
|
130 |
# Execution |
|
|
131 |
for struct in structs: |
|
|
132 |
patient_id, struct_folder, ref_folder, convert = struct |
|
|
133 |
if convert=='N': |
|
|
134 |
continue |
|
|
135 |
|
|
|
136 |
# print('Converting RTStruct for patient {}'.format(num)) |
|
|
137 |
# Get all the paths in order |
|
|
138 |
struct_file = os.listdir(struct_folder)[0] |
|
|
139 |
struct_path = os.path.join(struct_folder, struct_file) |
|
|
140 |
|
|
|
141 |
# Create the mask |
|
|
142 |
rtstruct = RTStructBuilder.create_from(dicom_series_path= ref_folder, rt_struct_path=struct_path) |
|
|
143 |
final_mask = load_merge_masks(rtstruct) |
|
|
144 |
|
|
|
145 |
# Load original DICOM image for reference |
|
|
146 |
reader = sitk.ImageSeriesReader() |
|
|
147 |
dicom_names = reader.GetGDCMSeriesFileNames(ref_folder) |
|
|
148 |
reader.SetFileNames(dicom_names) |
|
|
149 |
ref_img = reader.Execute() |
|
|
150 |
|
|
|
151 |
# Properly reference and convert the mask to an image object |
|
|
152 |
mask_img = sitk.GetImageFromArray(final_mask) |
|
|
153 |
mask_img.CopyInformation(ref_img) |
|
|
154 |
sitk.WriteImage(mask_img, os.path.join(save_dir_gt, f"{patient_id}.nii.gz"), imageIO="NiftiImageIO") |
|
|
155 |
|
|
|
156 |
print('Patient {} mask saved'.format(patient_id)) |