Skip to content

Instantly share code, notes, and snippets.

@prerakmody
Last active January 11, 2023 18:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save prerakmody/e5bfee797a327a99f48e558020e58288 to your computer and use it in GitHub Desktop.
Save prerakmody/e5bfee797a327a99f48e558020e58288 to your computer and use it in GitHub Desktop.
RayStation 10B - Upload .dcm (from disk), auto-contour (in RS) and download .dcm (to disk and not in PACS)
"""
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()

Step I - To open RayStation

Some steps are specific to my work environment

  • Launch VMWare Horizon Client
    • Server: <>
  • Enter username: <>, password: <>, Domain: <>
  • Launch RayStation 10B Research and then RayStation Planning

Step II - Scripting

  • In the left tab, click on the Scripting tab, then click on Script creation
  • Select a Python interpreter: <>
  • Documentation/References can be found in
  • For debugging, one can also use the Run Console script
    • To check which packages exist in your environment via the command line use pkg_resources
  • Code for uploading, auto-contouring and downloading
    """
    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
    """
    
    # Import private libs
    import connect
    db      = connect.get_current('PatientDB')
    patient = connect.get_current("Patient")
    patient.Save() # need to do this so to avoid "PreConditionViolationException: State must be saved."
    
    # Import public libs
    import sys
    import traceback
    from pathlib import Path
    
    
    # Step 0 - Init
    DIR_DATA = Path('H:\\').joinpath('RayStationData')
    DIR_MICCAI2015 = Path(DIR_DATA).joinpath('MICCAI2015')
    
    if Path(DIR_MICCAI2015).exists():
    
      # Step 1 - Loop and upload each patient
      for path_patient in Path(DIR_MICCAI2015).iterdir():
        print (' - path_patient: ', path_patient)
        patient_foldername = Path(path_patient).parts[-1]
        if patient_foldername != '0522c0555-v2':
          continue
    
        try:
          print ('   - Importing {} ... '.format(patient_foldername))
          warnings = db.ImportPatientFromPath(Path=str(path_patient), SeriesOrInstances=[{'PatientID': '0522c0555', 'StudyInstanceUID': '1.2.826.0.1.3680043.2.1125.1.61298505395097488367431771696839433', 'SeriesInstanceUID': '1.2.826.0.1.3680043.2.1125.1.57994303646388285925672833263663467'}], ImportFilter='', BrachyPlanImportOverrides={})
          patient = connect.get_current("Patient")
          patient.Save()
    
          # Step 2 - 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 3 - Download data
          pass
    
        except:
          print (' \n ----------- ERROR ----------- ')
          traceback.print_exc()
          print ('  ----------- ERROR ----------- \n')
    
    else:
      print (' - [ERROR] Path issue: ', DIR_MICCAI2015)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment