├── .gitignore ├── README.md ├── TODO.md ├── __init__.py ├── build_dicom.py ├── builders.py ├── coordinates.py ├── doseData.py ├── example.ps1 ├── generate_import_test_data.py ├── modules.py ├── orientationtests.py ├── plotting.py ├── roiconform.py ├── studybuilderexample.py ├── test.sh ├── test_coordinates.py └── viewer ├── __init__.py ├── mlc.py └── treeview.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | # Run results 30 | *.dcm 31 | 32 | # Emacs temp files 33 | *~ 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dicomutils 2 | ========== 3 | 4 | A set of utilities for working with DICOM files. 5 | 6 | The main utility is currently build_dicom, which can generate simple synthetic CT data, MR data, PET data, 7 | RT Structure sets, RT Doses and RT Plans. 8 | 9 | All output files will be placed in the current working directory, and named as `_.dcm`, e.g. `CT_2.25.119389864082697057857042902898482259876.84.dcm`. 10 | 11 | Examples 12 | ======== 13 | 14 | Get help: 15 | ```bash 16 | $ ./build_dicom.py --help 17 | ``` 18 | 19 | Generate a 50cm x 50cm x 50cm water phantom CT data with 5mm resolution and a RT Structure set with a box ROI: 20 | 21 | ```bash 22 | $ ./build_dicom.py \ 23 | --patient-position HFS --values 0 --voxelsize 5,5,5 --voxels 100,100,100 --modality CT \ 24 | --structure external --modality RTSTRUCT 25 | ``` 26 | 27 | ![Screenshot of 50x50x50 water phantom with outline](https://github.com/raysearchlabs/dicomutils/wiki/simplebox.png) 28 | 29 | Generate CT data with two cavities (one denser), rois covering them, a box outline, an arbitrary plan 30 | and a lightfield "dose": 31 | 32 | ```bash 33 | $ ./build_dicom.py \ 34 | --patient-position HFS --values 0 \ 35 | --values "sphere,-100,25,[50;86.6;0]" --values "box,100,25,[50;-86.6;0]" \ 36 | --voxelsize 4,3,4 --voxels 48,64,48 --modality CT \ 37 | --structure external \ 38 | --structure "sphere,Ball,25,CAVITY,[50;86.6;0]" \ 39 | --structure "box,Cube,25,CAVITY,[50;-86.6;0]" --modality RTSTRUCT \ 40 | --beams "[3;123;270]" \ 41 | --mlc-shape "1,circle,30" --jaw-shape "1,[60;60]" \ 42 | --mlc-shape "2,rectangle,60,60" --jaw-shape "2,[70;70;10;10]" \ 43 | --mlc-shape "3,rectangle,40,80" --jaw-shape "3,[40;80]" \ 44 | --nominal-energy 6 --modality RTPLAN \ 45 | --values 0 --values lightfield --modality RTDOSE 46 | ``` 47 | 48 | ![Screenshot of plan with lightfield dose](https://raw.github.com/wiki/raysearchlabs/dicomutils/lightfieldplan.png) 49 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | In rough order of priority: 2 | 3 | * Use BeamMU in lightfield 4 | 5 | * Reading parts of the study, e.g. reading CT and creating an RTSTRUCT on the CT, or reading CT & RTSTRUCT and creating RTPLAN, or reading CT, RTSTRUCT and RTPLAN and writing lightfield RTDOSE. 6 | 7 | * Electrons modality - would probably mostly be supporting blocks and setting radiation type 8 | 9 | * MR support 10 | 11 | * PET support 12 | 13 | * Wedges 14 | 15 | * RT Ion Plans 16 | 17 | * RT Images? 18 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raysearchlabs/dicomutils/c4811340625ae41a6b0d187e81580ff322b60d5d/__init__.py -------------------------------------------------------------------------------- /build_dicom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import dicom 6 | import time 7 | import uuid 8 | import sys 9 | import datetime 10 | import os 11 | import coordinates 12 | from builders import StudyBuilder, TableTop, TableTopEcc 13 | # Be careful to pass good fp numbers... 14 | if hasattr(dicom, 'config'): 15 | dicom.config.allow_DS_float = True 16 | 17 | import argparse 18 | 19 | 20 | class ModalityGroupAction(argparse.Action): 21 | def __call__(self, parser, namespace, values, option_string=None): 22 | ns = namespace.__dict__.copy() 23 | ns.pop('studies') 24 | ns['modality'] = values 25 | namespace.studies[-1].append(argparse.Namespace(**ns)) 26 | 27 | 28 | class NewStudyAction(argparse.Action): 29 | def __call__(self, parser, namespace, values, option_string=None): 30 | namespace.studies.append([]) 31 | 32 | pixel_representations = {'signed': 1, 'unsigned' : 0} 33 | 34 | parser = argparse.ArgumentParser(description='Create DICOM data.') 35 | parser.add_argument('--patient-position', dest='patient_position', 36 | choices=['HFS', 'HFP', 'FFS', 'FFP', 'HFDR', 'HFDL', 'FFDR', 'FFDL'], 37 | help='The patient position written in the images. Required for CT, MR and PT. ' 38 | '(default: not specified)') 39 | parser.add_argument('--patient-id', dest='patient_id', default='Patient ID', 40 | help='The patient ID.') 41 | parser.add_argument('--patients-name', dest='patient_name', default='LastName^GivenName^^^', 42 | help="The patient's name, in DICOM caret notation.") 43 | parser.add_argument('--patients-birthdate', dest='patient_birthdate', default='', 44 | help="The patient's birthdate, in DICOM DA notation (YYYYMMDD).") 45 | parser.add_argument('--voxelsize', dest='VoxelSize', default="1,2,4", 46 | help='The size of a single voxel in mm. (default: 1,2,4)') 47 | parser.add_argument('--voxels', dest='NumVoxels', default="64,32,16", 48 | help='The number of voxels in the dataset. (default: 64,32,16)') 49 | parser.add_argument('--modality', dest='modality', default=[], 50 | choices=["CT", "MR", "PT", "RTDOSE", "RTPLAN", "RTSTRUCT"], 51 | help='The modality to write. (default: CT)', action=ModalityGroupAction) 52 | parser.add_argument('--nominal-energy', dest='nominal_energy', default=None, 53 | help='The nominal energy of beams in an RT Plan.') 54 | parser.add_argument('--values', dest='values', default=[], action='append', metavar='VALUE | SHAPE{,PARAMETERS}', 55 | help="""Set the Hounsfield or dose values in a volume to the given value.\n\n\n 56 | For syntax, see the forthcoming documentation or the source code...""") 57 | parser.add_argument('--pixel-representation', dest='pixel_representation', default='signed', 58 | choices=pixel_representations, 59 | help='signed: Stored pixel value type is int16, unsigned: Stored pixel value type is uint16.') 60 | parser.add_argument('--rescale-slope', dest='rescale_slope', type=float, default=1.0, 61 | help="""Set the rescale slope (defaults - CT: 1.0, PET: 1.0).""") 62 | parser.add_argument('--rescale-intercept', dest='rescale_intercept', type=float, default=-1024.0, 63 | help="""Set the rescale intercept (defaults - CT: -1024.0).""") 64 | parser.add_argument('--center', dest='center', default="[0;0;0]", 65 | help="""Center of the image, in dicom patient coordinates.""") 66 | parser.add_argument('--sad', dest='sad', default=1000, help="The Source to Axis distance.") 67 | parser.add_argument('--structure', dest='structures', default=[], action='append', 68 | metavar='SHAPE{,PARAMETERS}', 69 | help="""Add a structure to the current list of structure sets. 70 | For syntax, see the forthcoming documentation or the source code...""") 71 | parser.add_argument('--beams', dest='beams', default='3', 72 | help="""Set the number of equidistant beams to write in an RTPLAN.""") 73 | parser.add_argument('--meterset', dest='meterset', default='1.0', 74 | help="""Set the beam meterset weight, either as a single number for all beams 75 | or as [1;2;3;...] to specify the meterset weight for all beams in an RTPLAN.""") 76 | parser.add_argument('--collimator-angles', dest='collimator_angles', default='0', 77 | help="""Set the collimator angle (Beam Limiting Device Angle) of the beams. 78 | In IEC61217 terminology, that corresponds to the theta_b angle.""") 79 | parser.add_argument('--patient-support-angles', dest='patient_support_angles', default='0', 80 | help="""Set the Patient Support Angle ("couch angle") of the beams. 81 | In IEC61217 terminology, that corresponds to the theta_s angle.""") 82 | parser.add_argument('--table-top', dest='table_top', default='0,0,0,0,0', 83 | help="""Set the table top pitch, roll and lateral, longitudinal and vertical positions. 84 | In IEC61217 terminology, that corresponds to the 85 | psi_t, phi_t, Tx, Ty, Tz coordinates, respectively.""") 86 | parser.add_argument('--table-top-eccentric', dest='table_top_eccentric', default='0,0', 87 | help="""Set the table top eccentric axis distance and angle. 88 | In IEC61217 terminology, that corresponds to the Ls and theta_e coordinates, respectively.""") 89 | parser.add_argument('--isocenter', dest='isocenter', default='[0;0;0]', 90 | help="""Set the isocenter of the beams.""") 91 | parser.add_argument('--mlc-direction', dest='mlc_direction', default='MLCX', 92 | help="""Set the direction of the MLC - MLCX or MLCY.""") 93 | parser.add_argument('--mlc-shape', dest='mlcshapes', default=[], action='append', 94 | help="""Add an opening to the current list of mlc openings. 95 | For syntax, see the forthcoming documentation or the source code...""") 96 | parser.add_argument('--jaw-shape', dest='jawshapes', default=[], action='append', 97 | help="""Sets the jaw shape to x * y, centered at (xc, yc). 98 | Given as [x;y;xc;yc]. Defaults to conforming to the MLC.""") 99 | parser.add_argument('--outdir', dest='outdir', default='.', 100 | help="""Generate data to this directory. (default: working directory)""") 101 | args = parser.parse_args(namespace=argparse.Namespace(studies=[[]])) 102 | 103 | voxel_size = [float(x) for x in args.VoxelSize.split(",")] 104 | num_voxels = [int(x) for x in args.NumVoxels.split(",")] 105 | 106 | if not os.path.exists(args.outdir): 107 | os.makedirs(args.outdir) 108 | for study in args.studies: 109 | sb = StudyBuilder() 110 | for series in study: 111 | if series.center.__class__ is str: 112 | series.center = [float(b) for b in series.center.lstrip('[').rstrip(']').split(";")] 113 | if series.modality == "CT": 114 | if 'PatientPosition' not in sb.current_study: 115 | parser.error("Patient position must be specified when writing CT images!") 116 | 117 | ib = sb.build_ct( 118 | num_voxels=num_voxels, 119 | voxel_size=voxel_size, 120 | pixel_representation=pixel_representations[series.pixel_representation], 121 | rescale_slope=series.rescale_slope, 122 | rescale_intercept=series.rescale_intercept, 123 | center=np.array(series.center)) 124 | elif series.modality == "MR": 125 | if 'PatientPosition' not in sb.current_study: 126 | parser.error("Patient position must be specified when writing MR images!") 127 | 128 | ib = sb.build_mr( 129 | num_voxels=num_voxels, 130 | voxel_size=voxel_size, 131 | pixel_representation=pixel_representations[series.pixel_representation], 132 | center=np.array(series.center)) 133 | elif series.modality == "PT": 134 | if 'PatientPosition' not in sb.current_study: 135 | parser.error("Patient position must be specified when writing MR images!") 136 | 137 | ib = sb.build_pt( 138 | num_voxels=num_voxels, 139 | voxel_size=voxel_size, 140 | pixel_representation=pixel_representations[series.pixel_representation], 141 | rescale_slope=series.rescale_slope, 142 | center=np.array(series.center)) 143 | elif series.modality == "RTDOSE": 144 | ib = sb.build_dose( 145 | num_voxels=num_voxels, 146 | voxel_size=voxel_size, 147 | center=np.array(series.center)) 148 | elif series.modality == "RTPLAN": 149 | isocenter = [float(b) for b in series.isocenter.lstrip('[').rstrip(']').split(";")] 150 | rp = sb.build_static_plan(nominal_beam_energy=series.nominal_energy, 151 | isocenter=isocenter, 152 | mlc_direction=series.mlc_direction, 153 | sad=series.sad) 154 | elif series.modality == "RTSTRUCT": 155 | rtstruct = sb.build_structure_set() 156 | else: 157 | assert "Unknown modality" 158 | 159 | for value in series.values: 160 | value = value.split(",") 161 | if len(value) == 1 and (value[0][0].isdigit() or value[0][0] == '-'): 162 | ib.clear(float(value[0])) 163 | else: 164 | shape = value[0] 165 | if shape == "sphere": 166 | val = float(value[1]) 167 | radius = float(value[2]) 168 | if len(value) > 3: 169 | center = [float(c) for c in value[3].lstrip('[').rstrip(']').split(";")] 170 | else: 171 | center = [0, 0, 0] 172 | ib.add_sphere(radius=radius, center=center, stored_value=val, mode='set') 173 | elif shape == "box": 174 | val = float(value[1]) 175 | size = value[2] 176 | if size.startswith("[") and size.endswith("]"): 177 | size = [float(c) for c in size.lstrip('[').rstrip(']').split(";")] 178 | else: 179 | size = [float(size), float(size), float(size)] 180 | if len(value) > 3: 181 | center = [float(c) for c in value[3].lstrip('[').rstrip(']').split(";")] 182 | else: 183 | center = [0, 0, 0] 184 | ib.add_box(size=size, center=center, stored_value=val, mode='set') 185 | elif shape == "lightfield": 186 | for beam in sb.seriesbuilders['RTPLAN'][-1].beam_builders: 187 | ib.add_lightfield(beam.rtbeam, beam.meterset) 188 | if series.patient_position != None: 189 | sb.current_study['PatientPosition'] = series.patient_position 190 | if series.patient_id != None: 191 | sb.current_study['PatientID'] = series.patient_id 192 | if series.patient_name != None: 193 | sb.current_study['PatientName'] = series.patient_name 194 | if series.patient_birthdate != None: 195 | sb.current_study['PatientBirthDate'] = series.patient_birthdate 196 | if series.modality == "CT": 197 | ib.build() 198 | elif series.modality == "MR": 199 | ib.build() 200 | elif series.modality == "PT": 201 | ib.build() 202 | elif series.modality == "RTDOSE": 203 | ib.build() 204 | elif series.modality == "RTPLAN": 205 | if all(d.isdigit() for d in series.beams): 206 | nbeams = int(series.beams) 207 | gantry_angles = [i * 360 / nbeams for i in range(nbeams)] 208 | else: 209 | gantry_angles = [int(b) for b in series.beams.lstrip('[').rstrip(']').split(";")] 210 | nbeams = len(gantry_angles) 211 | if all(d.isdigit() for d in series.collimator_angles): 212 | collimator_angles = [int(series.collimator_angles)] * nbeams 213 | else: 214 | collimator_angles = [int(b) for b in series.collimator_angles.lstrip('[').rstrip(']').split(";")] 215 | if all(d.isdigit() or d == '.' for d in series.meterset): 216 | meterset = [float(series.meterset)] * nbeams 217 | else: 218 | meterset = [float(b) for b in series.meterset.lstrip('[').rstrip(']').split(";")] 219 | if all(d.isdigit() for d in series.patient_support_angles): 220 | patient_support_angles = [int(series.patient_support_angles)] * nbeams 221 | else: 222 | patient_support_angles = \ 223 | [int(b) for b in series.patient_support_angles.lstrip('[').rstrip(']').split(";")] 224 | table_top = TableTop(*[float(b) for b in series.table_top.split(",")]) 225 | table_top_eccentric = TableTopEcc(*[float(b) for b in series.table_top_eccentric.split(",")]) 226 | for i in range(nbeams): 227 | rp.build_beam(gantry_angle=gantry_angles[i], 228 | meterset=meterset[i], 229 | collimator_angle=collimator_angles[i], 230 | patient_support_angle=patient_support_angles[i], 231 | table_top=table_top, 232 | table_top_eccentric=table_top_eccentric) 233 | for mlcshape in series.mlcshapes: 234 | mlcshape = mlcshape.split(",") 235 | if all(d.isdigit() for d in mlcshape[0]): 236 | beams = [rp.beam_builders[int(mlcshape[0])-1]] 237 | mlcshape = mlcshape[1:] 238 | else: 239 | beams = rp.beam_builders 240 | if mlcshape[0] == "circle": 241 | radius = float(mlcshape[1]) 242 | if len(mlcshape) > 2: 243 | center = [float(c) for c in mlcshape[2].lstrip('[').rstrip(']').split(";")] 244 | else: 245 | center = [0, 0] 246 | for beam in beams: 247 | beam.conform_to_circle(radius, center) 248 | elif mlcshape[0] == "rectangle": 249 | X, Y = float(mlcshape[1]), float(mlcshape[2]) 250 | if len(mlcshape) > 3: 251 | center = [float(c) for c in mlcshape[3].lstrip('[').rstrip(']').split(";")] 252 | else: 253 | center = [0, 0] 254 | for beam in beams: 255 | beam.conform_to_rectangle(X, Y, center) 256 | for beam in beams: 257 | beam.conform_jaws_to_mlc() 258 | for jawshape in series.jawshapes: 259 | jawshape = jawshape.split(",") 260 | if len(jawshape) == 2: 261 | beams = [rp.beam_builders[int(jawshape[0])-1]] 262 | jawshape = jawshape[1:] 263 | else: 264 | beams = rp.beam_builders 265 | jawsize = [float(c) for c in jawshape[0].lstrip('[').rstrip(']').split(";")] 266 | if len(jawsize) > 2: 267 | center = [jawsize[2], jawsize[3]] 268 | else: 269 | center = [0, 0] 270 | for beam in beams: 271 | beam.conform_jaws_to_rectangle(jawsize[0], jawsize[1], center) 272 | rp.build() 273 | elif series.modality == "RTSTRUCT": 274 | for structure in series.structures: 275 | structure = structure.split(",") 276 | shape = structure[0] 277 | if shape == 'sphere': 278 | name = structure[1] 279 | radius = float(structure[2]) 280 | interpreted_type = structure[3] 281 | if len(structure) > 4: 282 | center = [float(c) for c in structure[4].lstrip('[').rstrip(']').split(";")] 283 | else: 284 | center = [0, 0, 0] 285 | rtstruct.add_sphere(name=name, radius=radius, center=center, interpreted_type=interpreted_type) 286 | elif shape == 'box': 287 | name = structure[1] 288 | size = structure[2] 289 | if size.startswith("[") and size.endswith("]"): 290 | size = [float(c) for c in size.lstrip('[').rstrip(']').split(";")] 291 | else: 292 | size = [float(size), float(size), float(size)] 293 | interpreted_type = structure[3] 294 | if len(structure) > 4: 295 | center = [float(c) for c in structure[4].lstrip('[').rstrip(']').split(";")] 296 | else: 297 | center = [0, 0, 0] 298 | rtstruct.add_box(name=name, size=size, center=center, interpreted_type=interpreted_type) 299 | elif shape == "external": 300 | rtstruct.add_external_box() 301 | rtstruct.build() 302 | sb.write(args.outdir) 303 | -------------------------------------------------------------------------------- /builders.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import modules 3 | from collections import defaultdict 4 | import os 5 | import pydicom as dicom 6 | dicom.config.use_DS_decimal = False 7 | dicom.config.allow_DS_float = True 8 | 9 | class ImageBuilder(object): 10 | @property 11 | def gridsize(self): 12 | return np.array([self.num_voxels[0] * self.voxel_size[0], 13 | self.num_voxels[1] * self.voxel_size[1], 14 | self.num_voxels[2] * self.voxel_size[2]]) 15 | 16 | @property 17 | def column_direction(self): 18 | return self.ImageOrientationPatient[:3] 19 | 20 | @property 21 | def row_direction(self): 22 | return self.ImageOrientationPatient[3:] 23 | 24 | def mgrid(self): 25 | coldir = self.column_direction 26 | rowdir = self.row_direction 27 | slicedir = self.slice_direction 28 | if hasattr(self, '_last_mgrid_params') and (coldir, rowdir, slicedir, self.num_voxels, self.center, self.voxel_size) == self._last_mgrid_params: 29 | return self._last_mgrid 30 | self._last_mgrid_params = (coldir, rowdir, slicedir, self.num_voxels, self.center, self.voxel_size) 31 | nv = np.array(self.num_voxels)/2.0 32 | col,row,slice=np.mgrid[-nv[0]:nv[0], -nv[1]:nv[1], -nv[2]:nv[2]] 33 | x = (self.center[0] + (row + 0.5) * rowdir[0] * self.voxel_size[1] + 34 | (col + 0.5) * coldir[0] * self.voxel_size[0] + 35 | (slice + 0.5) * slicedir[0] * self.voxel_size[2]) 36 | y = (self.center[1] + (row + 0.5) * rowdir[1] * self.voxel_size[1] + 37 | (col + 0.5) * coldir[1] * self.voxel_size[0] + 38 | (slice + 0.5) * slicedir[1] * self.voxel_size[2]) 39 | z = (self.center[2] + (row + 0.5) * rowdir[2] * self.voxel_size[1] + 40 | (col + 0.5) * coldir[2] * self.voxel_size[0] + 41 | (slice + 0.5) * slicedir[2] * self.voxel_size[2]) 42 | self._last_mgrid = (x,y,z) 43 | return x,y,z 44 | 45 | def clear(self, real_value = None, stored_value = None): 46 | if real_value != None: 47 | assert stored_value is None 48 | stored_value = self.real_value_to_stored_value(real_value) 49 | self.pixel_array[:] = stored_value 50 | 51 | 52 | def add_sphere(self, radius, center, stored_value = None, real_value = None, mode = 'set'): 53 | if real_value != None: 54 | assert stored_value is None 55 | stored_value = self.real_value_to_stored_value(real_value) 56 | x,y,z = self.mgrid() 57 | voxels = (x-center[0])**2 + (y-center[1])**2 + (z-center[2])**2 <= radius**2 58 | if mode == 'set': 59 | self.pixel_array[voxels] = stored_value 60 | elif mode == 'add': 61 | self.pixel_array[voxels] += stored_value 62 | elif mode == 'subtract': 63 | self.pixel_array[voxels] -= stored_value 64 | else: 65 | assert 'unknown mode' 66 | 67 | def add_box(self, size, center, stored_value = None, real_value = None, mode = 'set'): 68 | if real_value != None: 69 | assert stored_value is None 70 | stored_value = (real_value - self.rescale_intercept) / self.rescale_slope 71 | x,y,z = self.mgrid() 72 | voxels = (abs(x-center[0]) <= size[0]/2.0) * (abs(y-center[1]) <= size[1]/2.0) * (abs(z-center[2]) <= size[2]/2.0) 73 | if mode == 'set': 74 | self.pixel_array[voxels] = stored_value 75 | elif mode == 'add': 76 | self.pixel_array[voxels] += stored_value 77 | elif mode == 'subtract': 78 | self.pixel_array[voxels] -= stored_value 79 | else: 80 | assert 'unknown mode' 81 | 82 | class StudyBuilder(object): 83 | def __init__(self, patient_position="HFS", patient_id="", patient_name="", patient_birthdate=""): 84 | self.modalityorder = ["CT", "MR", "PT", "RTSTRUCT", "RTPLAN", "RTDOSE"] 85 | self.current_study = {} 86 | self.current_study['PatientID'] = patient_id 87 | self.current_study['PatientName'] = patient_name 88 | self.current_study['PatientBirthDate'] = patient_birthdate 89 | self.current_study['PatientPosition'] = patient_position 90 | self.seriesbuilders = defaultdict(lambda: []) 91 | self.built = False 92 | 93 | def build_ct(self, num_voxels, voxel_size, pixel_representation, rescale_slope, rescale_intercept, 94 | center=None, column_direction=None, row_direction=None, slice_direction=None): 95 | b = CTBuilder(self.current_study, num_voxels, voxel_size, 96 | pixel_representation=pixel_representation, 97 | center=center, 98 | rescale_slope=rescale_slope, 99 | rescale_intercept=rescale_intercept, 100 | column_direction=column_direction, 101 | row_direction=row_direction, 102 | slice_direction=slice_direction) 103 | self.seriesbuilders['CT'].append(b) 104 | return b 105 | 106 | def build_mr(self, num_voxels, voxel_size, pixel_representation, center=None, column_direction=None, 107 | row_direction=None, slice_direction=None): 108 | b = MRBuilder(self.current_study, num_voxels, voxel_size, 109 | pixel_representation=pixel_representation, 110 | center=center, 111 | column_direction=column_direction, 112 | row_direction=row_direction, 113 | slice_direction=slice_direction) 114 | self.seriesbuilders['MR'].append(b) 115 | return b 116 | 117 | def build_pt(self, num_voxels, voxel_size, pixel_representation, rescale_slope, center=None, column_direction=None, 118 | row_direction=None, slice_direction=None): 119 | b = PTBuilder(self.current_study, num_voxels, voxel_size, 120 | pixel_representation=pixel_representation, 121 | center=center, 122 | rescale_slope=rescale_slope, 123 | column_direction=column_direction, 124 | row_direction=row_direction, 125 | slice_direction=slice_direction) 126 | self.seriesbuilders['PT'].append(b) 127 | return b 128 | 129 | def build_static_plan(self, nominal_beam_energy=6, isocenter=None, num_leaves=None, mlc_direction=None, leaf_widths=None, structure_set=None, sad=None): 130 | if structure_set is None and len(self.seriesbuilders['RTSTRUCT']) == 1: 131 | structure_set = self.seriesbuilders['RTSTRUCT'][0] 132 | b = StaticPlanBuilder(current_study=self.current_study, 133 | nominal_beam_energy=nominal_beam_energy, isocenter=isocenter, 134 | num_leaves=num_leaves, mlc_direction=mlc_direction, leaf_widths=leaf_widths, 135 | structure_set=structure_set, sad=sad) 136 | self.seriesbuilders['RTPLAN'].append(b) 137 | return b 138 | 139 | def build_structure_set(self, images=None): 140 | if images is None and len(self.seriesbuilders['CT']) == 1: 141 | images = self.seriesbuilders['CT'][0] 142 | b = StructureSetBuilder(self.current_study, images=images) 143 | self.seriesbuilders['RTSTRUCT'].append(b) 144 | return b 145 | 146 | def build_dose(self, planbuilder=None, num_voxels=None, voxel_size=None, center=None, dose_grid_scaling=1.0, column_direction=None, row_direction=None, slice_direction=None): 147 | if planbuilder is None and len(self.seriesbuilders['RTPLAN']) == 1: 148 | planbuilder = self.seriesbuilders['RTPLAN'][0] 149 | if (planbuilder != None 150 | and planbuilder.structure_set != None 151 | and planbuilder.structure_set.images != None): 152 | images = planbuilder.structure_set.images 153 | if num_voxels is None and voxel_size is None and center is None: 154 | num_voxels = images.num_voxels 155 | voxel_size = images.voxel_size 156 | center = images.center 157 | if column_direction is None and row_direction is None: 158 | column_direction, row_direction = images.column_direction, images.row_direction 159 | if slice_direction is None: 160 | slice_direction = images.slice_direction 161 | 162 | b = DoseBuilder(current_study=self.current_study, planbuilder=planbuilder, num_voxels=num_voxels, voxel_size=voxel_size, center=center, dose_grid_scaling=dose_grid_scaling, column_direction=column_direction, row_direction=row_direction, slice_direction=slice_direction) 163 | self.seriesbuilders['RTDOSE'].append(b) 164 | return b 165 | 166 | def build(self): 167 | if self.built: 168 | return self.datasets 169 | datasets = [] 170 | for modality, sbs in self.seriesbuilders.iteritems(): 171 | for sb in sbs: 172 | datasets += sb.Build() 173 | self.built = True 174 | self.datasets = datasets 175 | return self.datasets 176 | 177 | def write(self, outdir='.', print_filenames=False): 178 | for modality in self.modalityorder: 179 | for sb in self.seriesbuilders[modality]: 180 | print modality, sb 181 | for ds in sb.build(): 182 | dicom.write_file(os.path.join(outdir, ds.filename), ds) 183 | if print_filenames: 184 | print ds.filename 185 | 186 | 187 | class CTBuilder(ImageBuilder): 188 | def __init__( 189 | self, 190 | current_study, 191 | num_voxels, 192 | voxel_size, 193 | pixel_representation, 194 | rescale_slope, 195 | rescale_intercept, 196 | center=None, 197 | column_direction=None, 198 | row_direction=None, 199 | slice_direction=None): 200 | self.num_voxels = num_voxels 201 | self.voxel_size = voxel_size 202 | self.pixel_representation = pixel_representation 203 | self.rescale_slope = rescale_slope 204 | self.rescale_intercept = rescale_intercept 205 | if center is None: 206 | center = [0, 0, 0] 207 | self.center = np.array(center) 208 | 209 | assert self.pixel_representation == 0 or self.pixel_representation == 1 210 | if self.pixel_representation == 0: 211 | self.pixel_array = np.zeros(self.num_voxels, dtype=np.uint16) 212 | else: 213 | self.pixel_array = np.zeros(self.num_voxels, dtype=np.int16) 214 | 215 | if column_direction is None or row_direction is None: 216 | assert column_direction is None and row_direction is None 217 | column_direction = [1,0,0] 218 | row_direction = [0,1,0] 219 | if slice_direction is None: 220 | slice_direction = np.cross(column_direction, row_direction) 221 | slice_direction = slice_direction / np.linalg.norm(slice_direction) 222 | self.ImageOrientationPatient = column_direction + row_direction 223 | self.slice_direction = slice_direction 224 | self.current_study = current_study 225 | self.built = False 226 | 227 | def real_value_to_stored_value(self, real_value): 228 | return (real_value - self.rescale_intercept) / self.rescale_slope 229 | 230 | def build(self): 231 | if self.built: 232 | return self.datasets 233 | cts = modules.build_ct( 234 | ct_data=self.pixel_array, 235 | pixel_representation=self.pixel_representation, 236 | voxel_size=self.voxel_size, 237 | center=self.center, 238 | current_study=self.current_study, 239 | rescale_slope=self.rescale_slope, 240 | rescale_intercept=self.rescale_intercept) 241 | x, y, z = self.mgrid() 242 | for slicei in range(len(cts)): 243 | cts[slicei].ImagePositionPatient = [x[0,0,slicei],y[0,0,slicei],z[0,0,slicei]] 244 | cts[slicei].ImageOrientationPatient = self.ImageOrientationPatient 245 | self.built = True 246 | self.datasets = cts 247 | return self.datasets 248 | 249 | 250 | class MRBuilder(ImageBuilder): 251 | def __init__( 252 | self, 253 | current_study, 254 | num_voxels, 255 | voxel_size, 256 | pixel_representation, 257 | center=None, 258 | column_direction=None, 259 | row_direction=None, 260 | slice_direction=None): 261 | self.num_voxels = num_voxels 262 | self.voxel_size = voxel_size 263 | self.pixel_representation = pixel_representation 264 | if center is None: 265 | center = [0, 0, 0] 266 | self.center = np.array(center) 267 | 268 | assert self.pixel_representation == 0 or self.pixel_representation == 1 269 | if self.pixel_representation == 0: 270 | self.pixel_array = np.zeros(self.num_voxels, dtype=np.uint16) 271 | else: 272 | self.pixel_array = np.zeros(self.num_voxels, dtype=np.int16) 273 | 274 | if column_direction is None or row_direction is None: 275 | assert column_direction is None and row_direction is None 276 | column_direction = [1, 0, 0] 277 | row_direction = [0, 1, 0] 278 | if slice_direction is None: 279 | slice_direction = np.cross(column_direction, row_direction) 280 | slice_direction = slice_direction / np.linalg.norm(slice_direction) 281 | self.ImageOrientationPatient = column_direction + row_direction 282 | self.slice_direction = slice_direction 283 | self.current_study = current_study 284 | self.built = False 285 | 286 | def real_value_to_stored_value(self, real_value): 287 | return real_value 288 | 289 | def build(self): 290 | if self.built: 291 | return self.datasets 292 | mrs = modules.build_mr( 293 | mr_data=self.pixel_array, 294 | pixel_representation=self.pixel_representation, 295 | voxel_size=self.voxel_size, 296 | center=self.center, 297 | current_study=self.current_study) 298 | x, y, z = self.mgrid() 299 | for slicei in range(len(mrs)): 300 | mrs[slicei].ImagePositionPatient = [x[0, 0, slicei], y[0, 0, slicei], z[0, 0, slicei]] 301 | mrs[slicei].ImageOrientationPatient = self.ImageOrientationPatient 302 | self.built = True 303 | self.datasets = mrs 304 | return self.datasets 305 | 306 | 307 | class PTBuilder(ImageBuilder): 308 | def __init__( 309 | self, 310 | current_study, 311 | num_voxels, 312 | voxel_size, 313 | pixel_representation, 314 | rescale_slope, 315 | center=None, 316 | column_direction=None, 317 | row_direction=None, 318 | slice_direction=None): 319 | self.num_voxels = num_voxels 320 | self.voxel_size = voxel_size 321 | self.pixel_representation = pixel_representation 322 | self.rescale_slope = rescale_slope 323 | if center is None: 324 | center = [0, 0, 0] 325 | self.center = np.array(center) 326 | 327 | assert self.pixel_representation == 0 or self.pixel_representation == 1 328 | if self.pixel_representation == 0: 329 | self.pixel_array = np.zeros(self.num_voxels, dtype=np.uint16) 330 | else: 331 | self.pixel_array = np.zeros(self.num_voxels, dtype=np.int16) 332 | 333 | if column_direction is None or row_direction is None: 334 | assert column_direction is None and row_direction is None 335 | column_direction = [1, 0, 0] 336 | row_direction = [0, 1, 0] 337 | if slice_direction is None: 338 | slice_direction = np.cross(column_direction, row_direction) 339 | slice_direction = slice_direction / np.linalg.norm(slice_direction) 340 | self.ImageOrientationPatient = column_direction + row_direction 341 | self.slice_direction = slice_direction 342 | self.current_study = current_study 343 | self.built = False 344 | 345 | def real_value_to_stored_value(self, real_value): 346 | return real_value 347 | 348 | def build(self): 349 | if self.built: 350 | return self.datasets 351 | pts = modules.build_pt( 352 | pt_data=self.pixel_array, 353 | pixel_representation=self.pixel_representation, 354 | rescale_slope=self.rescale_slope, 355 | voxel_size=self.voxel_size, 356 | center=self.center, 357 | current_study=self.current_study) 358 | x, y, z = self.mgrid() 359 | for slicei in range(len(pts)): 360 | pts[slicei].ImagePositionPatient = [x[0, 0, slicei], y[0, 0, slicei], z[0, 0, slicei]] 361 | pts[slicei].ImageOrientationPatient = self.ImageOrientationPatient 362 | self.built = True 363 | self.datasets = pts 364 | return self.datasets 365 | 366 | 367 | from coordinates import TableTop, TableTopEcc 368 | 369 | 370 | class StaticBeamBuilder(object): 371 | def __init__(self, current_study, gantry_angle, meterset, nominal_beam_energy, 372 | collimator_angle=0, patient_support_angle=0, table_top=None, table_top_eccentric=None, sad=None): 373 | if table_top is None: 374 | table_top = TableTop() 375 | if table_top_eccentric is None: 376 | table_top_eccentric = TableTopEcc() 377 | self.gantry_angle = gantry_angle 378 | self.sad = sad 379 | self.collimator_angle = collimator_angle 380 | self.patient_support_angle = patient_support_angle 381 | self.table_top = table_top 382 | self.table_top_eccentric = table_top_eccentric 383 | self.meterset = meterset 384 | self.nominal_beam_energy = nominal_beam_energy 385 | self.current_study = current_study 386 | self.conform_calls = [] 387 | self.jaws = None 388 | self.built = False 389 | 390 | def conform_to_circle(self, radius, center): 391 | self.conform_calls.append(lambda beam: modules.conform_mlc_to_circle(beam, radius, center)) 392 | 393 | def conform_to_rectangle(self, x, y, center): 394 | self.conform_calls.append(lambda beam: modules.conform_mlc_to_rectangle(beam, x, y, center)) 395 | 396 | def conform_jaws_to_rectangle(self, x, y, center): 397 | self.conform_calls.append(lambda beam: modules.conform_jaws_to_rectangle(beam, x, y, center)) 398 | 399 | def conform_jaws_to_mlc(self): 400 | self.conform_calls.append(lambda beam: modules.conform_jaws_to_mlc(beam)) 401 | 402 | def finalize_mlc(self): 403 | modules.finalize_mlc(self.rtbeam) 404 | 405 | def build(self, rtplan, planbuilder, finalize_mlc=True): 406 | if self.built: 407 | return self.rtbeam 408 | self.built = True 409 | self.rtbeam = modules.add_static_rt_beam(ds = rtplan, nleaves = planbuilder.num_leaves, mlcdir = planbuilder.mlc_direction, leafwidths = planbuilder.leaf_widths, gantry_angle = self.gantry_angle, collimator_angle = self.collimator_angle, patient_support_angle = self.patient_support_angle, table_top = self.table_top, table_top_eccentric = self.table_top_eccentric, isocenter = planbuilder.isocenter, nominal_beam_energy = self.nominal_beam_energy, current_study = self.current_study, sad=self.sad) 410 | for call in self.conform_calls: 411 | call(self.rtbeam) 412 | if self.jaws is None: 413 | modules.conform_jaws_to_mlc(self.rtbeam) 414 | if finalize_mlc: 415 | self.finalize_mlc() 416 | return self.rtbeam 417 | 418 | class StaticPlanBuilder(object): 419 | def __init__(self, current_study, nominal_beam_energy=6, isocenter=None, num_leaves=None, mlc_direction=None, leaf_widths=None, structure_set=None, sad=None): 420 | self.isocenter = isocenter or [0,0,0] 421 | self.num_leaves = num_leaves or [10,40,10] 422 | self.leaf_widths = leaf_widths or [10, 5, 10] 423 | self.mlc_direction = mlc_direction or "MLCX" 424 | self.beam_builders = [] 425 | self.current_study = current_study 426 | self.structure_set = structure_set 427 | self.nominal_beam_energy = nominal_beam_energy 428 | self.sad = sad 429 | self.built = False 430 | 431 | def build_beam(self, gantry_angle, meterset, collimator_angle=0, patient_support_angle=0, table_top=None, table_top_eccentric=None, sad=None): 432 | if sad is None: 433 | sad = self.sad 434 | sbb = StaticBeamBuilder(current_study = self.current_study, meterset = meterset, nominal_beam_energy = self.nominal_beam_energy, gantry_angle = gantry_angle, collimator_angle = collimator_angle, patient_support_angle = patient_support_angle, table_top = table_top, table_top_eccentric = table_top_eccentric, sad = sad) 435 | self.beam_builders.append(sbb) 436 | return sbb 437 | 438 | def build(self, finalize_mlc = True): 439 | if self.built: 440 | return self.datasets 441 | rtplan = modules.build_rt_plan(self.current_study, self.isocenter, self.structure_set.build()[0]) 442 | assert len(rtplan.FractionGroupSequence) == 1 443 | fraction_group = rtplan.FractionGroupSequence[0] 444 | for bb in self.beam_builders: 445 | rtbeam = bb.build(rtplan, self, finalize_mlc=finalize_mlc) 446 | modules.add_beam_to_rt_fraction_group(fraction_group, rtbeam, bb.meterset) 447 | self.built = True 448 | self.datasets = [rtplan] 449 | return self.datasets 450 | 451 | class ROIBuilder(object): 452 | def __init__(self, structure_set_builder, name, interpreted_type, roi_number, contours=None): 453 | self.structure_set_builder = structure_set_builder 454 | if contours is None: 455 | self.contours = [] 456 | else: 457 | self.contours = contours 458 | self.name = name 459 | self.interpreted_type = interpreted_type 460 | self.roi_number = roi_number 461 | self.built = False 462 | 463 | def build(self, structure_set): 464 | if self.built: 465 | return self.roi 466 | roi = modules.add_roi_to_structure_set(structure_set, self.name, self.structure_set_builder.current_study) 467 | roi_contour = modules.add_roi_to_roi_contour(structure_set, roi, self.contours, self.structure_set_builder.images.build()) 468 | roi_observation = modules.add_roi_to_rt_roi_observation(structure_set, roi, self.name, self.interpreted_type) 469 | self.built = True 470 | self.roi = roi 471 | self.roi_contour = roi_contour 472 | self.roi_observation = roi_observation 473 | return self.roi 474 | 475 | class StructureSetBuilder(object): 476 | def __init__(self, current_study, images): 477 | self.current_study = current_study 478 | self.images = images 479 | self.roi_builders = [] 480 | self.built = False 481 | 482 | def add_external_box(self, name="External", roi_number=None): 483 | self.add_box(size = self.images.gridsize, 484 | center = self.images.center, 485 | name = name, 486 | interpreted_type = "EXTERNAL", 487 | roi_number = roi_number) 488 | 489 | def add_box(self, size, center, name, interpreted_type, roi_number = None): 490 | x,y,z = self.images.mgrid() 491 | contours = np.array([[[X*size[0]/2 + center[0], 492 | Y*X*size[1]/2 + center[1], 493 | Z] 494 | for X in [-1,1] 495 | for Y in [-1,1]] 496 | for Z in z[0,0,:] if ((Z - center[2]) >= -size[2]/2 and 497 | (Z - center[2]) < size[2]/2)]) 498 | return self.add_contours(contours, name, interpreted_type, roi_number) 499 | 500 | def add_sphere(self, radius, center, name, interpreted_type, roi_number = None, ntheta = 12): 501 | x,y,z = self.images.mgrid() 502 | contours = np.array([[[np.sqrt(radius**2 - (Z-center[2])**2) * np.cos(theta) + center[0], 503 | np.sqrt(radius**2 - (Z-center[2])**2) * np.sin(theta) + center[1], 504 | Z] 505 | for theta in np.linspace(0, 2*np.pi, ntheta, endpoint=False)] 506 | for Z in z[0,0,np.abs(z[0,0,:] - center[2]) < radius]]) 507 | return self.add_contours(contours, name, interpreted_type, roi_number) 508 | 509 | 510 | def add_contours(self, contours, name, interpreted_type, roi_number = None): 511 | if roi_number is None: 512 | roi_number = 1 513 | for rb in self.roi_builders: 514 | roi_number = max(roi_number, rb.roi_number + 1) 515 | 516 | rb = ROIBuilder(name = name, structure_set_builder = self, interpreted_type = interpreted_type, 517 | roi_number = roi_number, contours = contours) 518 | self.roi_builders.append(rb) 519 | return rb 520 | 521 | 522 | def build(self): 523 | if self.built: 524 | return self.datasets 525 | rs = modules.build_rt_structure_set(self.images.build(), self.current_study) 526 | for rb in self.roi_builders: 527 | rb.build(rs) 528 | self.built = True 529 | self.datasets = [rs] 530 | return self.datasets 531 | 532 | from modules import do_for_all_cps 533 | 534 | class DoseBuilder(ImageBuilder): 535 | def __init__(self, current_study, planbuilder, num_voxels, voxel_size, center=None, dose_grid_scaling=1.0, column_direction=None, row_direction=None, slice_direction=None): 536 | self.current_study = current_study 537 | self.planbuilder = planbuilder 538 | self.num_voxels = num_voxels 539 | self.voxel_size = voxel_size 540 | self.pixel_array = np.zeros(self.num_voxels, dtype=np.int16) 541 | if center is None: 542 | center = [0,0,0] 543 | self.center = np.array(center) 544 | if column_direction is None or row_direction is None: 545 | assert column_direction is None and row_direction is None 546 | column_direction = [1,0,0] 547 | row_direction = [0,1,0] 548 | if slice_direction is None: 549 | slice_direction = np.cross(column_direction, row_direction) 550 | slice_direction = slice_direction / np.linalg.norm(slice_direction) 551 | self.ImageOrientationPatient = column_direction + row_direction 552 | self.slice_direction = slice_direction 553 | self.dose_grid_scaling = dose_grid_scaling 554 | self.built = False 555 | 556 | def real_value_to_stored_value(self, real_value): 557 | return real_value / self.dose_grid_scaling 558 | 559 | def add_lightfield(self, beam, weight): 560 | x,y,z = self.mgrid() 561 | coords = (np.array([x.ravel(), y.ravel(), z.ravel(), np.ones(x.shape).ravel()]).reshape((4,1,1,np.prod(x.shape)))) 562 | bld = modules.getblds(beam.BeamLimitingDeviceSequence) 563 | mlcdir, jawdir1, jawdir2 = modules.get_mlc_and_jaw_directions(bld) 564 | mlcidx = (0,1) if mlcdir == "MLCX" else (1,0) 565 | 566 | def add_lightfield_for_cp(cp, gantry_angle, gantry_pitch_angle, beam_limiting_device_angle, 567 | patient_support_angle, patient_position, 568 | table_top, table_top_ecc, sad, isocenter, bldp): 569 | Mdb = modules.get_dicom_to_bld_coordinate_transform(gantry_angle, gantry_pitch_angle, beam_limiting_device_angle, 570 | patient_support_angle, patient_position, 571 | table_top, table_top_ecc, sad, isocenter) 572 | c = Mdb * coords 573 | # Negation here since everything is at z < 0 in the b system, and that rotates by 180 degrees 574 | c2 = -np.array([float(beam.SourceAxisDistance)*c[0,:]/c[2,:], 575 | float(beam.SourceAxisDistance)*c[1,:]/c[2,:]]).squeeze() 576 | nleaves = len(bld[mlcdir].LeafPositionBoundaries)-1 577 | for i in range(nleaves): 578 | self.pixel_array.ravel()[ 579 | (c2[0,:] >= float(bldp['ASYMX'].LeafJawPositions[0])) * 580 | (c2[0,:] < float(bldp['ASYMX'].LeafJawPositions[1])) * 581 | (c2[1,:] >= float(bldp['ASYMY'].LeafJawPositions[0])) * 582 | (c2[1,:] < float(bldp['ASYMY'].LeafJawPositions[1])) * 583 | (c2[mlcidx[0],:] >= float(bldp[mlcdir].LeafJawPositions[i])) * 584 | (c2[mlcidx[0],:] < float(bldp[mlcdir].LeafJawPositions[i + nleaves])) * 585 | (c2[mlcidx[1],:] >= float(bld[mlcdir].LeafPositionBoundaries[i])) * 586 | (c2[mlcidx[1],:] < float(bld[mlcdir].LeafPositionBoundaries[i+1])) 587 | ] += 1 588 | do_for_all_cps(beam, self.current_study['PatientPosition'], add_lightfield_for_cp) 589 | 590 | def build(self): 591 | if self.built: 592 | return self.datasets 593 | rd = modules.build_rt_dose(self.pixel_array, self.voxel_size, self.center, self.current_study, 594 | self.planbuilder.build()[0], self.dose_grid_scaling, self.dose_summation_type, self.beam_number) 595 | x,y,z = self.mgrid() 596 | rd.ImagePositionPatient = [x[0,0,0],y[0,0,0],z[0,0,0]] 597 | rd.ImageOrientationPatient = self.ImageOrientationPatient 598 | 599 | self.built = True 600 | self.datasets = [rd] 601 | return self.datasets 602 | -------------------------------------------------------------------------------- /coordinates.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | debug = False 3 | 4 | if not debug: 5 | from math import sin, cos, pi 6 | def n(x): 7 | return float(x) 8 | else: 9 | from sympy import sin, cos, pi, Symbol 10 | tg, tb, tw, ts, te, Ey, Ty, Bz, Wz = [Symbol(s) for s in ['tg', 'tb', 'tw', 'ts', 'te', 'Ey', 'Ty', 'Bz', 'Wz']] 11 | def n(x): 12 | return x 13 | 14 | def rotX(psi): 15 | return np.matrix([[1, 0, 0, 0], 16 | [0, cos(n(psi)*pi/180), sin(n(psi)*pi/180), 0], 17 | [0, -sin(n(psi)*pi/180), cos(n(psi)*pi/180), 0], 18 | [0, 0, 0, 1]]) 19 | def rotY(phi): 20 | return np.matrix([[cos(n(phi)*pi/180), 0, -sin(n(phi)*pi/180), 0], 21 | [0, 1, 0, 0], 22 | [sin(n(phi)*pi/180), 0, cos(n(phi)*pi/180), 0], 23 | [0, 0, 0, 1]]) 24 | def rotZ(theta): 25 | return np.matrix([[cos(n(theta)*pi/180), sin(n(theta)*pi/180), 0, 0], 26 | [-sin(n(theta)*pi/180), cos(n(theta)*pi/180), 0, 0], 27 | [0, 0, 1, 0], 28 | [0, 0, 0, 1]]) 29 | 30 | def translate(x,y,z): 31 | return np.matrix([[1,0,0,n(x)], 32 | [0,1,0,n(y)], 33 | [0,0,1,n(z)], 34 | [0,0,0,1]]) 35 | 36 | def invert4x4fast(m): 37 | """Actually slower and less exact than numpy.linalg.inv().""" 38 | m = m.copy() 39 | m[1,0], m[0,1] = m[0,1], m[1,0] 40 | m[2,0], m[0,2] = m[0,2], m[2,0] 41 | m[2,1], m[1,2] = m[1,2], m[2,1] 42 | m03 = -(m[0,0] * m[0,3] + m[0,1] * m[1,3] + m[0,2] * m[2,3]) 43 | m13 = -(m[1,0] * m[0,3] + m[1,1] * m[1,3] + m[1,2] * m[2,3]) 44 | m[2,3] = -(m[2,0] * m[0,3] + m[2,1] * m[1,3] + m[2,2] * m[2,3]) 45 | m[1,3] = m13 46 | m[0,3] = m03 47 | return m 48 | 49 | def Mfs(theta_s): 50 | """Transform from fixed to patient support coordinate system.""" 51 | return rotZ(theta_s) 52 | def Mse(Ls, theta_e): 53 | """Transform from patient support to table top eccentric coordinate system.""" 54 | return rotZ(theta_e) * translate(0, -Ls, 0) 55 | def Met(Tx, Ty, Tz, psi_t, phi_t): 56 | """Transform from table top eccentric to table top coordinate system.""" 57 | # The order of rotations must be the same as the rotations are described in IEC61217 58 | return rotY(phi_t) * rotX(psi_t) * translate(-Tx, -Ty, -Tz) 59 | def Mtp(Px, Py, Pz, psi_p, phi_p, theta_p): 60 | """Transform from table top to patient coordinate system.""" 61 | # The order of rotations must be the same as the rotations are described in IEC61217 62 | return rotZ(theta_p) * rotY(phi_p) * rotX(psi_p) * translate(-Px, -Py, -Pz) 63 | def Mfg(psi_g, phi_g): 64 | """Transform from fixed to gantry coordinate system, plus the non-standard DICOM gantry pitch rotation psi_g.""" 65 | return rotX(psi_g) * rotY(phi_g) 66 | def Mgb(Bz, theta_b): 67 | """Transform from gantry to beam limiting device or delineator coordinate system.""" 68 | return rotZ(theta_b) * translate(0, 0, -Bz) 69 | def Mbw(Wz, theta_w): 70 | """Transform from beam limiting device or delineator to wedge filter coordinate system.""" 71 | return rotZ(theta_w) * translate(0, 0, -Wz) 72 | def Mgr(Rx, Ry, Rz, theta_r): 73 | """Transform from gantry to X-ray image receptor coordinate system.""" 74 | return rotZ(theta_r) * translate(-Rx, -Ry, -Rz) 75 | def Mpd(): 76 | """Transform from patient to DICOM patient coordinate system.""" 77 | return np.matrix([[1, 0, 0, 0], 78 | [0, 0, -1, 0], 79 | [0, 1, 0, 0], 80 | [0, 0, 0, 1]]) 81 | 82 | class Coordinates(object): 83 | def __init__(self, theta_s = 0, 84 | Ls = 0, theta_e = 0, 85 | Tx = 0, Ty = 0, Tz = 0, psi_t = 0, phi_t = 0, 86 | Px = 0, Py = 0, Pz = 0, psi_p = 0, phi_p = 0, theta_p = 0, 87 | psi_g = 0, phi_g = 0, 88 | Bz = 0, theta_b = 0, 89 | Wz = 0, theta_w = 0, 90 | Rx = 0, Ry = 0, Rz = 0, theta_r = 0): 91 | self.theta_s = theta_s 92 | self.Ls = Ls 93 | self.theta_e = theta_e 94 | self.Tx = Tx 95 | self.Ty = Ty 96 | self.Tz = Tz 97 | self.psi_t = psi_t 98 | self.phi_t = phi_t 99 | self.Px = Px 100 | self.Py = Py 101 | self.Pz = Pz 102 | self.psi_p = psi_p 103 | self.phi_p = phi_p 104 | self.theta_p = theta_p 105 | self.psi_g = psi_g 106 | self.phi_g = phi_g 107 | self.Bz = Bz 108 | self.theta_b = theta_b 109 | self.Wz = Wz 110 | self.theta_w = theta_w 111 | self.Rx = Rx 112 | self.Ry = Ry 113 | self.Rz = Rz 114 | self.theta_r = theta_r 115 | 116 | def get_parents(self, from_system): 117 | for c in coordinate_systems: 118 | if c[1] == from_system and c[2] != None: 119 | return [c[2]] + self.get_parents(c[2]) 120 | return [] 121 | 122 | def fill_out(self, transformation): 123 | return transformation(**{k:v for k,v in self.__dict__.iteritems() if k in transformation.func_code.co_varnames}) 124 | 125 | def get_transformation(self, from_system, to_system): 126 | M = np.eye(4) 127 | parents_of_from = self.get_parents(from_system) 128 | if to_system in parents_of_from: 129 | for c in [from_system] + parents_of_from: 130 | transform = [ct[3] for ct in coordinate_systems if ct[1] == c][0] 131 | M = np.linalg.inv(self.fill_out(c[3])) * M 132 | return M 133 | parents_of_to = self.get_parents(to_system) 134 | 135 | 136 | class TableTop(object): 137 | def __init__(self, psi_t=0, phi_t=0, Tx=0, Ty=0, Tz=0): 138 | self.psi_t = psi_t 139 | self.phi_t = phi_t 140 | self.Tx = Tx 141 | self.Ty = Ty 142 | self.Tz = Tz 143 | 144 | class TableTopEcc(object): 145 | def __init__(self, Ls=0, theta_e=0): 146 | self.Ls = Ls 147 | self.theta_e = theta_e 148 | 149 | def transform3d(v3, m): 150 | v3 = np.atleast_2d(v3) 151 | if v3.shape == (1,3): 152 | v3=v3.T 153 | assert v3.shape[0] == 3 154 | return np.array((m * np.vstack((v3, np.ones((1,v3.shape[1])))))[:3,:]) 155 | 156 | coordinate_systems = [ 157 | ("Fixed", "f", None, None), 158 | ("Gantry", "g", "f", Mfg), 159 | ("Beam limiting device or delineator", "b", "g", Mgb), 160 | ("Wedge filter", "w", "b", Mbw), 161 | ("X-ray image receptor", "r", "g", Mgr), 162 | ("Patient support", "s", "f", Mfs), 163 | ("Table top eccentric rotation", "e", "s", Mse), 164 | ("Table top", "t", "e", Met), 165 | ("Patient", "p", "t", Mtp), 166 | ("DICOM Patient", "d", "p", Mpd) # Non-standard 167 | ] 168 | 169 | -------------------------------------------------------------------------------- /doseData.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import numpy 3 | try: 4 | shutil.rmtree("orientationtests") 5 | except: 6 | pass 7 | import builders 8 | reload(builders) 9 | import modules 10 | reload(modules) 11 | from builders import StudyBuilder 12 | 13 | import os 14 | if not os.path.exists("orientationtests"): 15 | os.mkdir("orientationtests") 16 | 17 | def build_orientation(patient_position, column_direction, row_direction, frame_of_reference_uid = None): 18 | sb = StudyBuilder(patient_position=patient_position, patient_id="OrientationTests", patient_name="Orientation^Tests", patient_birthdate = "20121212") 19 | if frame_of_reference_uid != None: 20 | sb.current_study['FrameOfReferenceUID'] = frame_of_reference_uid 21 | 22 | print "building %s..." % (patient_position,) 23 | print "ct" 24 | ct = sb.build_ct( 25 | num_voxels=[7, 7, 7], 26 | voxel_size=[4, 4, 4], 27 | pixel_representation=0, 28 | rescale_slope=1, 29 | rescale_intercept=-1024, 30 | row_direction=row_direction, 31 | column_direction=column_direction) 32 | ct.clear(real_value = -1000) 33 | ct.add_box(size = [4,4,4], center = [0,0,0], real_value = 0) 34 | ct.add_box(size = [20,4,4], center = [0,-8,-8], real_value = 0) 35 | ct.add_box(size = [4,20,4], center = [8,0,-8], real_value = 0) 36 | ct.add_box(size = [4,4,20], center = [8,8,0], real_value = 0) 37 | ct.add_sphere(radius = 4, center = [-8,-8,-8], real_value = 0) 38 | 39 | print "rtstruct" 40 | rtstruct = sb.build_structure_set(ct) 41 | rtstruct.add_external_box() 42 | rtstruct.add_box(size = [4,4,4], center = [0,0,0], name='CenterVoxel', interpreted_type='SITE') 43 | rtstruct.add_box(size = [20,4,4], center = [0,-8,-8], name='x=-8 to 8, y=z=-8', interpreted_type='SITE') 44 | rtstruct.add_box(size = [4,20,4], center = [8,0,-8], name='y=-8 to 8 x=8, z=-8', interpreted_type='SITE') 45 | rtstruct.add_box(size = [4,4,20], center = [8,8,0], name='z=-8 to 8, x=y=8', interpreted_type='SITE') 46 | rtstruct.add_sphere(radius=4, center = [-8,-8,-8], name='x=y=z=-8', interpreted_type='SITE') 47 | rtstruct.build() 48 | 49 | print "rtplan" 50 | rtplan = sb.build_static_plan(structure_set = rtstruct, sad=20) 51 | b1 = rtplan.build_beam(gantry_angle = 0, collimator_angle=30, meterset = 100) 52 | b1.conform_to_rectangle(4, 4, [0,0]) 53 | b2 = rtplan.build_beam(gantry_angle = 120, meterset = 100) 54 | b2.conform_to_rectangle(4, 4, [4,4]) 55 | rtplan.build() 56 | 57 | print "rtdose beam 1" 58 | rtdose1 = sb.build_dose(planbuilder = rtplan) 59 | rtdose1.dose_grid_scaling = 1 60 | rtdose1.dose_summation_type = "BEAM" 61 | rtdose1.beam_number = 1 62 | 63 | rtdose1.add_box(size = [4,4,4], center = [0,-12, 0], stored_value = 65535) #100% 64 | rtdose1.add_box(size = [4,4,4], center = [0, -8, 0], stored_value = 39321) #60% 65 | rtdose1.add_box(size = [4,4,4], center = [0, -4, 0], stored_value = 38665) #59% 66 | rtdose1.add_box(size = [4,4,4], center = [0, 0, 0], stored_value = 32767) #50% 67 | rtdose1.add_box(size = [4,4,4], center = [0, 4, 0], stored_value = 32112) #49% 68 | rtdose1.add_box(size = [4,4,4], center = [0, 8, 0], stored_value = 15728) #24% 69 | rtdose1.add_box(size = [4,4,4], center = [0, 12, 0], stored_value = 0) #0% 70 | 71 | ############# second beam 72 | print "rtdose beam 2" 73 | rtdose2 = sb.build_dose(planbuilder = rtplan) 74 | rtdose2.dose_grid_scaling = 1 75 | rtdose2.dose_summation_type = "BEAM" 76 | rtdose2.beam_number = 2 77 | 78 | rtdose2.add_box(size = [4,4,4], center = [-12,0, 0], stored_value = 65535) # 100% 79 | rtdose2.add_box(size = [4,4,4], center = [-8, 0, 0], stored_value = 62259) # 95% 80 | rtdose2.add_box(size = [4,4,4], center = [-4, 0, 0], stored_value = 61602) # 94% 81 | rtdose2.add_box(size = [4,4,4], center = [0, 0, 0], stored_value = 32767) # 50% 82 | rtdose2.add_box(size = [4,4,4], center = [4, 0, 0], stored_value = 52428) # 80% 83 | rtdose2.add_box(size = [4,4,4], center = [8, 0, 0], stored_value = 51772) # 79% 84 | rtdose2.add_box(size = [4,4,4], center = [12, 0, 0], stored_value = 15728) # 24% 85 | 86 | ############# second plan 87 | print "rtdose plan dose" 88 | rtdosePlan = sb.build_dose(planbuilder = rtplan) 89 | rtdosePlan.dose_grid_scaling = 1 90 | rtdosePlan.dose_summation_type = "PLAN" 91 | rtdosePlan.beam_number = None 92 | 93 | rtdosePlan.pixel_array = numpy.add(rtdose2.pixel_array, rtdose1.pixel_array) 94 | 95 | return sb 96 | 97 | ##orientations = [([1,0,0], [0,1,0]),([-1,0,0], [0,-1,0]),([-1,0,0, [0,1,0]]),([1,0,0], [0,-1,0])] 98 | ##patientpositions = ['HFS','HFP','FFS','FFP','HFDR', 'HFDL', 'FFDR', 'FFDL'] 99 | orientations = [([1,0,0], [0,1,0])] 100 | patientpositions = ['HFS'] 101 | 102 | sbs = [] 103 | FoR = None 104 | for o in orientations: 105 | for p in patientpositions: 106 | sb = build_orientation(p, o[0], o[1], FoR) 107 | sbs.append(sb) 108 | FoR = sbs[0].current_study['FrameOfReferenceUID'] 109 | d = "orientationtests/" + p + "/" + "%s%s%s%s%s%s" % tuple(x for y in o for x in y) 110 | os.makedirs(d) 111 | sb.write(d) 112 | -------------------------------------------------------------------------------- /example.ps1: -------------------------------------------------------------------------------- 1 | C:\Anaconda\python.exe build_dicom.py --outdir example1 ` 2 | --patient-position HFS --values 0 --patients-name "Kalle Kula" ` 3 | --values "sphere,924,25,[50;86.6;0]" --values "box,1124,25,[50;-86.6;0]" ` 4 | --voxelsize "4,3,4" --voxels "48,64,48" --modality CT ` 5 | --structure external ` 6 | --structure "sphere,Ball,25,CAVITY,[50;86.6;0]" ` 7 | --structure "box,Cube,25,CAVITY,[50;-86.6;0]" --modality RTSTRUCT ` 8 | --beams "[3;123;270]" ` 9 | --mlc-direction MLCX ` 10 | --mlc-shape "1,circle,30" --jaw-shape "1,[60;60]" ` 11 | --mlc-shape "2,rectangle,60,60,[0;40]" --jaw-shape "2,[70;70;10;10]" ` 12 | --mlc-shape "3,rectangle,40,80" --jaw-shape "3,[40;80]" ` 13 | --nominal-energy 6 --modality RTPLAN ` 14 | --values 0 --values lightfield --modality RTDOSE 15 | -------------------------------------------------------------------------------- /generate_import_test_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import builders 5 | reload(builders) 6 | import modules 7 | reload(modules) 8 | from builders import StudyBuilder 9 | 10 | import argparse 11 | import os 12 | import subprocess 13 | import sys 14 | 15 | 16 | def mk_fresh_dir(dir_path): 17 | if not os.path.exists(dir_path): 18 | os.mkdir(dir_path) 19 | else: 20 | for root, dirs, files in os.walk(dir_path, topdown=False): 21 | for dcm_file in [f for f in files if f.endswith(".dcm")]: 22 | os.remove(os.path.join(root, dcm_file)) 23 | 24 | 25 | def get_pixel_representation(s='signed'): 26 | assert s == 'signed' or s == 'unsigned' 27 | if s == 'signed': 28 | return 1 29 | elif s == 'unsigned': 30 | return 0 31 | 32 | 33 | def generate_mr_unsigned_in_short_range(out_dir, patient_id): 34 | sb = StudyBuilder(patient_position="HFS", 35 | patient_id=patient_id, 36 | patients_name="MrUnsigned^InShortRange", 37 | patients_birthdate="20121212") 38 | ct = sb.build_mr(num_voxels=[48, 64, 10], 39 | voxel_size=[4, 3, 4], # [mm] 40 | pixel_representation=get_pixel_representation('unsigned')) 41 | ct.clear(stored_value=0) 42 | ct.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=12345, mode='set') 43 | 44 | # Smallest stored value is: 0 (in short range) 45 | # Largest stored value is: 12345 (in short range). 46 | 47 | mk_fresh_dir(out_dir) 48 | sb.write(out_dir) 49 | print out_dir 50 | 51 | 52 | def generate_mr_unsigned_not_in_short_range(out_dir, patient_id): 53 | sb = StudyBuilder(patient_position="HFS", 54 | patient_id=patient_id, 55 | patients_name="MrUnsigned^NotInShortRange", 56 | patients_birthdate="20121212") 57 | ct = sb.build_mr(num_voxels=[48, 64, 10], 58 | voxel_size=[4, 3, 4], # [mm] 59 | pixel_representation=get_pixel_representation('unsigned')) 60 | ct.clear(stored_value=0) 61 | ct.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=54321, mode='set') 62 | 63 | # Smallest stored value is: 0 (in short range) 64 | # Largest stored value is: 54321 (greater than 32767). 65 | 66 | mk_fresh_dir(out_dir) 67 | sb.write(out_dir) 68 | print out_dir 69 | 70 | 71 | def generate_mr_signed_in_short_range(out_dir, patient_id): 72 | sb = StudyBuilder(patient_position="HFS", 73 | patient_id=patient_id, 74 | patients_name="MrSigned^InShortRange", 75 | patients_birthdate="20121212") 76 | ct = sb.build_mr(num_voxels=[48, 64, 10], 77 | voxel_size=[4, 3, 4], # [mm] 78 | pixel_representation=get_pixel_representation('signed')) 79 | ct.clear(stored_value=0) 80 | ct.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=12345, mode='set') 81 | 82 | # Smallest stored value is: 0 (in short range) 83 | # Largest stored value is: 12345 (in short range). 84 | 85 | mk_fresh_dir(out_dir) 86 | sb.write(out_dir) 87 | print out_dir 88 | 89 | 90 | def generate_ct_unsigned_rescaled_in_short_range(out_dir, patient_id): 91 | sb = StudyBuilder(patient_position="HFS", 92 | patient_id=patient_id, 93 | patients_name="CtUnsigned^InShortRange", 94 | patients_birthdate="20121212") 95 | ct = sb.build_ct(num_voxels=[48, 64, 10], 96 | voxel_size=[4, 3, 4], # [mm] 97 | pixel_representation=get_pixel_representation('unsigned'), 98 | rescale_slope=1, 99 | rescale_intercept=-1024) 100 | ct.clear(stored_value=0) 101 | ct.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=12345, mode='set') 102 | 103 | # Smallest rescaled value is: 1 * 0 - 1024 = -1024 (in short range) 104 | # Largest rescaled value is: 1 * 54321 - 1024 = 11321 (in short range). 105 | 106 | mk_fresh_dir(out_dir) 107 | sb.write(out_dir) 108 | print out_dir 109 | 110 | 111 | def generate_ct_unsigned_rescaled_not_in_short_range(out_dir, patient_id): 112 | sb = StudyBuilder(patient_position="HFS", 113 | patient_id=patient_id, 114 | patients_name="CtUnsigned^NotInShortRange", 115 | patients_birthdate="20121212") 116 | ct = sb.build_ct(num_voxels=[48, 64, 10], 117 | voxel_size=[4, 3, 4], # [mm] 118 | pixel_representation=get_pixel_representation('unsigned'), 119 | rescale_slope=1, 120 | rescale_intercept=-1024) 121 | ct.clear(stored_value=0) 122 | ct.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=54321, mode='set') 123 | 124 | # Smallest rescaled value is: 1 * 0 - 1024 = -1024 (in short range) 125 | # Largest rescaled value is: 1 * 54321 - 1024 = 53297 (greater than 32767). 126 | 127 | mk_fresh_dir(out_dir) 128 | sb.write(out_dir) 129 | print out_dir 130 | 131 | 132 | def generate_ct_signed_rescaled_in_short_range(out_dir, patient_id): 133 | sb = StudyBuilder(patient_position="HFS", 134 | patient_id=patient_id, 135 | patients_name="CtSigned^InShortRange", 136 | patients_birthdate="20121212") 137 | ct = sb.build_ct(num_voxels=[48, 64, 10], 138 | voxel_size=[4, 3, 4], # [mm] 139 | pixel_representation=get_pixel_representation('signed'), 140 | rescale_slope=1, 141 | rescale_intercept=-1024) 142 | ct.clear(stored_value=0) 143 | ct.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=12345, mode='set') 144 | 145 | # Smallest rescaled value is: 1 * 0 - 1024 = -1024 (in short range) 146 | # Largest rescaled value is: 1 * 12345 - 1024 = 11321 (in short range). 147 | 148 | mk_fresh_dir(out_dir) 149 | sb.write(out_dir) 150 | print out_dir 151 | 152 | 153 | def generate_ct_signed_rescaled_not_in_short_range(out_dir, patient_id): 154 | sb = StudyBuilder(patient_position="HFS", 155 | patient_id=patient_id, 156 | patients_name="CtSigned^RescaledNotInShortRange", 157 | patients_birthdate="20121212") 158 | ct = sb.build_ct(num_voxels=[48, 64, 10], 159 | voxel_size=[4, 3, 4], # [mm] 160 | pixel_representation=get_pixel_representation('signed'), 161 | rescale_slope=5, 162 | rescale_intercept=-1024) 163 | ct.clear(stored_value=0) 164 | ct.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=12345, mode='set') 165 | 166 | # Smallest rescaled value is: 5 * 0 - 1024 = -1024 (in short range) 167 | # Largest rescaled value is: 5 * 12345 - 1024 = 60701 (greater than 32767). 168 | 169 | mk_fresh_dir(out_dir) 170 | sb.write(out_dir) 171 | print out_dir 172 | 173 | 174 | def generate_pt_unsigned_in_short_range(out_dir, patient_id): 175 | sb = StudyBuilder(patient_position="HFS", 176 | patient_id=patient_id, 177 | patients_name="PtUnsigned^InShortRange", 178 | patients_birthdate="20121212") 179 | pt = sb.build_pt(num_voxels=[48, 64, 10], 180 | voxel_size=[4, 3, 4], # [mm] 181 | pixel_representation=get_pixel_representation('unsigned'), 182 | rescale_slope=1) 183 | pt.clear(stored_value=0) 184 | pt.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=12345, mode='set') 185 | 186 | # Smallest stored value is: 0 (in short range) 187 | # Largest stored value is: 12345 (in short range). 188 | 189 | mk_fresh_dir(out_dir) 190 | sb.write(out_dir) 191 | print out_dir 192 | 193 | 194 | def generate_pt_unsigned_not_in_short_range(out_dir, patient_id): 195 | sb = StudyBuilder(patient_position="HFS", 196 | patient_id=patient_id, 197 | patients_name="PtUnsigned^NotInShortRange", 198 | patients_birthdate="20121212") 199 | pt = sb.build_pt(num_voxels=[48, 64, 10], 200 | voxel_size=[4, 3, 4], # [mm] 201 | pixel_representation=get_pixel_representation('unsigned'), 202 | rescale_slope=1) 203 | pt.clear(stored_value=0) 204 | pt.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=54321, mode='set') 205 | 206 | # Smallest stored value is: 0 (in short range) 207 | # Largest stored value is: 54321 (greater than 32767). 208 | 209 | mk_fresh_dir(out_dir) 210 | sb.write(out_dir) 211 | print out_dir 212 | 213 | 214 | def generate_pt_signed_in_short_range(out_dir, patient_id): 215 | sb = StudyBuilder(patient_position="HFS", 216 | patient_id=patient_id, 217 | patients_name="PtSigned^InShortRange", 218 | patients_birthdate="20121212") 219 | pt = sb.build_pt(num_voxels=[48, 64, 10], 220 | voxel_size=[4, 3, 4], # [mm] 221 | pixel_representation=get_pixel_representation('unsigned'), 222 | rescale_slope=1) 223 | pt.clear(stored_value=0) 224 | pt.add_box(size=[25, 50, 5], center=[0, 0, 0], stored_value=12345, mode='set') 225 | 226 | # Smallest stored value is: 0 (in short range) 227 | # Largest stored value is: 12345 (in short range). 228 | 229 | mk_fresh_dir(out_dir) 230 | sb.write(out_dir) 231 | print out_dir 232 | 233 | 234 | def parse_args(argv): 235 | parser = argparse.ArgumentParser(description='Create DICOM data for import testing.') 236 | parser.add_argument('--dciodvfy-path', dest='dciodvfy_path', default='', 237 | help='Path to dciodvfy.exe (example: C:\Users\tomhin\dicom3tools\dciodvfy.exe)') 238 | args = parser.parse_args(args=argv) 239 | return args 240 | 241 | 242 | def verify(dciodvfy_path, input_path): 243 | if dciodvfy_path == '': 244 | return 245 | files = [f for f in os.listdir(input_path) if os.path.isfile(os.path.join(input_path, f))] 246 | assert len(files) >= 1 247 | try: 248 | print subprocess.check_output([dciodvfy_path, os.path.join(input_path, files[0])]) 249 | except subprocess.CalledProcessError as e: 250 | print "Error code: {}".format(e.returncode) 251 | print "Output: {}".format(e.output) 252 | 253 | 254 | def main(argv): 255 | args = parse_args(argv) 256 | 257 | # MR unsigned 258 | generate_mr_unsigned_in_short_range( 259 | out_dir='./mr_unsigned_in_short_range', 260 | patient_id='1233321') 261 | verify(args.dciodvfy_path, './mr_unsigned_in_short_range') 262 | generate_mr_unsigned_not_in_short_range( 263 | out_dir='./mr_unsigned_not_in_short_range', 264 | patient_id='1234321') 265 | verify(args.dciodvfy_path, './mr_unsigned_not_in_short_range') 266 | 267 | # MR signed 268 | generate_mr_signed_in_short_range( 269 | out_dir='./mr_signed_in_short_range', 270 | patient_id='1235321') 271 | verify(args.dciodvfy_path, './mr_signed_in_short_range') 272 | 273 | # CT unsigned 274 | generate_ct_unsigned_rescaled_in_short_range( 275 | out_dir='./ct_unsigned_rescaled_in_short_range', 276 | patient_id='1236321') 277 | verify(args.dciodvfy_path, './ct_unsigned_rescaled_in_short_range') 278 | generate_ct_unsigned_rescaled_not_in_short_range( 279 | out_dir='./ct_unsigned_rescaled_not_in_short_range', 280 | patient_id='1237321') 281 | verify(args.dciodvfy_path, './ct_unsigned_rescaled_not_in_short_range') 282 | 283 | # CT signed 284 | generate_ct_signed_rescaled_in_short_range( 285 | out_dir='./ct_signed_rescaled_in_short_range', 286 | patient_id='1238321') 287 | verify(args.dciodvfy_path, './ct_signed_rescaled_in_short_range') 288 | generate_ct_signed_rescaled_not_in_short_range( 289 | out_dir='./ct_signed_rescaled_not_in_short_range', 290 | patient_id='1239321') 291 | verify(args.dciodvfy_path, './ct_signed_rescaled_not_in_short_range') 292 | 293 | # PET unsigned 294 | generate_pt_unsigned_in_short_range( 295 | out_dir='./pt_unsigned_in_short_range', 296 | patient_id='1244421') 297 | verify(args.dciodvfy_path, './pt_unsigned_in_short_range') 298 | generate_pt_unsigned_not_in_short_range( 299 | out_dir='./pt_unsigned_not_in_short_range', 300 | patient_id='1245421') 301 | verify(args.dciodvfy_path, './pt_unsigned_not_in_short_range') 302 | 303 | # PET signed 304 | generate_pt_signed_in_short_range( 305 | out_dir='./pt_signed_in_short_range', 306 | patient_id='1246421') 307 | verify(args.dciodvfy_path, './pt_signed_in_short_range') 308 | 309 | 310 | if __name__ == "__main__": 311 | main(sys.argv[1:]) 312 | -------------------------------------------------------------------------------- /modules.py: -------------------------------------------------------------------------------- 1 | import uuid, datetime 2 | import pydicom as dicom 3 | dicom.config.use_DS_decimal = False 4 | dicom.config.allow_DS_float = True 5 | import numpy as np 6 | import coordinates 7 | 8 | from pydicom._uid_dict import UID_dictionary 9 | 10 | # Be careful to pass good fp numbers... 11 | if hasattr(dicom, 'config'): 12 | dicom.config.allow_DS_float = True 13 | 14 | 15 | def get_uid(name): 16 | # print("{" + "\n".join("{}: {}".format(k, v) for k, v in dicom.UID.UID_dictionary.items()) + "}") 17 | return [k for k, v in UID_dictionary.iteritems() if v[0] == name][0] 18 | 19 | 20 | def generate_uid(_uuid=None): 21 | """Returns a new DICOM UID based on a UUID, as specified in CP1156 (Final).""" 22 | if _uuid is None: 23 | _uuid = uuid.uuid1() 24 | return "2.25.%i" % _uuid.int 25 | 26 | 27 | def get_current_study_uid(prop, current_study): 28 | if prop not in current_study: 29 | current_study[prop] = generate_uid() 30 | return current_study[prop] 31 | 32 | ImplementationClassUID = '2.25.229451600072090404564544894284998027172' 33 | 34 | 35 | def get_empty_dataset(filename, storagesopclass, sopinstanceuid): 36 | file_meta = dicom.dataset.Dataset() 37 | file_meta.MediaStorageSOPClassUID = get_uid(storagesopclass) 38 | file_meta.MediaStorageSOPInstanceUID = sopinstanceuid 39 | file_meta.ImplementationClassUID = ImplementationClassUID 40 | file_meta.TransferSyntaxUID = dicom.uid.ImplicitVRLittleEndian 41 | ds = dicom.dataset.FileDataset(filename, {}, file_meta=file_meta, preamble="\0"*128) 42 | return ds 43 | 44 | 45 | def get_default_ct_dataset(sopinstanceuid, current_study, pixel_representation, rescale_slope, rescale_intercept): 46 | if 'StudyTime' not in current_study: 47 | current_study['StudyTime'] = "%02i%02i%02i" % datetime.datetime.now().timetuple()[3:6] 48 | if 'StudyDate' not in current_study: 49 | current_study['StudyDate'] = "%04i%02i%02i" % datetime.datetime.now().timetuple()[:3] 50 | dt = current_study['StudyDate'] 51 | tm = current_study['StudyTime'] 52 | filename = "CT_%s.dcm" % (sopinstanceuid,) 53 | ds = get_empty_dataset(filename, "CT Image Storage", sopinstanceuid) 54 | get_sop_common_module(ds, dt, tm, "CT Image Storage", sopinstanceuid) 55 | get_ct_image_module(ds, rescale_slope=rescale_slope, rescale_intercept=rescale_intercept) 56 | get_image_pixel_macro(ds, pixel_representation) 57 | get_patient_module(ds, current_study) 58 | get_general_study_module(ds, current_study) 59 | get_general_series_module(ds, dt, tm, "CT") 60 | get_frame_of_reference_module(ds) 61 | get_general_equipment_module(ds) 62 | get_general_image_module(ds, dt, tm) 63 | get_image_plane_module(ds) 64 | return ds 65 | 66 | 67 | def get_default_mr_dataset(sopinstanceuid, current_study, pixel_representation): 68 | if 'StudyTime' not in current_study: 69 | current_study['StudyTime'] = "%02i%02i%02i" % datetime.datetime.now().timetuple()[3:6] 70 | if 'StudyDate' not in current_study: 71 | current_study['StudyDate'] = "%04i%02i%02i" % datetime.datetime.now().timetuple()[:3] 72 | dt = current_study['StudyDate'] 73 | tm = current_study['StudyTime'] 74 | filename = "MR_%s.dcm" % (sopinstanceuid,) 75 | ds = get_empty_dataset(filename, "MR Image Storage", sopinstanceuid) 76 | get_sop_common_module(ds, dt, tm, "MR Image Storage", sopinstanceuid) 77 | get_mr_image_module(ds) 78 | get_image_pixel_macro(ds, pixel_representation) 79 | get_patient_module(ds, current_study) 80 | get_general_study_module(ds, current_study) 81 | get_general_series_module(ds, dt, tm, "MR") 82 | get_frame_of_reference_module(ds) 83 | get_general_equipment_module(ds) 84 | get_general_image_module(ds, dt, tm) 85 | get_image_plane_module(ds) 86 | return ds 87 | 88 | 89 | def get_default_pt_dataset( 90 | sopinstanceuid, 91 | current_study, 92 | image_index, 93 | number_of_slices, 94 | pixel_representation, 95 | rescale_slope): 96 | if 'StudyTime' not in current_study: 97 | current_study['StudyTime'] = "%02i%02i%02i" % datetime.datetime.now().timetuple()[3:6] 98 | if 'StudyDate' not in current_study: 99 | current_study['StudyDate'] = "%04i%02i%02i" % datetime.datetime.now().timetuple()[:3] 100 | dt = current_study['StudyDate'] 101 | tm = current_study['StudyTime'] 102 | filename = "PT_%s.dcm" % (sopinstanceuid,) 103 | ds = get_empty_dataset(filename, "Positron Emission Tomography Image Storage", sopinstanceuid) 104 | get_sop_common_module(ds, dt, tm, "Positron Emission Tomography Image Storage", sopinstanceuid) 105 | get_pet_image_module(ds, image_index, rescale_slope=rescale_slope) 106 | get_image_pixel_macro(ds, pixel_representation) 107 | get_patient_module(ds, current_study) 108 | get_general_study_module(ds, current_study) 109 | get_general_series_module(ds, dt, tm, "PT") 110 | get_pet_series_module(ds, number_of_slices) 111 | get_pet_isotope_module(ds) 112 | get_nmpet_patient_orientation_module(ds) 113 | get_frame_of_reference_module(ds) 114 | get_general_equipment_module(ds) 115 | get_general_image_module(ds, dt, tm) 116 | get_image_plane_module(ds) 117 | return ds 118 | 119 | 120 | def get_default_rt_dose_dataset(current_study, rtplan, dose_summation_type, beam_number): 121 | DT = "%04i%02i%02i" % datetime.datetime.now().timetuple()[:3] 122 | TM = "%02i%02i%02i" % datetime.datetime.now().timetuple()[3:6] 123 | if 'StudyTime' not in current_study: 124 | current_study['StudyTime'] = TM 125 | if 'StudyDate' not in current_study: 126 | current_study['StudyDate'] = DT 127 | sopinstanceuid = generate_uid() 128 | filename = "RTDOSE_%s.dcm" % (sopinstanceuid,) 129 | ds = get_empty_dataset(filename, "RT Dose Storage", sopinstanceuid) 130 | get_sop_common_module(ds, DT, TM, "RT Dose Storage", sopinstanceuid) 131 | get_patient_module(ds, current_study) 132 | pixel_representation = 0 133 | get_image_pixel_macro(ds, pixel_representation) 134 | get_general_study_module(ds, current_study) 135 | get_rt_series_module(ds, DT, TM, "RTDOSE") 136 | get_frame_of_reference_module(ds) 137 | get_general_equipment_module(ds) 138 | get_general_image_module(ds, DT, TM) 139 | get_image_plane_module(ds) 140 | get_multi_frame_module(ds) 141 | get_rt_dose_module(ds, rtplan, dose_summation_type, beam_number) 142 | return ds 143 | 144 | def get_default_rt_structure_set_dataset(ref_images, current_study): 145 | DT = "%04i%02i%02i" % datetime.datetime.now().timetuple()[:3] 146 | TM = "%02i%02i%02i" % datetime.datetime.now().timetuple()[3:6] 147 | if 'StudyTime' not in current_study: 148 | current_study['StudyTime'] = TM 149 | if 'StudyDate' not in current_study: 150 | current_study['StudyDate'] = DT 151 | sopinstanceuid = generate_uid() 152 | filename = "RTSTRUCT_%s.dcm" % (sopinstanceuid,) 153 | ds = get_empty_dataset(filename, "RT Structure Set Storage", sopinstanceuid) 154 | get_sop_common_module(ds, DT, TM, "RT Structure Set Storage", sopinstanceuid) 155 | get_patient_module(ds, current_study) 156 | get_general_study_module(ds, current_study) 157 | get_rt_series_module(ds, DT, TM, "RTSTRUCT") 158 | get_general_equipment_module(ds) 159 | get_structure_set_module(ds, DT, TM, ref_images, current_study) 160 | get_roi_contour_module(ds) 161 | get_rt_roi_observations_module(ds) 162 | return ds 163 | 164 | def get_default_rt_plan_dataset(current_study, isocenter, structure_set=None): 165 | DT = "%04i%02i%02i" % datetime.datetime.now().timetuple()[:3] 166 | TM = "%02i%02i%02i" % datetime.datetime.now().timetuple()[3:6] 167 | if 'StudyTime' not in current_study: 168 | current_study['StudyTime'] = TM 169 | if 'StudyDate' not in current_study: 170 | current_study['StudyDate'] = DT 171 | sopinstanceuid = generate_uid() 172 | filename = "RTPLAN_%s.dcm" % (sopinstanceuid,) 173 | ds = get_empty_dataset(filename, "RT Plan Storage", sopinstanceuid) 174 | get_sop_common_module(ds, DT, TM, "RT Plan Storage", sopinstanceuid) 175 | get_patient_module(ds, current_study) 176 | get_general_study_module(ds, current_study) 177 | get_rt_series_module(ds, DT, TM, "RTPLAN") 178 | get_frame_of_reference_module(ds) 179 | get_general_equipment_module(ds) 180 | get_rt_general_plan_module(ds, DT, TM, structure_set) 181 | #get_rt_prescription_module(ds) 182 | #get_rt_tolerance_tables(ds) 183 | if 'PatientPosition' in current_study: 184 | get_rt_patient_setup_module(ds, current_study) 185 | get_rt_beams_module(ds, isocenter, current_study) 186 | get_rt_fraction_scheme_module(ds, 30) 187 | #get_approval_module(ds) 188 | return ds 189 | 190 | def get_default_rt_ion_plan_dataset(current_study, numbeams, collimator_angles, patient_support_angles, table_top, table_top_eccentric, isocenter): 191 | """Not done, in development""" 192 | DT = "%04i%02i%02i" % datetime.datetime.now().timetuple()[:3] 193 | TM = "%02i%02i%02i" % datetime.datetime.now().timetuple()[3:6] 194 | if 'StudyTime' not in current_study: 195 | current_study['StudyTime'] = TM 196 | if 'StudyDate' not in current_study: 197 | current_study['StudyDate'] = DT 198 | sopinstanceuid = generate_uid() 199 | filename = "RTPLAN_%s.dcm" % (sopinstanceuid,) 200 | ds = get_empty_dataset(filename, "RT Plan Storage", sopinstanceuid) 201 | get_sop_common_module(ds, DT, TM, "RT Plan Storage", sopinstanceuid) 202 | get_patient_module(ds, current_study) 203 | get_general_study_module(ds, current_study) 204 | get_rt_series_module(ds, DT, TM, "RTIONPLAN") 205 | get_frame_of_reference_module(ds) 206 | get_general_equipment_module(ds) 207 | get_rt_general_plan_module(ds, DT, TM, current_study) 208 | #get_rt_prescription_module(ds) 209 | #get_rt_tolerance_tables(ds) 210 | if 'PatientPosition' in current_study: 211 | get_rt_patient_setup_module(ds, current_study) 212 | get_rt_ion_beams_module(ds, numbeams, collimator_angles, patient_support_angles, table_top, table_top_eccentric, isocenter, current_study) 213 | get_rt_fraction_scheme_module(ds, 30) 214 | #get_approval_module(ds) 215 | return ds 216 | 217 | 218 | def get_sop_common_module(ds, DT, TM, modality, sopinstanceuid): 219 | # Type 1 220 | ds.SOPClassUID = get_uid(modality) 221 | ds.SOPInstanceUID = sopinstanceuid 222 | # Type 3 223 | ds.InstanceCreationDate = DT 224 | ds.InstanceCreationTime = TM 225 | 226 | def get_ct_image_module(ds, rescale_slope=1.0, rescale_intercept=-1024.0): 227 | # Type 1 228 | ds.ImageType = "ORIGINAL\SECONDARY\AXIAL" 229 | ds.SamplesPerPixel = 1 230 | ds.PhotometricInterpretation = "MONOCHROME2" 231 | ds.BitsAllocated = 16 232 | ds.BitsStored = 16 233 | ds.HighBit = 15 234 | ds.RescaleIntercept = rescale_intercept 235 | ds.RescaleSlope = rescale_slope 236 | # Type 2 237 | ds.KVP = "" 238 | ds.AcquisitionNumber = "" 239 | 240 | 241 | def get_mr_image_module(ds): 242 | # Type 1 243 | ds.ImageType = "ORIGINAL\SECONDARY\OTHER" 244 | ds.SamplesPerPixel = 1 245 | ds.PhotometricInterpretation = "MONOCHROME2" 246 | ds.BitsAllocated = 16 247 | ds.BitsStored = 16 248 | ds.HighBit = 15 249 | ds.ScanningSequence = "RM" 250 | ds.SequenceVariant = "NONE" 251 | ds.ScanOptions = "PER" 252 | # Type 2 253 | ds.MRAcquisitionType = "2D" 254 | ds.EchoTime = 1 # [ms] 255 | ds.EchoTrainLength = 1 # ??? 256 | ds.RepetitionTime = 1 # [ms] 257 | 258 | 259 | def get_pet_image_module(ds, image_index, rescale_slope): 260 | """C.8.9.4""" 261 | # Type 1 262 | ds.ImageType = "ORIGINAL\PRIMARY" 263 | ds.SamplesPerPixel = 1 264 | ds.PhotometricInterpretation = "MONOCHROME2" 265 | ds.BitsAllocated = 16 266 | ds.BitsStored = 16 267 | ds.HighBit = 15 268 | ds.RescaleIntercept = 0 269 | ds.RescaleSlope = rescale_slope 270 | ds.FrameReferenceTime = '1696000' # (0054, 1300) 271 | ds.ImageIndex = image_index # (0054, 1330) 272 | 273 | # Type 1C 274 | ds.DecayFactor = 1.21051 # Decay Factor (0054, 1321) 275 | # ds.TriggerTime = 0 # Trigger Time (0018, 1060) 276 | # ds.FrameTime = 0 # Frame Time (0018, 1063) 277 | # Low R-R Value (0018, 1081) 278 | # High R-R Value (0018, 1082) 279 | ds.LossyImageCompression = '00' # Lossy Image Compression (0028, 2110) 280 | 281 | # Type 2 282 | ds.AcquisitionDate = '20121212' # Acquisition Date (0008,0022) 283 | ds.AcquisitionTime = '134804' # Acquisition Time (0008, 0032) 284 | ds.ActualFrameDuration = '24000' # Actual Frame Duration (0018,1242) 285 | 286 | 287 | def get_image_pixel_macro(ds, pixel_representation): 288 | # Type 1 289 | ds.Rows = 256 290 | ds.Columns = 256 291 | ds.PixelRepresentation = pixel_representation 292 | 293 | 294 | def get_patient_module(ds, current_study): 295 | # Type 2 296 | ds.PatientName = current_study['PatientName'] 297 | ds.PatientID = current_study['PatientID'] 298 | ds.PatientBirthDate = current_study['PatientBirthDate'] 299 | ds.PatientSex = "O" 300 | 301 | 302 | def get_general_study_module(ds, current_study): 303 | # Type 1 304 | ds.StudyInstanceUID = "" 305 | # Type 2 306 | ds.StudyDate = current_study['StudyDate'] 307 | ds.StudyTime = current_study['StudyTime'] 308 | ds.ReferringPhysicianName = "" 309 | ds.StudyID = "" 310 | ds.AccessionNumber = "" 311 | # Type 3 312 | #ds.StudyDescription = "" 313 | 314 | 315 | def get_general_series_module(ds, DT, TM, modality): 316 | # Type 1 317 | ds.Modality = modality 318 | ds.SeriesInstanceUID = "" 319 | # Type 2 320 | ds.SeriesNumber = "" 321 | # Type 2C on Modality in 322 | # ['CT', 'MR', 'Enhanced CT', 'Enhanced MR Image', 'Enhanced Color MR Image', 'MR Spectroscopy']. 323 | # May not be present if Patient Orientation Code Sequence is present. 324 | # ds.PatientPosition = "HFS" 325 | if modality is 'MR' or modality is 'PT': 326 | ds.Laterality = 'R' 327 | 328 | # Type 3 329 | ds.SeriesDate = DT 330 | ds.SeriesTime = TM 331 | # ds.SeriesDescription = "" 332 | 333 | 334 | def get_pet_series_module(ds, number_of_slices): 335 | # Type 1 336 | ds.Units = 'BQML' # Units (0054, 1001) 337 | ds.CountsSource = 'EMISSION' # Counts Source (0054, 1002) 338 | ds.SeriesType = 'STATIC\IMAGE' # Series Type (0054,1000) 339 | ds.NumberOfSlices = number_of_slices # Number of Slices (0054,0081) 340 | ds.DecayCorrection = 'START' 341 | # Type 2 342 | ds.CorrectedImage = 'DECY\ATTN\SCAT\DTIM\RAN\DCAL\NORM' # Corrected Image (0028,0051) 343 | ds.CollimatorType = 'NONE' # Collimator Type (0018,1181) 344 | 345 | 346 | def get_pet_isotope_module(ds): 347 | # Type 2 348 | # ds.RadiopharmaceuticalInformationSequence = # Radiopharmaceutical Information Sequence (0054,0016) 349 | pass 350 | 351 | 352 | def get_nmpet_patient_orientation_module(ds): 353 | # Type 2 354 | # ds.PatientOrientationCodeSequence = '???' # (0054,0410) 355 | # ds.PatientGantryRelationshipCodeSequence = '???' # (0054,0414) 356 | pass 357 | 358 | 359 | def get_rt_series_module(ds, DT, TM, modality): 360 | # Type 1 361 | ds.Modality = modality 362 | ds.SeriesInstanceUID = "" 363 | # Type 2 364 | ds.SeriesNumber = "" 365 | ds.OperatorsName = "" 366 | # ds.SeriesDescriptionCodeSequence = None 367 | # ds.ReferencedPerformedProcedureStepSequence = None 368 | # ds.RequestAttributesSequence = None 369 | # Performed Procedure Step Summary Macro... 370 | # ds.SeriesDescription = "" 371 | 372 | 373 | def get_frame_of_reference_module(ds): 374 | # Type 1 375 | ds.FrameOfReferenceUID = "" 376 | # Type 2 377 | ds.PositionReferenceIndicator = "" 378 | 379 | 380 | def get_general_equipment_module(ds): 381 | # Type 1 382 | ds.Manufacturer = "pydicom" 383 | # Type 3 384 | ds.ManufacturerModelName = "https://github.com/raysearchlabs/dicomutils" 385 | ds.SoftwareVersions = "PyDICOM %s" % (dicom.__version__,) 386 | 387 | 388 | def get_general_image_module(ds, DT, TM): 389 | # Type 2 390 | ds.InstanceNumber = "" 391 | # Type 3 392 | ds.AcquisitionDate = DT 393 | ds.AcquisitionTime = TM 394 | ds.ImagesInAcquisition = 1 395 | ds.DerivationDescription = "Generated from numpy" 396 | 397 | 398 | def get_image_plane_module(ds): 399 | # Type 1 400 | ds.PixelSpacing = [1.0, 1.0] 401 | ds.ImageOrientationPatient = [1.0, 0.0, 0.0, 402 | 0.0, 1.0, 0.0] 403 | ds.ImagePositionPatient = [0.0, 0.0, 0.0] 404 | # Type 2 405 | ds.SliceThickness = 1.0 406 | # Type 3 407 | # ds.SliceLocation = 0 408 | 409 | 410 | def get_multi_frame_module(ds): 411 | # Type 1 412 | ds.NumberOfFrames = 1 413 | ds.FrameIncrementPointer = dicom.datadict.Tag(dicom.datadict.tag_for_name("GridFrameOffsetVector")) 414 | 415 | 416 | def get_rt_dose_module(ds, rtplan=None, doseSummationType="PLAN", beam_number=None): 417 | # Type 1C on PixelData 418 | ds.SamplesPerPixel = 1 419 | ds.DoseGridScaling = 1.0 420 | ds.SamplesPerPixel = 1 421 | ds.PhotometricInterpretation = "MONOCHROME2" 422 | ds.BitsAllocated = 16 423 | ds.BitsStored = 16 424 | ds.HighBit = 15 425 | ds.PixelRepresentation = 0 426 | 427 | # Type 1 428 | ds.DoseUnits = "GY" 429 | ds.DoseType = "PHYSICAL" 430 | ds.DoseSummationType = doseSummationType 431 | 432 | # Type 1C if Dose Summation Type is any of the enumerated values. 433 | ds.ReferencedRTPlanSequence = [] 434 | if rtplan != None: 435 | refplan = dicom.dataset.Dataset() 436 | refplan.ReferencedSOPClassUID = get_uid("RT Plan Storage") 437 | refplan.ReferencedSOPInstanceUID = rtplan.SOPInstanceUID 438 | ds.ReferencedRTPlanSequence.append(refplan) 439 | 440 | # Type 1C on multi-frame 441 | ds.GridFrameOffsetVector = [0,1,2,3,4] 442 | 443 | # Type 1C 444 | if (ds.DoseSummationType == "FRACTION" or 445 | ds.DoseSummationType == "BEAM" or 446 | ds.DoseSummationType == "BRACHY" or 447 | ds.DoseSummationType == "CONTROL_POINT"): 448 | ds.ReferencedRTPlanSequence[0].ReferencedFractionGroupSequence = [dicom.dataset.Dataset()] 449 | # Type 1 450 | ds.ReferencedRTPlanSequence[0].ReferencedFractionGroupSequence[0].ReferencedFractionGroupNumber = 0 451 | # Type 1C 452 | if (ds.DoseSummationType == "BEAM"): 453 | referencedBeamSequence = dicom.dataset.Dataset() 454 | referencedBeamSequence.ReferencedBeamNumber = beam_number 455 | referencedBeamSequence.ReferencedFractionGroupNumber = 1 456 | ds.ReferencedRTPlanSequence[0].ReferencedFractionGroupSequence[0].ReferencedBeamSequence = [referencedBeamSequence]; 457 | 458 | if (ds.DoseSummationType == "CONTROL_POINT"): 459 | ds.ReferencedRTPlanSequence[0].ReferencedFractionGroupSequence[0].ReferencedBeamSequence = [dicom.dataset.Dataset()] 460 | # ... and on it goes... 461 | raise NotImplementedError 462 | elif ds.DoseSummationType == "BRACHY": 463 | raise NotImplementedError 464 | 465 | # Type 3 466 | # ds.InstanceNumber = 0 467 | # ds.DoseComment = "blabla" 468 | # ds.NormalizationPoint = [0,0,0] 469 | # ds.TissueHeterogeneityCorrection = "IMAGE" # or "ROI_OVERRIDE" or "WATER" 470 | 471 | 472 | def get_rt_general_plan_module(ds, DT, TM, structure_set=None, dose=None): 473 | # Type 1 474 | ds.RTPlanLabel = "Plan" 475 | if structure_set is None: 476 | ds.RTPlanGeometry = "TREATMENT_DEVICE" 477 | else: 478 | ds.RTPlanGeometry = "PATIENT" 479 | ds.ReferencedStructureSetSequence = [dicom.dataset.Dataset()] 480 | ds.ReferencedStructureSetSequence[0].ReferencedSOPClassUID = get_uid("RT Structure Set Storage") 481 | ds.ReferencedStructureSetSequence[0].ReferencedSOPInstanceUID = structure_set.SOPInstanceUID 482 | 483 | # Type 2 484 | ds.RTPlanDate = DT 485 | ds.RTPlanTime = TM 486 | 487 | # Type 3 488 | ds.RTPlanName = "PlanName" 489 | # ds.RTPlanDescription = "" 490 | # ds.InstanceNumber = 1 491 | # ds.TreatmentProtocols = "" 492 | ds.PlanIntent = "RESEARCH" 493 | # ds.TreatmentSties = "" 494 | if dose != None: 495 | ds.ReferencedDoseSequence = [dicom.dataset.Dataset()] 496 | ds.ReferencedDoseSequence[0].ReferencedSOPClassUID = get_uid("RT Dose Storage") 497 | ds.ReferencedDoseSequence[0].ReferencedSOPInstanceUID = dose.SOPInstanceUID 498 | # ds.ReferencedRTPlanSequence = [] 499 | 500 | 501 | def get_rt_fraction_scheme_module(ds, nfractions): 502 | ds.FractionGroupSequence = [dicom.dataset.Dataset()] # T1 503 | fg = ds.FractionGroupSequence[0] 504 | fg.FractionGroupNumber = 1 # T1 505 | fg.FractionGroupDescription = "Primary fraction group" # T3 506 | # fg.ReferencedDoseSequence = [] # T3 507 | # fg.ReferencedDoseReferenceSequence = [] # T3 508 | fg.NumberOfFractionsPlanned = nfractions # T2 509 | # fg.NumberOfFractionPatternDigitsPerDay # T3 510 | # fg.RepeatFractionCycleLength # T3 511 | # fg.FractionPattern # T3 512 | fg.NumberOfBeams = len(ds.BeamSequence) # T1 513 | fg.ReferencedBeamSequence = [] 514 | for beam in ds.BeamSequence: 515 | add_beam_to_rt_fraction_group(fg, beam) 516 | fg.NumberOfBrachyApplicationSetups = 0 517 | 518 | 519 | def add_beam_to_rt_fraction_group(fg, beam, beam_meterset): 520 | refbeam = dicom.dataset.Dataset() 521 | refbeam.ReferencedBeamNumber = beam.BeamNumber 522 | # refbeam.BeamDoseSpecificationPoint = [0,0,0] # T3 523 | # refbeam.BeamDose = 10 # T3 524 | # refbeam.BeamDosePointDepth # T3 525 | # refbeam.BeamDosePointEquivalentDepth # T3 526 | # refbeam.BeamDosePointSSD # T3 527 | refbeam.BeamMeterset = beam_meterset 528 | fg.NumberOfBeams += 1 529 | fg.ReferencedBeamSequence.append(refbeam) 530 | 531 | 532 | def cumsum(i): 533 | """Yields len(i)+1 values from 0 to sum(i)""" 534 | s = 0.0 535 | yield s 536 | for x in i: 537 | s += x 538 | yield s 539 | 540 | 541 | def get_rt_patient_setup_module(ds, current_study): 542 | ps = dicom.dataset.Dataset() 543 | ps.PatientSetupNumber = 1 544 | ps.PatientPosition = current_study['PatientPosition'] 545 | ds.PatientSetupSequence = [ps] 546 | return ps 547 | 548 | def get_rt_beams_module(ds, isocenter, current_study): 549 | """nleaves is a list [na, nb, nc, ...] and leafwidths is a list [wa, wb, wc, ...] 550 | so that there are na leaves with width wa followed by nb leaves with width wb etc.""" 551 | ds.BeamSequence = [] 552 | 553 | def get_dicom_to_bld_coordinate_transform(gantryAngle, gantryPitchAngle, beamLimitingDeviceAngle, patientSupportAngle, patientPosition, table_top, table_top_ecc, SAD, isocenter_d): 554 | if patientPosition == 'HFS': 555 | psi_p, phi_p, theta_p = 0,0,0 556 | elif patientPosition == 'HFP': 557 | psi_p, phi_p, theta_p = 0,180,0 558 | elif patientPosition == 'FFS': 559 | psi_p, phi_p, theta_p = 0,0,180 560 | elif patientPosition == 'FFP': 561 | psi_p, phi_p, theta_p = 180,0,0 562 | elif patientPosition == 'HFDL': 563 | psi_p, phi_p, theta_p = 0,90,0 564 | elif patientPosition == 'HFDR': 565 | psi_p, phi_p, theta_p = 0,270,0 566 | elif patientPosition == 'FFDL': 567 | psi_p, phi_p, theta_p = 180,270,0 568 | elif patientPosition == 'FFDR': 569 | psi_p, phi_p, theta_p = 180,90,0 570 | else: 571 | assert False, "Unknown patient position %s!" % (patientPosition,) 572 | 573 | # Find the isocenter in patient coordinate system, had the patient system not been translated 574 | isocenter_p0 = (coordinates.Mfs(patientSupportAngle) 575 | * coordinates.Mse(table_top_ecc.Ls, table_top_ecc.theta_e) 576 | * coordinates.Met(table_top.Tx, table_top.Ty, table_top.Tz, table_top.psi_t, table_top.phi_t) 577 | * coordinates.Mtp(0, 0, 0, psi_p, phi_p, theta_p)) * [[0],[0],[0],[1]] 578 | # Find the coordinates in the patient system of the desired isocenter 579 | isocenter_p1 = np.linalg.inv(coordinates.Mpd()) * np.array([float(isocenter_d[0]), float(isocenter_d[1]), float(isocenter_d[2]), 1.0]).reshape((4,1)) 580 | # Compute the patient coordinate system translation 581 | Px,Py,Pz,_ = isocenter_p0 - isocenter_p1 582 | 583 | M = (coordinates.Mgb(SAD, beamLimitingDeviceAngle) 584 | * coordinates.Mfg(gantryPitchAngle, gantryAngle) 585 | * np.linalg.inv(coordinates.Mfs(patientSupportAngle)) 586 | * np.linalg.inv(coordinates.Mse(table_top_ecc.Ls, table_top_ecc.theta_e)) 587 | * np.linalg.inv(coordinates.Met(table_top.Tx, table_top.Ty, table_top.Tz, table_top.psi_t, table_top.phi_t)) 588 | * np.linalg.inv(coordinates.Mtp(Px, Py, Pz, psi_p, phi_p, theta_p)) 589 | * np.linalg.inv(coordinates.Mpd())) 590 | return M 591 | 592 | from collections import defaultdict 593 | def getblds(blds): 594 | d = defaultdict(lambda: None) 595 | for bld in blds: 596 | if hasattr(bld, 'RTBeamLimitingDeviceType'): 597 | d[bld.RTBeamLimitingDeviceType] = bld 598 | return d 599 | 600 | from coordinates import TableTop, TableTopEcc 601 | 602 | def do_for_all_cps(beam, patient_position, func, *args, **kwargs): 603 | gantry_angle = None 604 | gantry_pitch_angle = 0 605 | isocenter = [0,0,0] 606 | beam_limiting_device_angle = 0 607 | table_top = TableTop() 608 | table_top_ecc = TableTopEcc() 609 | beam_limiting_device_positions = None 610 | 611 | patient_support_angle = 0 612 | if hasattr(beam, 'SourceAxisDistance'): 613 | sad = beam.SourceAxisDistance 614 | else: 615 | sad = 1000 616 | 617 | for cp in beam.ControlPointSequence: 618 | gantry_angle = getattr(cp, 'GantryAngle', gantry_angle) 619 | gantry_pitch_angle = getattr(cp, 'GantryPitchAngle', gantry_pitch_angle) 620 | beam_limiting_device_angle = getattr(cp, 'BeamLimitingDeviceAngle', beam_limiting_device_angle) 621 | patient_support_angle = getattr(cp, 'PatientSupportAngle', patient_support_angle) 622 | isocenter = getattr(cp, 'IsocenterPosition', isocenter) 623 | table_top_ecc.Ls = getattr(cp, 'TableTopEccentricAxisDistance', table_top_ecc.Ls) 624 | table_top_ecc.theta_e = getattr(cp, 'TableTopEccentricAngle', table_top_ecc.theta_e) 625 | table_top.psi_t = getattr(cp, 'TableTopPitchAngle', table_top.psi_t) 626 | table_top.phi_t = getattr(cp, 'TableTopRollAngle', table_top.phi_t) 627 | table_top.Tx = getattr(cp, 'TableTopLateralPosition', table_top.Tx) 628 | table_top.Ty = getattr(cp, 'TableTopLongitudinalPosition', table_top.Ty) 629 | table_top.Tz = getattr(cp, 'TableTopVerticalPosition', table_top.Tz) 630 | patient_position = getattr(cp, 'PatientPosition', patient_position) 631 | if hasattr(cp, 'BeamLimitingDevicePositionSequence') and cp.BeamLimitingDevicePositionSequence != None: 632 | beam_limiting_device_positions = getblds(cp.BeamLimitingDevicePositionSequence) 633 | func(cp, gantry_angle, gantry_pitch_angle, beam_limiting_device_angle, 634 | patient_support_angle, patient_position, 635 | table_top, table_top_ecc, sad, isocenter, beam_limiting_device_positions, 636 | *args, **kwargs) 637 | 638 | def nmin(it): 639 | n = None 640 | for i in it: 641 | if n is None or i < n: 642 | n = i 643 | return n 644 | 645 | def nmax(it): 646 | n = None 647 | for i in it: 648 | if n is None or i > n: 649 | n = i 650 | return n 651 | 652 | def get_mlc_and_jaw_directions(bld): 653 | if "MLCX" in bld: 654 | return "MLCX", "ASYMX", "ASYMY" 655 | elif "MLCY" in bld: 656 | return "MLCY", "ASYMY", "ASYMX" 657 | else: 658 | assert False, "Unknown mlc type" 659 | 660 | 661 | def conform_jaws_to_mlc(beam): 662 | bld = getblds(beam.BeamLimitingDeviceSequence) 663 | mlcdir, jawdir1, jawdir2 = get_mlc_and_jaw_directions(bld) 664 | nleaves = len(bld[mlcdir].LeafPositionBoundaries)-1 665 | for cp in beam.ControlPointSequence: 666 | opentolerance = 0.5 # mm 667 | if hasattr(cp, 'BeamLimitingDevicePositionSequence') and cp.BeamLimitingDevicePositionSequence != None: 668 | bldp = getblds(cp.BeamLimitingDevicePositionSequence) 669 | 670 | if bldp[mlcdir] != None and bldp[jawdir2] != None: 671 | min_open_leafi = nmin(i for i in range(nleaves) 672 | if bldp[mlcdir].LeafJawPositions[i] <= bldp[mlcdir].LeafJawPositions[i+nleaves] - opentolerance) 673 | max_open_leafi = nmax(i for i in range(nleaves) 674 | if bldp[mlcdir].LeafJawPositions[i] <= bldp[mlcdir].LeafJawPositions[i+nleaves] - opentolerance) 675 | if min_open_leafi != None and max_open_leafi != None: 676 | bldp[jawdir2].LeafJawPositions = [bld[mlcdir].LeafPositionBoundaries[min_open_leafi], 677 | bld[mlcdir].LeafPositionBoundaries[max_open_leafi + 1]] 678 | if bldp[mlcdir] != None and bldp[jawdir1] != None: 679 | min_open_leaf = min(bldp[mlcdir].LeafJawPositions[i] for i in range(nleaves) 680 | if bldp[mlcdir].LeafJawPositions[i] <= bldp[mlcdir].LeafJawPositions[i+nleaves] - opentolerance) 681 | max_open_leaf = max(bldp[mlcdir].LeafJawPositions[i+nleaves] for i in range(nleaves) 682 | if bldp[mlcdir].LeafJawPositions[i] <= bldp[mlcdir].LeafJawPositions[i+nleaves] - opentolerance) 683 | bldp[jawdir1].LeafJawPositions = [min_open_leaf, max_open_leaf] 684 | 685 | def conform_mlc_to_circle(beam, radius, center): 686 | bld = getblds(beam.BeamLimitingDeviceSequence) 687 | mlcdir, jawdir1, jawdir2 = get_mlc_and_jaw_directions(bld) 688 | nleaves = len(bld[mlcdir].LeafPositionBoundaries)-1 689 | for cp in beam.ControlPointSequence: 690 | if hasattr(cp, 'BeamLimitingDevicePositionSequence') and cp.BeamLimitingDevicePositionSequence != None: 691 | bldp = getblds(cp.BeamLimitingDevicePositionSequence) 692 | for i in range(nleaves): 693 | y = float((bld[mlcdir].LeafPositionBoundaries[i] + bld[mlcdir].LeafPositionBoundaries[i+1]) / 2) 694 | if abs(y) < radius: 695 | bldp[mlcdir].LeafJawPositions[i] = -np.sqrt(radius**2 - (y-center[1])**2) + center[0] 696 | bldp[mlcdir].LeafJawPositions[i + nleaves] = np.sqrt(radius**2 - (y-center[1])**2) + center[0] 697 | 698 | def conform_mlc_to_rectangle(beam, x, y, center): 699 | """Sets MLC to open at least x * y cm""" 700 | xy = x,y 701 | bld = getblds(beam.BeamLimitingDeviceSequence) 702 | mlcdir, jawdir1, jawdir2 = get_mlc_and_jaw_directions(bld) 703 | mlcidx = (0,1) if mlcdir == "MLCX" else (1,0) 704 | nleaves = len(bld[mlcdir].LeafPositionBoundaries)-1 705 | for cp in beam.ControlPointSequence: 706 | if hasattr(cp, 'BeamLimitingDevicePositionSequence') and cp.BeamLimitingDevicePositionSequence != None: 707 | bldp = getblds(cp.BeamLimitingDevicePositionSequence) 708 | for i in range(nleaves): 709 | if bld[mlcdir].LeafPositionBoundaries[i+1] > (center[mlcidx[1]]-xy[mlcidx[1]]/2.0) and bld[mlcdir].LeafPositionBoundaries[i] < (center[mlcidx[1]]+xy[mlcidx[1]]/2.0): 710 | bldp[mlcdir].LeafJawPositions[i] = center[mlcidx[0]] - xy[mlcidx[0]]/2.0 711 | bldp[mlcdir].LeafJawPositions[i + nleaves] = center[mlcidx[0]] + xy[mlcidx[0]]/2.0 712 | 713 | def conform_jaws_to_rectangle(beam, x, y, center): 714 | """Sets jaws opening to x * y cm, centered at `center`""" 715 | for cp in beam.ControlPointSequence: 716 | if hasattr(cp, 'BeamLimitingDevicePositionSequence') and cp.BeamLimitingDevicePositionSequence != None: 717 | bldp = getblds(cp.BeamLimitingDevicePositionSequence) 718 | bldp['ASYMX'].LeafJawPositions = [center[0] - x/2.0, center[0] + x/2.0] 719 | bldp['ASYMY'].LeafJawPositions = [center[1] - y/2.0, center[1] + y/2.0] 720 | 721 | def finalize_mlc(beam): 722 | # Just close the leaves at 0. TODO: be more clever 723 | for cp in beam.ControlPointSequence: 724 | if not hasattr(cp, 'BeamLimitingDevicePositionSequence'): 725 | continue 726 | mlcs = [bld for bld in cp.BeamLimitingDevicePositionSequence if bld.RTBeamLimitingDeviceType == "MLCX" or bld.RTBeamLimitingDeviceType == "MLCY"] 727 | if len(mlcs) != 1: 728 | continue 729 | mlc = mlcs[0] 730 | nleaves = len(mlc.LeafJawPositions)/2 731 | for i in range(nleaves): 732 | if mlc.LeafJawPositions[i] >= mlc.LeafJawPositions[i+nleaves]: 733 | mlc.LeafJawPositions[i] = 0 734 | mlc.LeafJawPositions[i+nleaves] = 0 735 | 736 | def conform_mlc_to_roi(beam, roi, current_study): 737 | bld = getblds(beam.BeamLimitingDeviceSequence) 738 | mlcdir, jawdir1, jawdir2 = get_mlc_and_jaw_directions(bld) 739 | mlcidx = (0,1) if mlcdir == "MLCX" else (1,0) 740 | def conform_mlc_to_roi_for_cp(cp, gantry_angle, gantry_pitch_angle, beam_limiting_device_angle, 741 | patient_support_angle, patient_position, 742 | table_top, table_top_ecc, sad, isocenter, beam_limiting_device_positions, roi): 743 | Mdb = get_dicom_to_bld_coordinate_transform(gantry_angle, gantry_pitch_angle, beam_limiting_device_angle, 744 | patient_support_angle, patient_position, 745 | table_top, table_top_ecc, sad, isocenter) 746 | for contour in roi.ContourSequence: 747 | nvertices = len(contour.ContourData)/3 748 | vertices = np.array(contour.ContourData).reshape((3,1,1,nvertices), order='F') 749 | vertices = np.vstack((vertices, np.ones((1,1,1,nvertices)))) 750 | 751 | lp = beam_limiting_device_positions[mlcdir].LeafJawPositions 752 | 753 | c = Mdb * vertices 754 | # Negation here since everything is at z < 0 in the b system, and that rotates by 180 degrees 755 | c2 = -np.array([float(beam.SourceAxisDistance)*c[mlcidx[0],:]/c[2,:], float(beam.SourceAxisDistance)*c[mlcidx[1],:]/c[2,:]]).squeeze() 756 | vs = zip(list(c2[0]), list(c2[1])) 757 | for v1,v2 in zip(vs[:-1], vs[1:]): 758 | open_mlc_for_line_segment(bld[mlcdir].LeafPositionBoundaries, lp, v1, v2) 759 | open_mlc_for_line_segment(bld[mlcdir].LeafPositionBoundaries, lp, vs[-1], vs[0]) 760 | 761 | 762 | do_for_all_cps(beam, current_study['PatientPosition'], conform_mlc_to_roi_for_cp, roi) 763 | 764 | def get_contours_in_bld(beam, roi, current_study): 765 | def conform_mlc_to_roi_for_cp(cp, gantry_angle, gantry_pitch_angle, beam_limiting_device_angle, 766 | patient_support_angle, patient_position, 767 | table_top, table_top_ecc, sad, isocenter, beam_limiting_device_positions, roi, contours): 768 | Mdb = get_dicom_to_bld_coordinate_transform(gantry_angle, gantry_pitch_angle, beam_limiting_device_angle, 769 | patient_support_angle, patient_position, 770 | table_top, table_top_ecc, sad, isocenter) 771 | for contour in roi.ContourSequence: 772 | nvertices = len(contour.ContourData)/3 773 | vertices = np.array(contour.ContourData).reshape((3,1,1,nvertices), order='F') 774 | vertices = np.vstack((vertices, np.ones((1,1,1,nvertices)))) 775 | 776 | c = Mdb * vertices 777 | # Negation here since everything is at z < 0 in the b system, and that rotates by 180 degrees 778 | c2 = -np.array([float(beam.SourceAxisDistance)*c[0,:]/c[2,:], float(beam.SourceAxisDistance)*c[1,:]/c[2,:]]).squeeze() 779 | contours[cp.ControlPointIndex].append(c2) 780 | 781 | 782 | contours = defaultdict(lambda: []) 783 | do_for_all_cps(beam, current_study['PatientPosition'], conform_mlc_to_roi_for_cp, roi, contours) 784 | return contours 785 | 786 | 787 | def open_mlc_for_line_segment(lpb, lp, v1, v2): 788 | if v1[1] > v2[1]: 789 | v1,v2 = v2,v1 790 | # line segment outside in y? 791 | if v2[1] < lpb[0] or v1[1] > lpb[-1]: 792 | return 793 | nleaves = len(lpb)-1 794 | for i in range(0,nleaves): 795 | if lpb[i+1] < v1[1]: 796 | continue 797 | if lpb[i] > v2[1]: 798 | break 799 | if v1[1] < lpb[i]: 800 | xstart = v1[0] + (v2[0]-v1[0]) * (lpb[i]-v1[1])/(v2[1]-v1[1]) 801 | else: 802 | xstart = v1[0] 803 | if v2[1] > lpb[i+1]: 804 | xend = v2[0] + (v1[0]-v2[0]) * (lpb[i+1]-v2[1])/(v1[1]-v2[1]) 805 | else: 806 | xend = v2[0] 807 | lp[i] = min(lp[i], xstart, xend) 808 | lp[i+nleaves] = max(lp[i+nleaves], xstart, xend) 809 | 810 | 811 | def zmax(g): 812 | try: 813 | return max(g) 814 | except ValueError: 815 | return 0 816 | 817 | def add_roi_to_structure_set(ds, ROIName, current_study): 818 | newroi = dicom.dataset.Dataset() 819 | roinumber = max([0] + [roi.ROINumber for roi in ds.StructureSetROISequence]) + 1 820 | newroi.ROIName = ROIName 821 | newroi.ReferencedFrameOfReferenceUID = get_current_study_uid('FrameOfReferenceUID', current_study) 822 | newroi.ROINumber = roinumber 823 | newroi.ROIGenerationAlgorithm = "SEMIAUTOMATIC" 824 | ds.StructureSetROISequence.append(newroi) 825 | return newroi 826 | 827 | def get_roi_contour_module(ds): 828 | ds.ROIContourSequence = [] 829 | return ds 830 | 831 | def get_rt_roi_observations_module(ds): 832 | ds.RTROIObservationsSequence = [] 833 | return ds 834 | 835 | def add_roi_to_rt_roi_observation(ds, roi, label, interpreted_type): 836 | roiobs = dicom.dataset.Dataset() 837 | ds.RTROIObservationsSequence.append(roiobs) 838 | roiobs.ObservationNumber = roi.ROINumber 839 | roiobs.ReferencedROINumber = roi.ROINumber 840 | roiobs.ROIObservationLabel = label # T3 841 | # roiobs.ROIObservationDescription = "" # T3 842 | # roiobs.RTRelatedROISequence = [] # T3 843 | # roiobs.RelatedRTROIObservationsSequence = [] # T3 844 | roiobs.RTROIInterpretedType = interpreted_type # T3 845 | roiobs.ROIInterpreter = "" # T2 846 | # roiobs.MaterialID = "" # T3 847 | # roiobs.ROIPhysicalPropertiesSequence = [] # T3 848 | return roiobs 849 | 850 | def add_roi_to_roi_contour(ds, roi, contours, ref_images): 851 | newroi = dicom.dataset.Dataset() 852 | ds.ROIContourSequence.append(newroi) 853 | newroi.ReferencedROINumber = roi.ROINumber 854 | newroi.ROIDisplayColor = roicolors[(roi.ROINumber-1) % len(roicolors)] 855 | newroi.ContourSequence = [] 856 | for i, contour in enumerate(contours, 1): 857 | c = dicom.dataset.Dataset() 858 | newroi.ContourSequence.append(c) 859 | c.ContourNumber = i 860 | c.ContourGeometricType = 'CLOSED_PLANAR' 861 | # c.AttachedContours = [] # T3 862 | if ref_images != None: 863 | c.ContourImageSequence = [] # T3 864 | for image in ref_images: 865 | if image.ImagePositionPatient[2] == contour[0,2]: 866 | imgref = dicom.dataset.Dataset() 867 | imgref.ReferencedSOPInstanceUID = image.SOPInstanceUID 868 | imgref.ReferencedSOPClassUID = image.SOPClassUID 869 | # imgref.ReferencedFrameNumber = "" # T1C on multiframe 870 | # imgref.ReferencedSegmentNumber = "" # T1C on segmentation 871 | c.ContourImageSequence.append(imgref) 872 | # c.ContourSlabThickness = "" # T3 873 | # c.ContourOffsetVector = [0,0,0] # T3 874 | c.NumberOfContourPoints = len(contour) 875 | c.ContourData = "\\".join(["%g" % x for x in contour.ravel().tolist()]) 876 | return newroi 877 | 878 | 879 | roicolors = [[255,0,0], 880 | [0,255,0], 881 | [0,0,255], 882 | [255,255,0], 883 | [0,255,255], 884 | [255,0,255], 885 | [255,127,0], 886 | [127,255,0], 887 | [0,255,127], 888 | [0,127,255], 889 | [127,0,255], 890 | [255,0,127], 891 | [255,127,127], 892 | [127,255,127], 893 | [127,127,255], 894 | [255,255,127], 895 | [255,127,255], 896 | [127,255,255]] 897 | 898 | def get_structure_set_module(ds, DT, TM, ref_images, current_study): 899 | ds.StructureSetLabel = "Structure Set" # T1 900 | # ds.StructureSetName = "" # T3 901 | # ds.StructureSetDescription = "" # T3 902 | # ds.InstanceNumber = "" # T3 903 | ds.StructureSetDate = DT # T2 904 | ds.StructureSetTime = TM # T2 905 | if ref_images != None and len(ref_images) > 0: 906 | reffor = dicom.dataset.Dataset() 907 | reffor.FrameOfReferenceUID = get_current_study_uid('FrameOfReferenceUID', current_study) 908 | refstudy = dicom.dataset.Dataset() 909 | refstudy.ReferencedSOPClassUID = get_uid("Detached Study Management SOP Class") # T1, but completely bogus. 910 | refstudy.ReferencedSOPInstanceUID = get_current_study_uid('StudyUID', current_study) # T1 911 | assert len(set(x.SeriesInstanceUID for x in ref_images)) == 1 912 | refseries = dicom.dataset.Dataset() 913 | refseries.SeriesInstanceUID = ref_images[0].SeriesInstanceUID 914 | refseries.ContourImageSequence = [] # T3 915 | for image in ref_images: 916 | imgref = dicom.dataset.Dataset() 917 | imgref.ReferencedSOPInstanceUID = image.SOPInstanceUID 918 | imgref.ReferencedSOPClassUID = image.SOPClassUID 919 | # imgref.ReferencedFrameNumber = "" # T1C on multiframe 920 | # imgref.ReferencedSegmentNumber = "" # T1C on segmentation 921 | refseries.ContourImageSequence.append(imgref) 922 | refstudy.RTReferencedSeriesSequence = [refseries] 923 | reffor.RTReferencedStudySequence = [refstudy] 924 | ds.ReferencedFrameOfReferenceSequence = [reffor] # T3 925 | ds.StructureSetROISequence = [] 926 | 927 | return ds 928 | 929 | def add_static_rt_beam(ds, nleaves, mlcdir, leafwidths, gantry_angle, collimator_angle, patient_support_angle, table_top, table_top_eccentric, isocenter, nominal_beam_energy, current_study, sad=None): 930 | assert mlcdir in ["MLCX", "MLCY"] 931 | beam_number = zmax(b.BeamNumber for b in ds.BeamSequence) + 1 932 | beam = dicom.dataset.Dataset() 933 | ds.BeamSequence.append(beam) 934 | beam.BeamNumber = beam_number 935 | beam.BeamName = "B{0}".format(beam_number) # T3 936 | # beam.BeamDescription # T3 937 | beam.BeamType = "STATIC" 938 | beam.RadiationType = "PHOTON" 939 | # beam.PrimaryFluenceModeSequence = [] # T3 940 | # beam.HighDoseTechniqueType = "NORMAL" # T1C 941 | beam.TreatmentMachineName = "Linac" # T2 942 | # beam.Manufacturer = "" # T3 943 | # beam.InstitutionName # T3 944 | # beam.InstitutionAddress # T3 945 | # beam.InstitutionalDepartmentName # T3 946 | # beam.ManufacturerModelName # T3 947 | # beam.DeviceSerialNumber # T3 948 | beam.PrimaryDosimeterUnit = "MU" # T3 949 | # beam.ReferencedToleranceTableNumber # T3 950 | if sad is None: 951 | beam.SourceAxisDistance = 1000 # mm, T3 952 | else: 953 | beam.SourceAxisDistance = sad # mm, T3 954 | beam.BeamLimitingDeviceSequence = [dicom.dataset.Dataset() for k in range(3)] 955 | beam.BeamLimitingDeviceSequence[0].RTBeamLimitingDeviceType = "ASYMX" 956 | #beam.BeamLimitingDeviceSequence[0].SourceToBeamLimitingDeviceDistance = 60 # T3 957 | beam.BeamLimitingDeviceSequence[0].NumberOfLeafJawPairs = 1 958 | beam.BeamLimitingDeviceSequence[1].RTBeamLimitingDeviceType = "ASYMY" 959 | #beam.BeamLimitingDeviceSequence[1].SourceToBeamLimitingDeviceDistance = 50 # T3 960 | beam.BeamLimitingDeviceSequence[1].NumberOfLeafJawPairs = 1 961 | beam.BeamLimitingDeviceSequence[2].RTBeamLimitingDeviceType = mlcdir 962 | #beam.BeamLimitingDeviceSequence[2].SourceToBeamLimitingDeviceDistance = 40 # T3 963 | beam.BeamLimitingDeviceSequence[2].NumberOfLeafJawPairs = sum(nleaves) 964 | mlcsize = sum(n*w for n,w in zip(nleaves, leafwidths)) 965 | beam.BeamLimitingDeviceSequence[2].LeafPositionBoundaries = list(x - mlcsize/2 for x in cumsum(w for n,w in zip(nleaves, leafwidths) for k in range(n))) 966 | if 'PatientPosition' in current_study: 967 | beam.ReferencedPatientSetupNumber = 1 # T3 968 | # beam.ReferencedReferenceImageSequence = [] # T3 969 | # beam.PlannedVerificationImageSequence = [] # T3 970 | beam.TreatmentDeliveryType = "TREATMENT" 971 | # beam.ReferencedDoseSequence = [] # T3 972 | beam.NumberOfWedges = 0 973 | # beam.WedgeSequence = [] # T1C on NumberOfWedges != 0 974 | beam.NumberOfCompensators = 0 975 | beam.NumberOfBoli = 0 976 | beam.NumberOfBlocks = 0 977 | beam.FinalCumulativeMetersetWeight = 100 978 | beam.NumberOfControlPoints = 2 979 | beam.ControlPointSequence = [dicom.dataset.Dataset() for k in range(2)] 980 | for j in range(2): 981 | cp = beam.ControlPointSequence[j] 982 | cp.ControlPointIndex = j 983 | cp.CumulativeMetersetWeight = j * beam.FinalCumulativeMetersetWeight / 1 984 | # cp.ReferencedDoseReferenceSequence = [] # T3 985 | # cp.ReferencedDoseSequence = [] # T1C on DoseSummationType == "CONTROL_POINT" 986 | # cp.NominalBeamEnergy = 6 # T3 987 | # cp.DoseRateSet = 100 # T3 988 | # cp.WedgePositionSequence = [] # T3 989 | if j == 0: 990 | cp.BeamLimitingDevicePositionSequence = [dicom.dataset.Dataset() for k in range(3)] 991 | cp.BeamLimitingDevicePositionSequence[0].RTBeamLimitingDeviceType = 'ASYMX' 992 | cp.BeamLimitingDevicePositionSequence[0].LeafJawPositions = [0,0] 993 | cp.BeamLimitingDevicePositionSequence[1].RTBeamLimitingDeviceType = 'ASYMY' 994 | cp.BeamLimitingDevicePositionSequence[1].LeafJawPositions = [0,0] 995 | cp.BeamLimitingDevicePositionSequence[2].RTBeamLimitingDeviceType = mlcdir 996 | cp.BeamLimitingDevicePositionSequence[2].LeafJawPositions = [1000]*sum(nleaves) + [-1000] * sum(nleaves) 997 | cp.GantryAngle = gantry_angle 998 | cp.GantryRotationDirection = 'NONE' 999 | cp.NominalBeamEnergy = nominal_beam_energy 1000 | # cp.GantryPitchAngle = 0 # T3 1001 | # cp.GantryPitchRotationDirection = "NONE" # T3 1002 | cp.BeamLimitingDeviceAngle = collimator_angle 1003 | cp.BeamLimitingDeviceRotationDirection = "NONE" 1004 | cp.PatientSupportAngle = patient_support_angle 1005 | cp.PatientSupportRotationDirection = "NONE" 1006 | cp.TableTopEccentricAxisDistance = table_top_eccentric.Ls # T3 1007 | cp.TableTopEccentricAngle = table_top_eccentric.theta_e 1008 | cp.TableTopEccentricRotationDirection = "NONE" 1009 | cp.TableTopPitchAngle = table_top.psi_t 1010 | cp.TableTopPitchRotationDirection = "NONE" 1011 | cp.TableTopRollAngle = table_top.phi_t 1012 | cp.TableTopRollRotationDirection = "NONE" 1013 | cp.TableTopVerticalPosition = table_top.Tz 1014 | cp.TableTopLongitudinalPosition = table_top.Ty 1015 | cp.TableTopLateralPosition = table_top.Tx 1016 | cp.IsocenterPosition = isocenter 1017 | # cp.SurfaceEntryPoint = [0,0,0] # T3 1018 | # cp.SourceToSurfaceDistance = 70 # T3 1019 | return beam 1020 | 1021 | 1022 | def get_rt_ion_beams_module(ds, nbeams, collimator_angles, patient_support_angles, table_top, table_top_eccentric, isocenter, current_study): 1023 | """Not done, in development""" 1024 | if isinstance(nbeams, int): 1025 | nbeams = [i * 360 / nbeams for i in range(nbeams)] 1026 | if isinstance(collimator_angles, int): 1027 | collimator_angles = [collimator_angles for i in nbeams] 1028 | if isinstance(patient_support_angles, int): 1029 | patient_support_angles = [patient_support_angles for i in nbeams] 1030 | ds.IonBeamSequence = [dicom.dataset.Dataset() for gantryAngle in nbeams] 1031 | for i, gantryAngle in enumerate(nbeams): 1032 | beam = ds.IonBeamSequence[i] 1033 | beam.BeamNumber = i + 1 1034 | beam.BeamName = "B{0}".format(i+1) # T3 1035 | # beam.BeamDescription # T3 1036 | beam.BeamType = "STATIC" 1037 | beam.RadiationType = "PROTON" 1038 | # beam.RadiationMassNumber = 1 # 1C on beam.RadiationType == ION 1039 | # beam.RadiationAtomicNumber = 1 # 1C on beam.RadiationType == ION 1040 | # beam.RadiationChargeState = 1 # 1C on beam.RadiationType == ION 1041 | beam.ScanMode = "MODULATED" 1042 | beam.TreatmentMachineName = "Mevion_1" # T2 1043 | # beam.Manufacturer = "" # T3 1044 | # beam.InstitutionName # T3 1045 | # beam.InstitutionAddress # T3 1046 | # beam.InstitutionalDepartmentName # T3 1047 | # beam.ManufacturerModelName # T3 1048 | # beam.DeviceSerialNumber # T3 1049 | beam.PrimaryDosimeterUnit = "MU" # T3 1050 | # beam.ReferencedToleranceTableNumber # T3 1051 | beam.VirtualSourceAxisDistance = 1000 # mm, T1 1052 | # beam.IonBeamLimitingDeviceSequence = [dicom.dataset.Dataset() for k in range(3)] # T3 1053 | if 'PatientPosition' in current_study: 1054 | beam.ReferencedPatientSetupNumber = 1 # T3 1055 | # beam.ReferencedReferenceImageSequence = [] # T3 1056 | beam.TreatmentDeliveryType = "TREATMENT" 1057 | # beam.ReferencedDoseSequence = [] # T3 1058 | beam.NumberOfWedges = 0 1059 | # beam.TotalWedgeTrayWaterEquivalentThickness = 0 # T3 1060 | # beam.IonWedgeSequence = [] # T1C on NumberOfWedges != 0 1061 | beam.NumberOfCompensators = 0 1062 | # beam.TotalCompensatorTrayWaterEquivalentThickness = 0 # T3 1063 | # beam.IonRangeCompensatorSequence = [] # T1C on NumberOfCompensators != 0 1064 | beam.NumberOfBoli = 0 1065 | beam.NumberOfBlocks = 0 1066 | # beam.SnoutSequence = [] # T3 1067 | # beam.ApplicatorSequence = [] 1068 | beam.NumberOfRangeShifters = 0 1069 | # beam.RangeShifterSequence = [] # T1C on NumberOfRangeShifters != 0 1070 | beam.NumberOfLateralSpreadingDevices = 0 # 1 for SS, 2 for DS? 1071 | # beam.LateralSpreadingDeviceSequence = [] # T1C on beam.NumberOfLateralSpreadingDevices != 0 1072 | beam.NumberOfRangeModulators = 0 1073 | # beam.RangeModulatorSequence = [] 1074 | # TODO: Patient Support Identification Macro 1075 | # beam.FixationLightAzimuthalAngle # T3 1076 | # beam.FixationLightPolarAngle # T3 1077 | beam.FinalCumulativeMetersetWeight = 100 1078 | beam.NumberOfControlPoints = 2 1079 | beam.IonControlPointSequence = [dicom.dataset.Dataset() for k in range(2)] 1080 | for j in range(2): 1081 | cp = beam.IonControlPointSequence[j] 1082 | cp.ControlPointIndex = j 1083 | cp.CumulativeMetersetWeight = j * beam.FinalCumulativeMetersetWeight / 1 1084 | # cp.ReferencedDoseReferenceSequence = [] # T3 1085 | # cp.ReferencedDoseSequence = [] # T1C on DoseSummationType == "CONTROL_POINT" 1086 | # cp.MetersetRate = 100 # T3 1087 | if j == 0: 1088 | cp.NominalBeamEnergy = current_study['NominalEnergy'] # T1C in first cp or change 1089 | # cp.IonWedgePositionSequence = [] # T1C on beam.NumberOfWedges != 0 1090 | # cp.RangeShifterSettingsSequence = [] # T1C on beam.NumberOfRangeShifters != 0 1091 | # cp.LateralSpreadingDeviceSettingsSequence = [] # T1C on beam.NumberOfLateralSpreadingDevices != 0 1092 | # cp.RangeModulatorSettingsSequence = [] # T1C on beam.NumberOfRangeModulators != 0 1093 | # TODO?: Beam Limiting Device Position Macro 1094 | cp.GantryAngle = gantryAngle 1095 | cp.GantryRotationDirection = 'NONE' 1096 | # cp.KVp = "" # T1C on nominal beam energy not present 1097 | cp.GantryPitchAngle = "" # T2C on first cp or change 1098 | cp.GantryPitchRotationDirection = "" # T2C on first cp or change 1099 | cp.BeamLimitingDeviceAngle = collimator_angles[i] 1100 | cp.BeamLimitingDeviceRotationDirection = "NONE" 1101 | # cp.ScanSpotTuneID = "XYZ" # T1C on beam.ScanMode == "MODULATED" 1102 | # cp.NumberOfScanSpotPositions = 0 # T1C on beam.ScanMode == "MODULATED" 1103 | # cp.ScanSpotPositionMap = [] # T1C on beam.ScanMode == "MODULATED" 1104 | # cp.ScanSpotMetersetWeights = [] # T1C on beam.ScanMode == "MODULATED" 1105 | # cp.ScanningSpotSize = "" # T3 1106 | # cp.NumberOfPaintings = 0 # T1C on beam.ScanMode == "MODULATED" 1107 | cp.PatientSupportAngle = patient_support_angles[i] 1108 | cp.PatientSupportRotationDirection = "NONE" 1109 | cp.TableTopPitchAngle = table_top.psi_t 1110 | cp.TableTopPitchRotationDirection = "NONE" 1111 | cp.TableTopRollAngle = table_top.phi_t 1112 | cp.TableTopRollRotationDirection = "NONE" 1113 | # cp.HeadFixationAngle = "" # T3 1114 | cp.TableTopVerticalPosition = table_top.Tz 1115 | cp.TableTopLongitudinalPosition = table_top.Ty 1116 | cp.TableTopLateralPosition = table_top.Tx 1117 | cp.SnoutPosition = "" # T2C on first cp or change 1118 | cp.IsocenterPosition = isocenter 1119 | # cp.SurfaceEntryPoint = [0,0,0] # T3 1120 | 1121 | 1122 | 1123 | def build_rt_plan(current_study, isocenter, structure_set=None, **kwargs): 1124 | FoRuid = get_current_study_uid('FrameOfReferenceUID', current_study) 1125 | studyuid = get_current_study_uid('StudyUID', current_study) 1126 | seriesuid = generate_uid() 1127 | rp = get_default_rt_plan_dataset(current_study, isocenter, structure_set) 1128 | rp.SeriesInstanceUID = seriesuid 1129 | rp.StudyInstanceUID = studyuid 1130 | rp.FrameOfReferenceUID = FoRuid 1131 | for k, v in kwargs.iteritems(): 1132 | if v != None: 1133 | setattr(rp, k, v) 1134 | return rp 1135 | 1136 | def build_rt_dose(dose_data, voxel_size, center, current_study, rtplan, dose_grid_scaling, dose_summation_type, beam_number, **kwargs): 1137 | nVoxels = dose_data.shape 1138 | FoRuid = get_current_study_uid('FrameOfReferenceUID', current_study) 1139 | studyuid = get_current_study_uid('StudyUID', current_study) 1140 | seriesuid = generate_uid() 1141 | rd = get_default_rt_dose_dataset(current_study, rtplan, dose_summation_type, beam_number) 1142 | rd.SeriesInstanceUID = seriesuid 1143 | rd.StudyInstanceUID = studyuid 1144 | rd.FrameOfReferenceUID = FoRuid 1145 | rd.Rows = nVoxels[1] 1146 | rd.Columns = nVoxels[0] 1147 | rd.NumberOfFrames = nVoxels[2] 1148 | rd.PixelSpacing = [voxel_size[1], voxel_size[0]] 1149 | rd.SliceThickness = voxel_size[2] 1150 | rd.GridFrameOffsetVector = [z*voxel_size[2] for z in range(nVoxels[2])] 1151 | rd.DoseGridScaling = dose_grid_scaling 1152 | rd.ImagePositionPatient = [center[0]-(nVoxels[0]-1)*voxel_size[0]/2.0, 1153 | center[1]-(nVoxels[1]-1)*voxel_size[1]/2.0, 1154 | center[2]-(nVoxels[2]-1)*voxel_size[2]/2.0 + z*voxel_size[2]] 1155 | 1156 | rd.PixelData=dose_data.tostring(order='F') 1157 | for k, v in kwargs.iteritems(): 1158 | if v != None: 1159 | setattr(rd, k, v) 1160 | return rd 1161 | 1162 | 1163 | def build_rt_structure_set(ref_images, current_study, **kwargs): 1164 | studyuid = get_current_study_uid('StudyUID', current_study) 1165 | seriesuid = generate_uid() 1166 | rs = get_default_rt_structure_set_dataset(ref_images, current_study) 1167 | rs.SeriesInstanceUID = seriesuid 1168 | rs.StudyInstanceUID = studyuid 1169 | for k, v in kwargs.iteritems(): 1170 | if v != None: 1171 | setattr(rs, k, v) 1172 | return rs 1173 | 1174 | 1175 | def build_ct(ct_data, pixel_representation, rescale_slope, rescale_intercept, voxel_size, center, current_study, **kwargs): 1176 | nVoxels = ct_data.shape 1177 | ctbaseuid = generate_uid() 1178 | FoRuid = get_current_study_uid('FrameOfReferenceUID', current_study) 1179 | studyuid = get_current_study_uid('StudyUID', current_study) 1180 | seriesuid = generate_uid() 1181 | cts = [] 1182 | for z in range(nVoxels[2]): 1183 | sopinstanceuid = "%s.%i" % (ctbaseuid, z) 1184 | ct = get_default_ct_dataset( 1185 | sopinstanceuid, 1186 | current_study, 1187 | pixel_representation, 1188 | rescale_slope=rescale_slope, 1189 | rescale_intercept=rescale_intercept) 1190 | ct.SeriesInstanceUID = seriesuid 1191 | ct.StudyInstanceUID = studyuid 1192 | ct.FrameOfReferenceUID = FoRuid 1193 | ct.Rows = nVoxels[1] 1194 | ct.Columns = nVoxels[0] 1195 | ct.PixelSpacing = [voxel_size[1], voxel_size[0]] 1196 | ct.SliceThickness = voxel_size[2] 1197 | ct.ImagePositionPatient = [center[0]-(nVoxels[0]-1)*voxel_size[0]/2.0, 1198 | center[1]-(nVoxels[1]-1)*voxel_size[1]/2.0, 1199 | center[2]-(nVoxels[2]-1)*voxel_size[2]/2.0 + z*voxel_size[2]] 1200 | ct.PixelData=ct_data[:,:,z].tostring(order='F') 1201 | if 'PatientPosition' in current_study: 1202 | ct.PatientPosition = current_study['PatientPosition'] 1203 | for k, v in kwargs.iteritems(): 1204 | if v != None: 1205 | setattr(ct, k, v) 1206 | cts.append(ct) 1207 | return cts 1208 | 1209 | 1210 | def build_mr(mr_data, pixel_representation, voxel_size, center, current_study, **kwargs): 1211 | voxel_count = mr_data.shape 1212 | mr_base_uid = generate_uid() 1213 | for_uid = get_current_study_uid('FrameOfReferenceUID', current_study) 1214 | study_uid = get_current_study_uid('StudyUID', current_study) 1215 | series_uid = generate_uid() 1216 | mrs = [] 1217 | for z in range(voxel_count[2]): 1218 | sop_instance_uid = "%s.%i" % (mr_base_uid, z) 1219 | mr = get_default_mr_dataset( 1220 | sop_instance_uid, 1221 | current_study, 1222 | pixel_representation) 1223 | mr.SeriesInstanceUID = series_uid 1224 | mr.StudyInstanceUID = study_uid 1225 | mr.FrameOfReferenceUID = for_uid 1226 | mr.Rows = voxel_count[1] 1227 | mr.Columns = voxel_count[0] 1228 | mr.PixelSpacing = [voxel_size[1], voxel_size[0]] 1229 | mr.SliceThickness = voxel_size[2] 1230 | mr.ImagePositionPatient = [ 1231 | center[0] - (voxel_count[0] - 1) * voxel_size[0] / 2.0, 1232 | center[1] - (voxel_count[1] - 1) * voxel_size[1] / 2.0, 1233 | center[2] - (voxel_count[2] - 1) * voxel_size[2] / 2.0 + z * voxel_size[2] 1234 | ] 1235 | mr.PixelData = mr_data[:, :, z].tostring(order='F') 1236 | if 'PatientPosition' in current_study: 1237 | mr.PatientPosition = current_study['PatientPosition'] 1238 | for k, v in kwargs.iteritems(): 1239 | if v is not None: 1240 | setattr(mr, k, v) 1241 | mrs.append(mr) 1242 | return mrs 1243 | 1244 | 1245 | def build_pt(pt_data, pixel_representation, rescale_slope, voxel_size, center, current_study, **kwargs): 1246 | voxel_count = pt_data.shape 1247 | pt_base_uid = generate_uid() 1248 | for_uid = get_current_study_uid('FrameOfReferenceUID', current_study) 1249 | study_uid = get_current_study_uid('StudyUID', current_study) 1250 | series_uid = generate_uid() 1251 | pts = [] 1252 | for z in range(voxel_count[2]): 1253 | sop_instance_uid = "%s.%i" % (pt_base_uid, z) 1254 | pt = get_default_pt_dataset( 1255 | sop_instance_uid, 1256 | current_study, 1257 | image_index=z, 1258 | number_of_slices=voxel_count[2], 1259 | pixel_representation=pixel_representation, 1260 | rescale_slope=rescale_slope) 1261 | pt.SeriesInstanceUID = series_uid 1262 | pt.StudyInstanceUID = study_uid 1263 | pt.FrameOfReferenceUID = for_uid 1264 | pt.Rows = voxel_count[1] 1265 | pt.Columns = voxel_count[0] 1266 | pt.PixelSpacing = [voxel_size[1], voxel_size[0]] 1267 | pt.SliceThickness = voxel_size[2] 1268 | pt.ImagePositionPatient = [ 1269 | center[0] - (voxel_count[0] - 1) * voxel_size[0] / 2.0, 1270 | center[1] - (voxel_count[1] - 1) * voxel_size[1] / 2.0, 1271 | center[2] - (voxel_count[2] - 1) * voxel_size[2] / 2.0 + z * voxel_size[2] 1272 | ] 1273 | pt.PixelData = pt_data[:, :, z].tostring(order='F') 1274 | if 'PatientPosition' in current_study: 1275 | pt.PatientPosition = current_study['PatientPosition'] 1276 | for k, v in kwargs.iteritems(): 1277 | if v is not None: 1278 | setattr(pt, k, v) 1279 | pts.append(pt) 1280 | return pts 1281 | -------------------------------------------------------------------------------- /orientationtests.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | try: 3 | shutil.rmtree("orientationtests") 4 | except: 5 | pass 6 | import builders 7 | reload(builders) 8 | import modules 9 | reload(modules) 10 | from builders import StudyBuilder 11 | 12 | import os 13 | if not os.path.exists("orientationtests"): 14 | os.mkdir("orientationtests") 15 | 16 | def build_orientation(patient_position, row_direction, column_direction, frame_of_reference_uid = None): 17 | sb = StudyBuilder(patient_position=patient_position, patient_id="OrientationTests", patient_name="Orientation^Tests", patient_birthdate = "20121212") 18 | if frame_of_reference_uid != None: 19 | sb.current_study['FrameOfReferenceUID'] = frame_of_reference_uid 20 | 21 | print "building %s..." % (patient_position,) 22 | print "ct" 23 | ct = sb.build_ct( 24 | num_voxels=[7, 7, 7], 25 | voxel_size=[4, 4, 4], 26 | pixel_representation=0, 27 | rescale_slope=1, 28 | rescale_intercept=-1024, 29 | row_direction=row_direction, 30 | column_direction=column_direction) 31 | ct.clear(real_value = -1000) 32 | ct.add_box(size = [4,4,4], center = [0,0,0], real_value = 0) 33 | ct.add_box(size = [20,4,4], center = [0,-8,-8], real_value = 0) 34 | ct.add_box(size = [4,20,4], center = [8,0,-8], real_value = 0) 35 | ct.add_box(size = [4,4,20], center = [8,8,0], real_value = 0) 36 | ct.add_sphere(radius = 4, center = [-8,-8,-8], real_value = 0) 37 | 38 | print "rtstruct" 39 | rtstruct = sb.build_structure_set(ct) 40 | rtstruct.add_external_box() 41 | rtstruct.add_box(size = [4,4,4], center = [0,0,0], name='CenterVoxel', interpreted_type='SITE') 42 | rtstruct.add_box(size = [20,4,4], center = [0,-8,-8], name='x=-8 to 8, y=z=-8', interpreted_type='SITE') 43 | rtstruct.add_box(size = [4,20,4], center = [8,0,-8], name='y=-8 to 8 x=8, z=-8', interpreted_type='SITE') 44 | rtstruct.add_box(size = [4,4,20], center = [8,8,0], name='z=-8 to 8, x=y=8', interpreted_type='SITE') 45 | rtstruct.add_sphere(radius=4, center = [-8,-8,-8], name='x=y=z=-8', interpreted_type='SITE') 46 | rtstruct.build() 47 | 48 | print "rtplan" 49 | rtplan = sb.build_static_plan(structure_set = rtstruct, sad=20) 50 | b1 = rtplan.build_beam(gantry_angle = 0, collimator_angle=30, meterset = 100) 51 | b1.conform_to_rectangle(4, 4, [0,0]) 52 | b2 = rtplan.build_beam(gantry_angle = 120, meterset = 100) 53 | b2.conform_to_rectangle(4, 4, [4,4]) 54 | rtplan.build() 55 | 56 | print "rtdose" 57 | rtdose = sb.build_dose(planbuilder = rtplan) 58 | for beam in rtplan.beam_builders: 59 | rtdose.add_lightfield(beam.rtbeam, beam.meterset) 60 | 61 | return sb 62 | 63 | orientations = [([0,1,0],[1,0,0]),([0,-1,0],[-1,0,0]),([0,1,0],[-1,0,0]),([0,-1,0],[1,0,0])] 64 | patientpositions = ['HFS','HFP','FFS','FFP','HFDR', 'HFDL', 'FFDR', 'FFDL'] 65 | sbs = [] 66 | FoR = None 67 | for o in orientations: 68 | for p in patientpositions: 69 | sb = build_orientation(p, o[0], o[1], FoR) 70 | sbs.append(sb) 71 | FoR = sbs[0].current_study['FrameOfReferenceUID'] 72 | d = "orientationtests/" + p + "/" + "%s%s%s%s%s%s" % tuple(x for y in o for x in y) 73 | os.makedirs(d) 74 | sb.write(d) 75 | -------------------------------------------------------------------------------- /plotting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Jan 23 09:14:42 2013 4 | 5 | @author: rickarad 6 | """ 7 | import matplotlib.pyplot as pp 8 | import numpy as np 9 | 10 | import modules 11 | 12 | def plot_roi_in_cp(beam, cp, roi_contour, study): 13 | contours = modules.get_contours_in_bld(beam, roi_contour, study) 14 | for c in contours[cp.ControlPointIndex]: 15 | pp.plot(list(c[0]) + [c[0][0]], list(c[1]) + [c[1][0]]) 16 | 17 | def plot_cp(beam, cp): 18 | plot_leaves(modules.getblds(beam.BLDs)['MLCX'].LeafPositionBoundaries, 19 | modules.getblds(cp.BLDPositions)['MLCX'].LeafJawPositions) 20 | 21 | def plot_leaves(boundaries, positions): 22 | b = boundaries 23 | p = positions 24 | pp.barh(b[:-1], p[:60]-np.min(p)+1, np.diff(b)-0.1*np.min(np.diff(b)), np.min(p)-1, alpha=0.4) 25 | pp.barh(b[:-1], p[60:]-np.max(p)-1, np.diff(b)-0.1*np.min(np.diff(b)), np.max(p)+1, alpha=0.4) -------------------------------------------------------------------------------- /roiconform.py: -------------------------------------------------------------------------------- 1 | import builders 2 | reload(builders) 3 | import modules 4 | reload(modules) 5 | from builders import StudyBuilder 6 | import matplotlib.pyplot as pp 7 | 8 | import os 9 | tmpdir = os.path.join(os.getenv("TEMP"), "studybuilder") 10 | if not os.path.exists(tmpdir): 11 | os.mkdir(tmpdir) 12 | 13 | sb = StudyBuilder(patient_position="HFS", patient_id="123", patient_name="Kalle^Kula", patient_birthdate = "20121212") 14 | ct = sb.build_ct( 15 | num_voxels=[48, 64, 75], 16 | voxel_size=[4, 3, 4], 17 | pixel_representation=0, 18 | rescale_slope=1, 19 | rescale_intercept=-1024) 20 | ct.clear(real_value = 0) 21 | print ct.pixel_array.max(),ct.pixel_array.min() 22 | ct.add_sphere(radius = 25, center = [0,0,0], real_value = -1000, mode = 'set') 23 | print ct.pixel_array.max(),ct.pixel_array.min() 24 | ct.add_box(size = [25,50,25], center = [0,0,0], stored_value = 300, mode = 'add') 25 | print ct.pixel_array.max(),ct.pixel_array.min() 26 | 27 | assert sb.seriesbuilders['CT'] == [ct] 28 | 29 | rtstruct = sb.build_structure_set() 30 | rtstruct.add_external_box() 31 | sph = rtstruct.add_sphere(radius=70, center = [-50,0,-100], name='Sph-Organ', interpreted_type='CAVITY') 32 | sph2 = rtstruct.add_sphere(radius = 25, center = [0,0,0], name='Sph-PTV', interpreted_type='PTV') 33 | 34 | rtplan = sb.build_static_plan() 35 | b1 = rtplan.build_beam(gantry_angle = 180, collimator_angle = 15, meterset = 100) 36 | b1.conform_to_rectangle(1,1,[0,0]) 37 | 38 | rtplan.build(finalize_mlc = False) 39 | 40 | modules.conform_mlc_to_roi(b1.rtbeam, sph.roi_contour, sb.current_study) 41 | modules.conform_mlc_to_roi(b1.rtbeam, sph2.roi_contour, sb.current_study) 42 | b1.finalize_mlc() 43 | 44 | sb.write(tmpdir) 45 | print tmpdir 46 | 47 | import plotting as p 48 | 49 | p.plot_cp(rtplan.datasets[0].Beams[0], rtplan.datasets[0].Beams[0].CPs[0]) 50 | p.plot_roi_in_cp(rtplan.datasets[0].Beams[0], rtplan.datasets[0].Beams[0].CPs[0], sph.roi_contour, sb.current_study) 51 | pp.axis('image') -------------------------------------------------------------------------------- /studybuilderexample.py: -------------------------------------------------------------------------------- 1 | import dicom 2 | import builders 3 | reload(builders) 4 | import modules 5 | reload(modules) 6 | from builders import StudyBuilder 7 | 8 | import os 9 | tmpdir = os.path.join(os.getenv("TEMP"), "studybuilder") 10 | if not os.path.exists(tmpdir): 11 | os.mkdir(tmpdir) 12 | 13 | sb = StudyBuilder(patient_position="HFS", patient_id="123", patient_name="Kalle^Kula", patient_birthdate = "20121212") 14 | ct = sb.build_ct( 15 | num_voxels=[48, 64, 48], 16 | voxel_size=[4, 3, 4], 17 | pixel_representation=0, 18 | rescale_slope=1, 19 | rescale_intercept=-1024) 20 | ct.clear(real_value = 0) 21 | print ct.pixel_array.max(),ct.pixel_array.min() 22 | ct.add_sphere(radius = 25, center = [0,0,0], real_value = -1000, mode = 'set') 23 | print ct.pixel_array.max(),ct.pixel_array.min() 24 | ct.add_box(size = [25,50,25], center = [0,0,0], stored_value = 300, mode = 'add') 25 | print ct.pixel_array.max(),ct.pixel_array.min() 26 | 27 | assert sb.seriesbuilders['CT'] == [ct] 28 | 29 | rtstruct = sb.build_structure_set() 30 | rtstruct.add_external_box() 31 | rtstruct.add_sphere(radius = 25, center = [0,0,0], name='Sph-PTV', interpreted_type='PTV') 32 | rtstruct.add_box(size = [25,50,25], center = [0,0,0], name='Box-Organ', interpreted_type='CAVITY') 33 | 34 | rtplan = sb.build_static_plan() 35 | b1 = rtplan.build_beam(gantry_angle = 0, meterset = 100) 36 | b1.conform_to_circle(25, [0,0]) 37 | b2 = rtplan.build_beam(gantry_angle = 120, meterset = 91) 38 | b2.conform_to_rectangle(25, 50, [0,0]) 39 | b3 = rtplan.build_beam(gantry_angle = 240, meterset = 71) 40 | b3.conform_to_rectangle(50, 25, [50,-50]) 41 | assert rtplan.beam_builders == [b1,b2,b3] 42 | 43 | rtplan.build() 44 | 45 | rtdose = sb.build_dose() 46 | for beam in rtplan.beam_builders: 47 | rtdose.add_lightfield(beam.rtbeam, beam.meterset) 48 | 49 | 50 | sb.write(tmpdir) 51 | print tmpdir 52 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tempdir="$(mktemp -d)" 4 | 5 | ./build_dicom.py --outdir "$tempdir" \ 6 | --patient-position HFS --values 0 --pixel_representation unsigned \ 7 | --values "sphere,-100,25,[50;86.6;0]" --values "box,100,25,[50;-86.6;0]" \ 8 | --voxelsize 4,3,4 --voxels 48,64,48 --modality CT \ 9 | --structure external \ 10 | --structure "sphere,Ball,25,CAVITY,[50;86.6;0]" \ 11 | --structure "box,Cube,25,CAVITY,[50;-86.6;0]" --modality RTSTRUCT \ 12 | --beams "[3;123;270]" \ 13 | --mlc-shape "1,circle,30" --jaw-shape "1,[60;60]" \ 14 | --mlc-shape "2,rectangle,60,60" --jaw-shape "2,[70;70;10;10]" \ 15 | --mlc-shape "3,rectangle,40,80" --jaw-shape "3,[40;80]" \ 16 | --nominal-energy 6 --modality RTPLAN \ 17 | --values 0 --values lightfield --modality RTDOSE 18 | 19 | fails=0 20 | for i in "$tempdir"/*.dcm ; do 21 | dciodvfy "$i" 2>&1 | grep -v "Error - Missing attribute Type 2C Conditional Element= Module=" | grep ^Error && fails=$(($fails+1)) 22 | done 23 | 24 | dcentvfy "$tempdir"/*.dcm || fails=$(($fails+1)) 25 | rm -rf "$tempdir" 26 | 27 | if [ "$fails" == 0 ] ; then 28 | echo Pass 29 | else 30 | echo FAIL - $fails fails 31 | exit -1 32 | fi 33 | -------------------------------------------------------------------------------- /test_coordinates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from coordinates import * 3 | 4 | import nose 5 | import numpy as np 6 | 7 | def all_rots(f): 8 | return all([np.abs(np.linalg.norm(f(i)[:3,:3]) - np.sqrt(3)) < np.sqrt(3)*(2.0**-50) for i in range(-360,360)]) 9 | 10 | def v(*x): 11 | assert len(x) == 3 12 | return np.array([x[0],x[1],x[2],1]).reshape((4,1)) 13 | 14 | def eq(a, b): 15 | return np.linalg.norm(a-b) < (1 + np.linalg.norm(a) * np.linalg.norm(b)) * 2.0**-40 16 | 17 | def test_Mfs(): 18 | assert (Mfs(0) == np.eye(4)).all() 19 | assert all_rots(lambda i: Mfs(i)) 20 | assert eq(Mfs(90) * v(1,0,0), v(0,-1,0)) 21 | assert eq(Mfs(90) * v(0,1,0), v(1,0,0)) 22 | 23 | def test_Mse(): 24 | assert (Mse(0,0) == np.eye(4)).all() 25 | assert all_rots(lambda i: Mse(0, i)) 26 | assert eq(Mse(13.8,0) * v(0,13.8,0), v(0,0,0)) 27 | assert eq(Mse(42.9,90) * v(0,43.9,0), v(1,0,0)) 28 | 29 | def test_Met(): 30 | assert (Met(0,0,0,0,0) == np.eye(4)).all() 31 | assert all_rots(lambda i: Met(0, 0, 0, i, 0)) 32 | assert all_rots(lambda i: Met(0, 0, 0, 0, i)) 33 | assert eq(Met(4,9,25,0,0) * v(4,9,25), v(0,0,0)) 34 | assert eq(Met(4,9,25,90,0) * v(4+1,9,25), v(1,0,0)) 35 | assert eq(Met(4,9,25,90,0) * v(4,9+1,25), v(0,0,-1)) 36 | assert eq(Met(4,9,25,90,0) * v(4,9,25+1), v(0,1,0)) 37 | assert eq(Met(4,9,25,0,90) * v(4+1,9,25), v(0,0,1)) 38 | assert eq(Met(4,9,25,0,90) * v(4,9+1,25), v(0,1,0)) 39 | assert eq(Met(4,9,25,0,90) * v(4,9,25+1), v(-1,0,0)) 40 | assert eq(Met(4,9,25,90,90) * v(4+1,9,25), v(0,0,1)) 41 | assert eq(Met(4,9,25,90,90) * v(4,9+1,25), v(1,0,0)) 42 | assert eq(Met(4,9,25,90,90) * v(4,9,25+1), v(0,1,0)) 43 | 44 | def test_Mtp(): 45 | assert (Mtp(0,0,0,0,0,0) == np.eye(4)).all() 46 | assert all_rots(lambda i: Mtp(0, 0, 0, i, 41, 12)) 47 | assert all_rots(lambda i: Mtp(0, 0, 0, 19, i, 41)) 48 | assert all_rots(lambda i: Mtp(0, 0, 0, -81, 93, i)) 49 | assert eq(Mtp(41,13,95,0,0,0) * v(41, 13, 95), v(0,0,0)) 50 | assert eq(Mtp(41,13,95,90,0,0) * v(41+1, 13, 95), v(1,0,0)) 51 | assert eq(Mtp(41,13,95,90,0,0) * v(41, 13+1, 95), v(0,0,-1)) 52 | assert eq(Mtp(41,13,95,90,0,0) * v(41, 13, 95+1), v(0,1,0)) 53 | assert eq(Mtp(41,13,95,0,90,0) * v(41+1, 13, 95), v(0,0,1)) 54 | assert eq(Mtp(41,13,95,0,90,0) * v(41, 13+1, 95), v(0,1,0)) 55 | assert eq(Mtp(41,13,95,0,90,0) * v(41, 13, 95+1), v(-1,0,0)) 56 | assert eq(Mtp(41,13,95,0,0,90) * v(41+1, 13, 95), v(0,-1,0)) 57 | assert eq(Mtp(41,13,95,0,0,90) * v(41, 13+1, 95), v(1,0,0)) 58 | assert eq(Mtp(41,13,95,0,0,90) * v(41, 13, 95+1), v(0,0,1)) 59 | assert eq(Mtp(41,13,95,90,90,0) * v(41+1, 13, 95), v(0,0,1)) 60 | assert eq(Mtp(41,13,95,90,90,0) * v(41, 13+1, 95), v(1,0,0)) 61 | assert eq(Mtp(41,13,95,90,90,0) * v(41, 13, 95+1), v(0,1,0)) 62 | assert eq(Mtp(41,13,95,90,0,90) * v(41+1, 13, 95), v(0,-1,0)) 63 | assert eq(Mtp(41,13,95,90,0,90) * v(41, 13+1, 95), v(0,0,-1)) 64 | assert eq(Mtp(41,13,95,90,0,90) * v(41, 13, 95+1), v(1,0,0)) 65 | assert eq(Mtp(41,13,95,90,90,90) * v(41+1, 13, 95), v(0,0,1)) 66 | assert eq(Mtp(41,13,95,90,90,90) * v(41, 13+1, 95), v(0,-1,0)) 67 | assert eq(Mtp(41,13,95,90,90,90) * v(41, 13, 95+1), v(1,0,0)) 68 | 69 | def test_Mfg(): 70 | assert (Mfg(0) == np.eye(4)).all() 71 | assert eq(Mfg(90) * v(1,0,0), v(0,0,1)) 72 | assert eq(Mfg(90) * v(0,1,0), v(0,1,0)) 73 | assert eq(Mfg(90) * v(0,0,1), v(-1,0,0)) 74 | assert all_rots(lambda i: Mfg(i)) 75 | 76 | def test_Mgb(): 77 | assert (Mgb(0,0) == np.eye(4)).all() 78 | assert eq(Mgb(100,0) * v(0,0,100), v(0,0,0)) 79 | assert eq(Mgb(100,90) * v(1,0,100), v(0,-1,0)) 80 | assert eq(Mgb(100,90) * v(0,1,100), v(1,0,0)) 81 | assert all_rots(lambda i: Mgb(0, i)) 82 | 83 | def test_Mbw(): 84 | assert (Mbw(0,0) == np.eye(4)).all() 85 | assert eq(Mbw(17.2, 0) * v(0,0,17.2), v(0,0,0)) 86 | assert eq(Mbw(17.2, 90) * v(0,1,17.2), v(1,0,0)) 87 | assert eq(Mbw(17.2, 90) * v(1,0,17.2), v(0,-1,0)) 88 | assert eq(Mbw(17.2, 90) * v(0,0,17.2+1), v(0,0,1)) 89 | 90 | def test_Mgr(): 91 | assert (Mgr(0, 0, 0, 0) == np.eye(4)).all() 92 | assert eq(Mgr(7, 19, 27, 0) * v(7,19,27), v(0,0,0)) 93 | assert eq(Mgr(0,0,0,90) * v(1,0,0), v(0,-1,0)) 94 | assert eq(Mgr(0,0,0,90) * v(0,1,0), v(1,0,0)) 95 | assert eq(Mgr(0,0,0,90) * v(0,0,1), v(0,0,1)) 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /viewer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raysearchlabs/dicomutils/c4811340625ae41a6b0d187e81580ff322b60d5d/viewer/__init__.py -------------------------------------------------------------------------------- /viewer/mlc.py: -------------------------------------------------------------------------------- 1 | from mayavi import mlab 2 | from traits.api import HasTraits, on_trait_change, Array, CFloat 3 | from traitsui.api import View 4 | import numpy as np 5 | from .. import coordinates 6 | from ..coordinates import transform3d 7 | 8 | class MLC(HasTraits): 9 | """ A class that shows an MLC (Multi-Leaf Collimator) 10 | """ 11 | 12 | source_distance = CFloat(500.0, 13 | desc='distance from the source to the furthest point on the MLC along the beam axis (bottom of the mlc)', 14 | enter_set=True, auto_set=False) 15 | leaf_positions = Array(float, value=np.zeros((2,1)), shape=(2,(1,None)), desc='the dynamic positions of the leaves', 16 | enter_set=True, auto_set=False) 17 | leaf_boundaries = Array(float, value=((0,1),), shape=(1,(2,None)), desc='the boundaires of the leaves, in the direction orthogonal to the movement', 18 | enter_set=True, auto_set=False) 19 | beam_limiting_device_angle = CFloat(0.0, desc="Beam Limiting Device Angle", 20 | enter_set=True, auto_set=False) 21 | gantry_angle = CFloat(0.0, desc="Gantry Angle", 22 | enter_set=True, auto_set=False) 23 | gantry_pitch_angle = CFloat(0.0, desc="Gantry Pitch Angle", 24 | enter_set=True, auto_set=False) 25 | sad = CFloat(1000.0, desc="Source-to-axis distance, in mm", 26 | enter_set=True, auto_set=False) 27 | thickness = CFloat(70.0, desc="MLC Thickness", 28 | enter_set=True, auto_set=False) 29 | 30 | _trimesh = None 31 | 32 | view = View('source_distance', 'leaf_positions', 'leaf_boundaries', 33 | 'beam_limiting_device_angle', 'gantry_angle', 'gantry_pitch_angle', 34 | 'sad', 'thickness', '_') 35 | 36 | @on_trait_change('source_distance,leaf_positions,leaf_boundaries,beam_limiting_device_angle,gantry_angle,gantry_pitch_angle,sad,thickness') 37 | def redraw(self): 38 | if hasattr(self, 'app') and self.app.scene._renderer is not None: 39 | self.display() 40 | #self.app.visualize_field() 41 | 42 | def display(self): 43 | """ 44 | Display the MLC in the 3D view. 45 | """ 46 | 47 | def leaf(pts, polys, xspan, yspan, zspan, sad): 48 | """ spans are tuples (min, max) """ 49 | ptsi = [[0,0,0],[1,0,0],[0,1,0],[1,1,0],[0,0,1],[1,0,1],[0,1,1],[1,1,1]] 50 | i0 = len(pts) 51 | for pt in ptsi: 52 | zp = zspan[pt[2]] 53 | xp = xspan[pt[0]] * zp / -sad 54 | yp = yspan[pt[1]] * zp / -sad 55 | 56 | pts.append([xp, yp, zp]) 57 | 58 | polys += [[xi+i0, yi+i0, zi+i0] for xi,yi,zi in 59 | [[2,1,0],[1,2,3],[4,5,6],[7,6,5], 60 | [4,1,0],[1,4,5],[2,3,6],[7,6,3], 61 | [4,2,0],[2,4,6],[1,3,5],[7,5,3]]] 62 | return pts, polys 63 | 64 | pts = [] 65 | polys = [] 66 | assert self.leaf_positions.shape[1]+1 == self.leaf_boundaries.shape[1] 67 | nleaves = self.leaf_boundaries.shape[1] - 1 68 | xmin = np.amin(self.leaf_positions) - 5 69 | xmax = np.amax(self.leaf_positions) + 5 70 | zspan = (-self.source_distance, -self.source_distance + self.thickness) 71 | for i in range(nleaves): 72 | leaf(pts, polys, (xmin, self.leaf_positions[0,i]), self.leaf_boundaries[0,i:i+2], zspan, self.sad) 73 | leaf(pts, polys, (self.leaf_positions[1,i], xmax), self.leaf_boundaries[0,i:i+2], zspan, self.sad) 74 | 75 | polys = np.array(polys) 76 | pts = np.array(pts) 77 | 78 | # pts are now in BLD coordinates. Transform to fixed coordinate system: 79 | M = (np.linalg.inv(coordinates.Mfg(self.gantry_pitch_angle, self.gantry_angle)) 80 | * np.linalg.inv(coordinates.Mgb(self.sad, self.beam_limiting_device_angle))) 81 | 82 | pts = transform3d(pts.T, M).T 83 | 84 | if self._trimesh is None: 85 | self._trimesh = mlab.triangular_mesh(pts[:,0], pts[:,1], pts[:,2], polys, color=(.5,.5,1)) 86 | else: 87 | self._trimesh.mlab_source.set(x=pts[:,0], y=pts[:,1], z=pts[:,2], triangles=polys) 88 | 89 | def move_camera_to_bev(self): 90 | # Okay, so there is a better solution to inclination and azimuth, but in 91 | # the interest of getting things done, I leave this as an 92 | # exercise to the reader to solve exactly. 93 | M = (np.linalg.inv(coordinates.Mfg(self.gantry_pitch_angle, self.gantry_angle)) 94 | * np.linalg.inv(coordinates.Mgb(self.sad, self.beam_limiting_device_angle))) 95 | p = transform3d([0,0,0], M) 96 | inclination = np.arccos(p[2]/np.linalg.norm(p)) * 180 / np.pi 97 | azimuth = np.arctan2(p[1], p[0]) * 180 / np.pi 98 | mlab.view(azimuth = azimuth[0], elevation = inclination[0], focalpoint = [0,0,0], 99 | distance = self.sad, roll = -self.beam_limiting_device_angle) 100 | 101 | def _test(mlc=None): 102 | import dicom 103 | import os 104 | rtplan = dicom.read_file(os.path.join(os.path.dirname(__file__), "..", "RTPLAN.dcm")) 105 | beam = rtplan.Beams[0] 106 | cp = beam.CPs[0] 107 | lp = cp.BLDPositions[2].LeafJawPositions 108 | lpb = np.atleast_2d(beam.BLDs[2].LeafPositionBoundaries) 109 | nleaves = lpb.shape[1] - 1 110 | if mlc == None: 111 | mlc=MLC() 112 | mlc.set(leaf_boundaries = lpb, 113 | leaf_positions = np.array([x for x in lp]).reshape((2,nleaves)), 114 | beam_limiting_device_angle = cp.BeamLimitingDeviceAngle, 115 | gantry_angle = cp.GantryAngle, 116 | gantry_pitch_angle = getattr(cp, 'GantryPitchAngle', 0), 117 | sad = beam.SourceAxisDistance, 118 | source_distance = beam.SourceAxisDistance / 2.0, 119 | thickness = beam.SourceAxisDistance * 0.05, 120 | ) 121 | 122 | mlc.display() 123 | 124 | 125 | if __name__ == '__main__': 126 | _test() 127 | -------------------------------------------------------------------------------- /viewer/treeview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from enthought.traits.api \ 4 | import HasTraits, Str, Regex, List, Instance, PythonValue, CInt, Property, on_trait_change 5 | from enthought.traits.ui.api \ 6 | import TreeEditor, TreeNode, View, Item, VSplit, \ 7 | HGroup, Handler, Group 8 | from enthought.traits.ui.menu \ 9 | import Menu, Action, Separator 10 | from enthought.traits.ui.wx.tree_editor \ 11 | import NewAction, CopyAction, CutAction, \ 12 | PasteAction, DeleteAction, RenameAction 13 | from collections import defaultdict 14 | 15 | import dicom 16 | 17 | # DATA CLASSES 18 | 19 | class SOPInstance(HasTraits): 20 | dicom_dataset = PythonValue 21 | sopinstanceuid = Property( depends_on = [ 'dicom_dataset' ] ) 22 | 23 | label = Property( depends_on = [ 'dicom_dataset' ] ) 24 | 25 | def _get_label(self): 26 | return "%s [%s]" % (dicom.UID.UID_dictionary[self.dicom_dataset.SOPClassUID][0].replace(" Storage",""), 27 | self.dicom_dataset.SOPInstanceUID) 28 | 29 | def _get_sopinstanceuid(self): 30 | if self.dicom_dataset != None: 31 | return str(self.dicom_dataset.SOPInstanceUID) 32 | else: 33 | return "N/A" 34 | # series = Instance(Series) 35 | 36 | class Image(SOPInstance): 37 | pass 38 | 39 | class RTImage(SOPInstance): 40 | # rtplan = Instance(RTPlan) 41 | # rttreatmentrecord = Instance(RTTreatmentRecord) 42 | pass 43 | 44 | class RTStructureSet(SOPInstance): 45 | images = List(Image) 46 | # doses = List(RTDose) 47 | 48 | class RTDose(SOPInstance): 49 | structure_sets = List(RTStructureSet) 50 | 51 | def _get_label(self): 52 | return "%s %s [%s]" % (dicom.UID.UID_dictionary[self.dicom_dataset.SOPClassUID][0].replace(" Storage",""), 53 | getattr(self.dicom_dataset, 'DoseSummationType', ''), 54 | self.dicom_dataset.SOPInstanceUID) 55 | # plan = Instance(RTPlan) 56 | 57 | class ControlPoint(HasTraits): 58 | dicom_dataset = PythonValue 59 | index = Property( depends_on = [ 'dicom_dataset' ] ) 60 | label = Property( depends_on = [ 'index' ] ) 61 | 62 | def _get_label(self): 63 | return "CP %i" % (self.index,) 64 | 65 | def _get_index(self): 66 | if hasattr(self.dicom_dataset, 'ControlPointIndex'): 67 | return self.dicom_dataset.ControlPointIndex 68 | else: 69 | return -1 70 | # beam = Instance(Beam) 71 | 72 | class Beam(HasTraits): 73 | control_points = List(ControlPoint) 74 | dicom_dataset = PythonValue 75 | name = Property( depends_on = [ 'dicom_dataset' ] ) 76 | 77 | def _get_name(self): 78 | return self.dicom_dataset.BeamName 79 | # plan = Instance(RTPlan) 80 | 81 | @on_trait_change("dicom_dataset") 82 | def update_control_points(self, obj, name, new): 83 | self.control_points = [ControlPoint(dicom_dataset = cp, beam = self) for cp in new.ControlPoints] 84 | 85 | class RTPlan(SOPInstance): 86 | doses = List(RTDose) 87 | structure_sets = List(RTStructureSet) 88 | rtimages = List(RTImage) 89 | beams = List(Beam) 90 | 91 | def _get_label(self): 92 | return "%s %s %s [%s]" % (dicom.UID.UID_dictionary[self.dicom_dataset.SOPClassUID][0].replace(" Storage",""), 93 | getattr(self.dicom_dataset, 'RTPlanLabel', ''), 94 | getattr(self.dicom_dataset, 'RTPlanName', ''), 95 | self.dicom_dataset.SOPInstanceUID) 96 | 97 | @on_trait_change("dicom_dataset") 98 | def update_beams(self, obj, name, new): 99 | self.beams = [Beam(dicom_dataset = beam, plan = self) for beam in new.Beams] 100 | 101 | # rttreatmentrecords = List # RTTreatmentRecord 102 | 103 | class RTTreatmentRecord(SOPInstance): 104 | plan = Instance(RTPlan) 105 | images = List(RTImage) 106 | 107 | specialsops = defaultdict(lambda: SOPInstance) 108 | specialsops.update({ 109 | '1.2.840.10008.5.1.4.1.1.481.1': RTImage, 110 | '1.2.840.10008.5.1.4.1.1.481.2': RTDose, 111 | '1.2.840.10008.5.1.4.1.1.481.3': RTStructureSet, 112 | '1.2.840.10008.5.1.4.1.1.481.4': RTTreatmentRecord, 113 | '1.2.840.10008.5.1.4.1.1.481.5': RTPlan, 114 | '1.2.840.10008.5.1.4.1.1.481.6': RTTreatmentRecord, 115 | '1.2.840.10008.5.1.4.1.1.481.7': RTTreatmentRecord, 116 | '1.2.840.10008.5.1.4.1.1.481.8': RTPlan, 117 | '1.2.840.10008.5.1.4.1.1.481.9': RTTreatmentRecord, 118 | }) 119 | 120 | class Series(HasTraits): 121 | sopinstances = List(SOPInstance) 122 | # study = Instance(Study) 123 | dicom_datasets = List(PythonValue) 124 | 125 | series_instance_uid = Str 126 | 127 | label = Property( depends_on = [ 'dicom_dataset' ] ) 128 | 129 | def _get_label(self): 130 | return "Series %s [%s]" % (self.dicom_datasets[0].Modality, self.series_instance_uid) 131 | 132 | @on_trait_change("dicom_datasets[]") 133 | def update_sopinstances(self, obj, name, old, new): 134 | print "Series.update_sopinstances()" 135 | sopuids = defaultdict(lambda: []) 136 | for x in self.dicom_datasets: 137 | sopuids[x.SOPInstanceUID].append(x) 138 | sd = {x.sopinstanceuid: x for x in self.sopinstances} 139 | newuids = [uid for uid in sopuids if uid not in sd] 140 | goneuids = [uid for uid in sd if uid not in sopuids] 141 | for uid in newuids: 142 | assert len(sopuids[uid]) == 1 143 | cls = specialsops[sopuids[uid][0].SOPClassUID] 144 | print "building %s" % (cls,) 145 | self.sopinstances.append(cls(dicom_dataset = sopuids[uid][0], series = self)) 146 | for uid in goneuids: 147 | self.sopinstances.pop(sd[uid]) 148 | 149 | class Study(HasTraits): 150 | series = List(Series) 151 | # patient = Instance(Patient) 152 | dicom_datasets = List(PythonValue) 153 | study_instance_uid = Str 154 | 155 | label = Property( depends_on = [ 'dicom_dataset' ] ) 156 | 157 | def _get_label(self): 158 | return "Study %s [%s]" % (self.dicom_datasets[0].StudyID, self.study_instance_uid) 159 | 160 | @on_trait_change("dicom_datasets[]") 161 | def update_series(self, obj, name, old, new): 162 | print "Study.update_series()" 163 | seriesuids = defaultdict(lambda: []) 164 | for x in self.dicom_datasets: 165 | seriesuids[x.SeriesInstanceUID].append(x) 166 | sd = {x.series_instance_uid: x for x in self.series} 167 | newuids = [uid for uid in seriesuids if uid not in sd] 168 | goneuids = [uid for uid in sd if uid not in seriesuids] 169 | updateduids = [uid for uid in sd if uid in seriesuids] 170 | for uid in newuids: 171 | self.series.append(Series(dicom_datasets = seriesuids[uid], 172 | study = self, 173 | series_instance_uid = uid)) 174 | for uid in goneuids: 175 | self.series.pop(sd[uid]) 176 | for uid in updateduids: 177 | sd[uid].dicom_datasets = seriesuids[uid] 178 | 179 | class Patient(HasTraits): 180 | studies = List(Study) 181 | dicom_datasets = List(PythonValue) 182 | 183 | patient_id = Str 184 | name = Str 185 | 186 | label = Property( depends_on = [ 'dicom_dataset' ] ) 187 | def _get_label(self): 188 | return "%s <%s>" % (self.name, self.patient_id) 189 | 190 | @on_trait_change("dicom_datasets[]") 191 | def update_studies(self, obj, name, old, new): 192 | print "Patient.update_studies()" 193 | studyuids = defaultdict(lambda: []) 194 | for x in self.dicom_datasets: 195 | studyuids[x.StudyInstanceUID].append(x) 196 | sd = {x.study_instance_uid: x for x in self.studies} 197 | newuids = [uid for uid in studyuids if uid not in sd] 198 | goneuids = [uid for uid in sd if uid not in studyuids] 199 | updateduids = [uid for uid in sd if uid in studyuids] 200 | for uid in newuids: 201 | self.studies.append(Study(dicom_datasets = studyuids[uid], 202 | patient = self, 203 | study_instance_uid = uid)) 204 | for uid in goneuids: 205 | self.studies.pop(sd[uid]) 206 | for uid in updateduids: 207 | sd[uid].dicom_datasets = studyuids[uid] 208 | 209 | class PatientList(HasTraits): 210 | patients = List(Patient) 211 | 212 | class Selection(HasTraits): 213 | plan = RTPlan 214 | beam = Beam 215 | control_point = ControlPoint 216 | dose = RTDose 217 | structure_set = RTStructureSet 218 | image = Image 219 | rtimage = RTImage 220 | series = Series 221 | study = Study 222 | patient = Patient 223 | 224 | 225 | class Root(HasTraits): 226 | patientlist = PatientList 227 | filenames = List(Str) 228 | selection = Selection 229 | 230 | _loaded_files = {} 231 | 232 | def get_patient_with_id(self, id): 233 | for patient in self.patientlist.patients: 234 | if patient.patient_id == id: 235 | return patient 236 | return None 237 | 238 | @on_trait_change("filenames[]") 239 | def filenames_changed(self, obj, name, old, new): 240 | for filename in new: 241 | self._loaded_files[filename] = dicom.read_file(filename) 242 | patient = self.get_patient_with_id(self._loaded_files[filename].PatientID) 243 | if patient == None: 244 | self.patientlist.patients.append(Patient(dicom_datasets = [self._loaded_files[filename]], 245 | name = self._loaded_files[filename].PatientName, 246 | patient_id = self._loaded_files[filename].PatientID)) 247 | else: 248 | patient.dicom_datasets.append(self._loaded_files[filename]) 249 | for filename in old: 250 | patient = self.get_patient_with_id(self._loaded_files[filename].PatientID) 251 | patient.dicom_datasets.pop(self._loaded_files[filename]) 252 | if len(patient.dicom_datasets) == 0: 253 | self.patientlist.patients.pop(patient) 254 | 255 | RTImage.add_class_trait('rtplan', Instance(RTPlan)) 256 | RTDose.add_class_trait('rtplan', Instance(RTPlan)) 257 | RTImage.add_class_trait('rttreatmentrecord', Instance(RTTreatmentRecord)) 258 | RTPlan.add_class_trait('rttreatmentrecord', Instance(RTTreatmentRecord)) 259 | SOPInstance.add_class_trait('series', Instance(Series)) 260 | Series.add_class_trait('study', Instance(Study)) 261 | Study.add_class_trait('patient', Instance(Patient)) 262 | Beam.add_class_trait('plan', Instance(RTPlan)) 263 | ControlPoint.add_class_trait('beam', Instance(Beam)) 264 | 265 | # View for objects that aren't edited 266 | no_view = View() 267 | 268 | # Actions used by tree editor context menu 269 | 270 | def_title_action = Action(name='Default title', 271 | action = 'object.default') 272 | 273 | import sys 274 | 275 | root = Root(patientlist=PatientList()) 276 | for fn in sys.argv[1:]: 277 | try: 278 | dicom.read_file(fn) 279 | root.filenames.append(fn) 280 | print "added %s" % (fn,) 281 | except: 282 | continue 283 | print "\n".join(root.filenames) 284 | 285 | patient_action = Action(name='Patient', action='handler.dump_patient(editor,object)') 286 | 287 | class TreeHandler ( Handler ): 288 | def dump_patient ( self, editor, object ): 289 | print 'dump_patient(%s)' % ( object, ) 290 | 291 | def on_tree_select(obj): 292 | print "on_tree_select(%s)" % (obj) 293 | if obj.__class__ is ControlPoint: 294 | root.selection.control_point = obj 295 | root.selection.beam = obj.beam 296 | root.selection.plan = obj.beam.plan 297 | root.selection.series = obj.beam.plan.series 298 | root.selection.study = obj.beam.plan.series.study 299 | root.selection.patient = obj.beam.plan.series.study.patient 300 | elif obj.__class__ is Beam: 301 | root.selection.control_point = None 302 | root.selection.beam = obj 303 | root.selection.plan = obj.plan 304 | root.selection.series = obj.plan.series 305 | root.selection.study = obj.plan.series.study 306 | root.selection.patient = obj.plan.series.study.patient 307 | elif obj.__class__ is RTPlan: 308 | root.selection.control_point = None 309 | root.selection.beam = None 310 | root.selection.plan = obj 311 | root.selection.series = obj.series 312 | root.selection.study = obj.series.study 313 | root.selection.patient = obj.series.study.patient 314 | elif obj.__class__ is Series: 315 | root.selection.control_point = None 316 | root.selection.beam = None 317 | root.selection.plan = None 318 | root.selection.series = obj 319 | root.selection.study = obj.study 320 | root.selection.patient = obj.study.patient 321 | elif obj.__class__ is Study: 322 | root.selection.control_point = None 323 | root.selection.beam = None 324 | root.selection.plan = None 325 | root.selection.series = None 326 | root.selection.study = obj 327 | root.selection.patient = obj.patient 328 | elif obj.__class__ is Patient: 329 | root.selection.control_point = None 330 | root.selection.beam = None 331 | root.selection.plan = None 332 | root.selection.series = None 333 | root.selection.study = None 334 | root.selection.patient = obj 335 | else: 336 | root.selection.control_point = None 337 | root.selection.beam = None 338 | root.selection.plan = None 339 | root.selection.series = None 340 | root.selection.study = None 341 | root.selection.patient = None 342 | print (root.selection.control_point, 343 | root.selection.beam, 344 | root.selection.plan, 345 | root.selection.series, 346 | root.selection.study, 347 | root.selection.patient, 348 | ) 349 | 350 | 351 | # Tree editor 352 | tree_editor = TreeEditor( 353 | nodes = [ 354 | TreeNode(node_for = [ PatientList ], 355 | auto_open = True, 356 | children = 'patients', 357 | label = '=Patients', 358 | view = no_view), 359 | TreeNode(node_for = [ Patient ], 360 | auto_open = True, 361 | children = 'studies', 362 | label = 'label', 363 | view = no_view), 364 | TreeNode(node_for = [ Study ], 365 | auto_open = True, 366 | children = 'series', 367 | label = 'label', 368 | view = no_view), 369 | TreeNode(node_for = [ Series ], 370 | auto_open = False, 371 | children = 'sopinstances', 372 | label = 'label', 373 | view = no_view), 374 | TreeNode(node_for = [ RTPlan ], 375 | auto_open = False, 376 | children = 'beams', 377 | label = 'label', 378 | view = no_view), 379 | TreeNode(node_for = [ SOPInstance ], 380 | auto_open = False, 381 | children = '', 382 | label = 'label', 383 | view = no_view), 384 | TreeNode(node_for = [ Beam ], 385 | auto_open = False, 386 | children = 'control_points', 387 | label = 'name', 388 | view = no_view), 389 | TreeNode(node_for = [ ControlPoint ], 390 | auto_open = False, 391 | children = '', 392 | label = 'label', 393 | view = no_view), 394 | ], 395 | on_select = on_tree_select 396 | ) 397 | 398 | # The main view 399 | view = View( 400 | Group( 401 | Item( 402 | name = 'patientlist', 403 | id = 'patientlist', 404 | editor = tree_editor, 405 | resizable = True ), 406 | orientation = 'vertical', 407 | show_labels = True, 408 | show_left = False, ), 409 | title = 'Patients', 410 | id = \ 411 | 'dicomutils.viewer.tree', 412 | dock = 'horizontal', 413 | drop_class = HasTraits, 414 | handler = TreeHandler(), 415 | buttons = [ 'Undo', 'OK', 'Cancel' ], 416 | resizable = True, 417 | width = .3, 418 | height = .3 ) 419 | 420 | 421 | 422 | if __name__ == '__main__': 423 | root.configure_traits( view = view ) 424 | 425 | --------------------------------------------------------------------------------