|
""" |
|
To |
|
1) upload DICOM (.dcm) data of a patient and |
|
2) perform auto-contouring on it and |
|
3) download the contours |
|
|
|
Tested with Raystation1-B and python3.6 |
|
|
|
Note: The MICCAI2015 dataset only has 1 planning scan/patient |
|
""" |
|
|
|
# Import private libs |
|
import connect |
|
|
|
# Import public libs |
|
import traceback |
|
from pathlib import Path |
|
|
|
############################################################################### |
|
# UTILS # |
|
############################################################################### |
|
|
|
def raystation_setup(): |
|
|
|
try: |
|
patient = connect.get_current("Patient") |
|
patient.Save() # need to do this so to avoid "PreConditionViolationException: State must be saved." |
|
# case = patient.Cases # [0].TreatmentPlans |
|
except: |
|
pass # if there is no patient open |
|
|
|
def vol_to_dicom_for_ct(path_img_ct, patient_name, patient_id, path_dicom): |
|
|
|
""" |
|
Converts a .nrrd/.mha/.nifti file into its .dcm files |
|
|
|
Params |
|
------ |
|
path_img_ct: str, the path of the .nrrd/.mha/.nifti file |
|
patient_name: str |
|
patient_id: str |
|
path_dicom: str, the final output directory |
|
|
|
Note: Verify the output with dciodvfy |
|
- Ref 1: https://www.dclunie.com/dicom3tools/workinprogress/index.html |
|
- Ref 2: https://manpages.debian.org/unstable/dicom3tools/dciodvfy.1.en.html |
|
- Ref 3: # Motivation: https://stackoverflow.com/questions/14350675/create-pydicom-file-from-numpy-array |
|
""" |
|
|
|
study_uid = None |
|
series_uid = None |
|
|
|
try: |
|
|
|
import sys |
|
import copy |
|
import random |
|
import shutil |
|
import subprocess |
|
import numpy as np |
|
|
|
if Path(path_img_ct).exists(): |
|
|
|
try: |
|
import pydicom |
|
import pydicom._storage_sopclass_uids |
|
except: |
|
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--user', 'pydicom']) |
|
import pydicom |
|
|
|
try: |
|
import SimpleITK as sitk |
|
except: |
|
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--user', 'SimpleITK']) # 2.1.1 |
|
import SimpleITK as sitk |
|
|
|
try: |
|
import matplotlib.pyplot as plt |
|
except: |
|
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--user', 'matplotlib']) # 2.1.1 |
|
import matplotlib.pyplot as plt |
|
|
|
# Step 0 - Create save directory |
|
if Path(path_dicom).exists(): |
|
shutil.rmtree(path_dicom) |
|
Path(path_dicom).mkdir(exist_ok=True, parents=True) |
|
|
|
# Step 1 - Get volume params |
|
img_ct = sitk.ReadImage(str(path_img_ct)) |
|
img_spacing = tuple(img_ct.GetSpacing()) |
|
img_origin = tuple(img_ct.GetOrigin()) # --> dicom.ImagePositionPatient |
|
img_array = sitk.GetArrayFromImage(img_ct).astype(np.int16) # [D,H,W] |
|
|
|
# Step 2 - Create dicom dataset |
|
ds = pydicom.dataset.Dataset() |
|
ds.FrameOfReferenceUID = pydicom.uid.generate_uid() # this will stay the same for all .dcm files of a volume |
|
|
|
# Step 2.1 - Modality details |
|
ds.SOPClassUID = pydicom._storage_sopclass_uids.CTImageStorage |
|
ds.Modality = 'CT' |
|
ds.ImageType = ['ORIGINAL', 'PRIMARY', 'AXIAL'] |
|
|
|
# Step 2.2 - Image Details |
|
ds.PixelSpacing = [float(img_spacing[0]), float(img_spacing[1])] |
|
ds.SliceThickness = str(img_spacing[-1]) |
|
ds.Rows = img_array.shape[1] |
|
ds.Columns = img_array.shape[2] |
|
|
|
ds.PatientPosition = 'HFS' |
|
ds.ImageOrientationPatient = [1, 0, 0, 0, 1, 0] |
|
ds.PositionReferenceIndicator = 'SN' |
|
|
|
ds.SamplesPerPixel = 1 |
|
ds.PhotometricInterpretation = 'MONOCHROME2' |
|
ds.BitsAllocated = 16 |
|
ds.BitsStored = 16 |
|
ds.HighBit = 15 |
|
ds.PixelRepresentation = 1 |
|
|
|
ds.RescaleIntercept = "0.0" |
|
ds.RescaleSlope = "1.0" |
|
ds.RescaleType = 'HU' |
|
|
|
# Step 3.1 - Metadata |
|
fileMeta = pydicom.Dataset() |
|
fileMeta.MediaStorageSOPClassUID = pydicom._storage_sopclass_uids.CTImageStorage |
|
fileMeta.MediaStorageSOPInstanceUID = pydicom.uid.generate_uid() # this will change for each .dcm file of a volume |
|
fileMeta.TransferSyntaxUID = pydicom.uid.ExplicitVRLittleEndian |
|
ds.file_meta = fileMeta |
|
|
|
# Step 3.2 - Include study details |
|
ds.StudyInstanceUID = pydicom.uid.generate_uid() |
|
ds.StudyDescription = '' |
|
ds.StudyDate = '19000101' # needed to create DICOMDIR |
|
ds.StudyID = str(random.randint(0,1000)) # needed to create DICOMDIR |
|
|
|
# Step 3.3 - Include series details |
|
ds.SeriesInstanceUID = pydicom.uid.generate_uid() |
|
ds.SeriesDescription = '' |
|
ds.SeriesNumber = str(random.randint(0,1000)) # needed to create DICOMDIR |
|
|
|
# Step 3.4 - Include patient details |
|
ds.PatientName = patient_name |
|
ds.PatientID = patient_id |
|
|
|
# Step 3.5 - Manufacturer details |
|
ds.Manufacturer = 'MICCAI2015' |
|
ds.ReferringPhysicianName = 'Mody' # needed for identification in RayStation |
|
ds.ManufacturerModelName = 'test_offsite' |
|
|
|
# Step 4 - Make slices |
|
for slice_id in range(img_array.shape[0]): |
|
|
|
# Step 4.1 - Slice identifier |
|
random_uuid = pydicom.uid.generate_uid() |
|
ds.file_meta.MediaStorageSOPInstanceUID = random_uuid |
|
ds.SOPInstanceUID = random_uuid |
|
ds.InstanceNumber = str(slice_id+1) |
|
|
|
vol_origin_tmp = list(copy.deepcopy(img_origin)) |
|
vol_origin_tmp[-1] += img_spacing[-1]*slice_id |
|
ds.ImagePositionPatient = vol_origin_tmp |
|
|
|
# Step 4.2 - Slice data |
|
img_slice = img_array[slice_id,:,:] |
|
# plt.imshow(img_slice); plt.savefig(str(Path(path_dicom, '{}.png'.format(slice_id)))); plt.close() |
|
ds.PixelData = img_slice.tobytes() |
|
|
|
save_path = Path(path_dicom).joinpath(str(ds.file_meta.MediaStorageSOPInstanceUID) + '.dcm') |
|
ds.save_as(str(save_path), write_like_original=False) |
|
|
|
study_uid = ds.StudyInstanceUID |
|
series_uid = ds.SeriesInstanceUID |
|
|
|
else: |
|
print (' - [ERROR][vol_to_dicom_for_ct()] Error in path: path_img_ct: ', path_img_ct) |
|
|
|
except: |
|
traceback.print_exc() |
|
|
|
return study_uid, series_uid |
|
|
|
def raystation_upload_predict_download(path_dcm_patient_ct, patient_id, study_uid, series_uid, ): |
|
|
|
""" |
|
Params |
|
------ |
|
path_dcm_patient_ct: Path to folder contains CT dicoms |
|
""" |
|
|
|
try: |
|
|
|
# Step 1 - Setup |
|
db = connect.get_current('PatientDB') |
|
|
|
# Step 2 - Upload data |
|
if Path(path_dcm_patient_ct).exists(): |
|
# Note: If the patient exists, this will still upload it and name it with a suffix |
|
warnings = db.ImportPatientFromPath(Path=str(path_dcm_patient_ct), SeriesOrInstances=[{'PatientID': patient_id, 'StudyInstanceUID': str(study_uid), 'SeriesInstanceUID': str(series_uid)}], ImportFilter='', BrachyPlanImportOverrides={}) |
|
patient = connect.get_current("Patient") |
|
patient.Save() |
|
|
|
# Step 3 - Perform auto-contouring |
|
examination = connect.get_current('Examination') |
|
examination.RunOarSegmentation(ModelName="RSL Head and Neck CT", ExaminationsAndRegistrations={ 'CT 1': None }, RoisToInclude=["Brainstem", "Bone_Mandible", "OpticNrv_L", "OpticNrv_R", "Parotid_L", "Parotid_R", "Glnd_Submand_L", "Glnd_Submand_R", "Joint_TM_L", "Joint_TM_R"]) |
|
|
|
# Step 4 - Download data |
|
patient.Save() |
|
case = connect.get_current('Case') |
|
examination = connect.get_current('Examination') |
|
|
|
path_dcm_patient_rs = str(path_dcm_patient_ct) + '-RSAutoContour' |
|
Path(path_dcm_patient_rs).mkdir(exist_ok=True) |
|
case.ScriptableDicomExport(ExportFolderPath = str(path_dcm_patient_rs), AnonymizationSettings={"Anonymize": False}, RtStructureSetsForExaminations = [examination.Name], IgnorePreConditionWarnings=False) # , Examinations = [examination.Name] |
|
|
|
else: |
|
print (' - [ERROR][raystation_upload_predict_download()] Issues with path: path_dcm_patient_ct: ', path_dcm_patient_ct) |
|
|
|
except: |
|
traceback.print_exc() |
|
|
|
def download_purepython_package(url_release, folderpath_package): |
|
""" |
|
We can directly download and use this since it is a pure python package |
|
Defunct in RS as you just pip install using sys.executable |
|
""" |
|
import importlib |
|
import urllib |
|
import zipfile |
|
import urllib.request |
|
|
|
def download_zip(url_zip, filepath_zip, filepath_output): |
|
urllib.request.urlretrieve(url_zip, filename=filepath_zip) |
|
read_zip(filepath_zip, filepath_output) |
|
|
|
def read_zip(filepath_zip, filepath_output=None, leave=False): |
|
|
|
# Step 0 - Init |
|
if Path(filepath_zip).exists(): |
|
if filepath_output is None: |
|
filepath_zip_parts = list(Path(filepath_zip).parts) |
|
filepath_zip_name = filepath_zip_parts[-1].split('.zip')[0] |
|
filepath_zip_parts[-1] = filepath_zip_name |
|
filepath_output = Path(*filepath_zip_parts) |
|
|
|
zip_fp = zipfile.ZipFile(filepath_zip, 'r') |
|
zip_fp_members = zip_fp.namelist() |
|
for member in zip_fp_members: |
|
zip_fp.extract(member, filepath_output) |
|
|
|
return filepath_output |
|
else: |
|
print (' - [ERROR][read_zip()] Path does not exist: ', filepath_zip) |
|
return None |
|
|
|
|
|
# Step 0.1 - Init |
|
module_name = Path(folderpath_package).parts[-1] |
|
filepath_zip = str(folderpath_package) + '.zip' |
|
folderpath_output = str(folderpath_package) + '-download' |
|
|
|
# Step 0.2 - Clear previous content |
|
if Path(filepath_zip).exists(): |
|
Path(filepath_zip).unlink() |
|
if Path(folderpath_output).exists(): |
|
shutil.rmtree(str(folderpath_output)) |
|
if Path(folderpath_package).exists(): |
|
shutil.rmtree(str(folderpath_package)) |
|
|
|
# Step 1 - Download release |
|
download_zip(url_release, filepath_zip, folderpath_output) |
|
Path(filepath_zip).unlink() |
|
|
|
# Step 2 - PMove around stuff |
|
folderpath_tmp = [path for path in Path(folderpath_output).iterdir()][0] |
|
src = str(Path(folderpath_tmp, module_name)) |
|
dst = str(Path(folderpath_package)) |
|
shutil.copytree(src, dst) |
|
shutil.rmtree(str(folderpath_output)) |
|
|
|
sys.path.append(str(Path(folderpath_output).parent.absolute())) |
|
importlib.import_module(module_name) |
|
|
|
# url_release_pydicom = 'https://github.com/pydicom/pydicom/archive/refs/tags/v2.3.1.zip' |
|
# folderpath_package_pydicom = Path(DIR_FILE).joinpath('pydicom') |
|
# download_purepython_package(url_release_pydicom, folderpath_package_pydicom) |
|
# import pydicom |
|
pass |
|
|
|
############################################################################### |
|
# MAIN # |
|
############################################################################### |
|
|
|
if __name__ == "__main__": |
|
""" |
|
Implement your custom dataset loop here! |
|
""" |
|
raystation_setup() |
|
|
|
if 1: |
|
|
|
print ('\n ======================================================== ') |
|
print ('= MICCAI 2015 (test_offsite) =') |
|
print (' ======================================================== \n') |
|
|
|
# Step 0 - Init |
|
DIR_FILE = Path(__file__).parent.absolute() |
|
DIR_DATA_MICOFFSITE = Path('H:\\').joinpath('RayStationData', 'MICCAI2015', 'test_offsite') |
|
assert Path(DIR_DATA_MICOFFSITE).exists() == True |
|
|
|
# Step 1 - Loop over patients |
|
for patient_count, path_patient in enumerate(Path(DIR_DATA_MICOFFSITE).iterdir()): |
|
|
|
try: |
|
patient_name = Path(path_patient).parts[-1] |
|
patient_id = 'MICCAI2015-Test-' + patient_name |
|
|
|
print ('\n\n ----------------------------------------- Patient: ', patient_id) |
|
path_img = Path(path_patient).joinpath('img_resampled_{}.nrrd'.format(patient_name)) |
|
path_img_dicom = Path(path_patient).joinpath('img_resampled_{}_dcm'.format(patient_name)) |
|
|
|
# Step 2 - Dicomize |
|
study_uid, series_uid = vol_to_dicom_for_ct(path_img_ct=path_img, patient_name=patient_name, patient_id=patient_id, path_dicom=path_img_dicom) |
|
|
|
# Step 3 - Predict OARs |
|
if study_uid is not None and series_uid is not None: |
|
raystation_upload_predict_download(path_img_dicom, patient_id=patient_id, study_uid=study_uid, series_uid=series_uid) |
|
|
|
# break |
|
# if patient_count > 2: |
|
# break |
|
|
|
except: |
|
traceback.print_exc() |