def create_trip_exec_simple(self, no_output=False): """ Generates the .exec script and stores it into self.trip_exec. """ logger.info("Generating the trip_exec script...") oar_list = self.oar_list targets = self.targets fields = self.plan.get_fields() projectile = fields[0].get_projectile() output = [] output.extend(self.create_exec_header()) output.extend(self.create_exec_load_data_files(projectile)) output.extend(self.create_exec_field(fields)) output.extend(self.create_exec_oar(oar_list)) dosecube = None if len(targets) > 1: dosecube = DosCube(self.images) for i, voi in enumerate(targets): temp = DosCube(self.images) dose_level = int(voi.get_dose() / self.target_dose * 1000) if dose_level == 0: dose_level = -1 temp.load_from_structure(voi.get_voi().get_voi_data(), dose_level) if i == 0: dosecube = temp * 1 else: dosecube.merge_zero(temp) dosecube.cube[dosecube.cube == -1] = int(0) if not self.plan.get_target_dose_cube() is None: dosecube = self.plan.get_target_dose_cube() if dosecube is not None: if not no_output: dosecube.write(os.path.join(self.path, "target_dose.dos")) output.extend(self.create_exec_plan(incube="target_dose")) else: output.extend(self.create_exec_plan()) if self.plan.get_optimize(): output.extend(self.create_exec_opt()) name = self.plan_name output.extend(self.create_exec_output(name, fields)) out = "\n".join(output) + "\nexit\n" self.trip_exec = out
class Voi: """ This is a class for handling volume of interests (VOIs). This class can e.g. be found inside the VdxCube object. VOIs may for instance be organs (lung, eye...) or targets (PTV, GTV...), or any other volume of interest. """ sagital = 2 #: deprecated, backwards compability to pytripgui, do not use. sagittal = 2 #: id for sagittal view coronal = 1 #: id for coronal view def __init__(self, name, cube=None): self.cube = cube self.name = name self.is_concated = False self.type = 90 self.slice_z = [] self.slices = {} self.color = [0, 230, 0] # default colour self.define_colors() def create_copy(self, margin=0): """ Returns an independent copy of the Voi object :param margin: (unused) :returns: a deep copy of the Voi object """ voi = copy.deepcopy(self) if not margin == 0: pass return voi def get_voi_cube(self): """ This method returns a DosCube object with value 1000 in each voxel within the Voi and zeros elsewhere. It can be used as a mask, for selecting certain voxels. The function may take some time to execute the first invocation, but is faster for subsequent calls, due to caching. :returns: a DosCube object which holds the value 1000 in those voxels which are inside the Voi. """ if hasattr(self, "voi_cube"): # caching: checks if class has voi_cube attribute # TODO: add parameter as argument to this function. Note, this needs to be compatible with # caching the cube. E.g. the method might be called twice with different arguments. return self.voi_cube self.voi_cube = DosCube(self.cube) self.voi_cube.load_from_structure(self, 1000) return self.voi_cube def add_slice(self, slice): """ Add another slice to this VOI, and update self.slice_z table. :param Slice slice: the Slice object to be appended. """ key = int(slice.get_position() * 100) self.slice_z.append(key) self.slices[key] = slice def get_name(self): """ :returns: The name of this VOI. """ return self.name def calculate_bad_angles(self, voi): """ (Not implemented.) """ pass def concat_to_3d_polygon(self): """ Concats all contours into a single contour, and writes all data points to sefl.polygon3d. """ self.concat_contour() data = [] for slice in self.slices: data.extend(self.slices[slice].contour[0].contour) self.polygon3d = np.array(data) def get_3d_polygon(self): """ Returns a list of points rendering a 3D polygon of this VOI, which is stored in sefl.polygon3d. If this attibute does not exist, create it. """ if not hasattr(self, "polygon3d"): self.concat_to_3d_polygon() return self.polygon3d def create_point_tree(self): """ Concats all contours. Writes a list of points into self.points describing this VOI. """ points = {} self.concat_contour() slice_keys = sorted(self.slices.keys()) for key in slice_keys: contour = self.slices[key].contour[0].contour p = {} for x in contour: p[x[0], x[1], x[2]] = [] points.update(p) n_slice = len(slice_keys) last_contour = None for i, key in enumerate(slice_keys): contour = self.slices[key].contour[0].contour n_points = len(contour) if i < n_slice - 1: next_contour = self.slices[slice_keys[i + 1]].contour[0].contour else: next_contour = None for j, point in enumerate(contour): j2 = (j + 1) % (n_points - 2) point2 = contour[j2] points[(point[0], point[1], point[2])].append(point2) points[(point2[0], point2[1], point2[2])].append(point) if next_contour is not None: point3 = pytrip.res.point.get_nearest_point(point, next_contour) points[(point[0], point[1], point[2])].append(point3) points[(point3[0], point3[1], point3[2])].append(point) if last_contour is not None: point4 = pytrip.res.point.get_nearest_point(point, last_contour) if point4 not in points[(point[0], point[1], point[2])]: points[(point[0], point[1], point[2])].append(point4) points[(point4[0], point4[1], point4[2])].append(point) last_contour = contour self.points = points def get_2d_projection_on_basis(self, basis, offset=None): """ (TODO: Documentation) """ a = np.array(basis[0]) b = np.array(basis[1]) self.concat_contour() bas = np.array([a, b]) data = self.get_3d_polygon() product = np.dot(data, np.transpose(bas)) compare = self.cube.pixel_size filtered = pytriplib.filter_points(product, compare / 2.0) filtered = np.array(sorted(filtered, key=cmp_to_key(_voi_point_cmp))) filtered = pytriplib.points_to_contour(filtered) product = filtered if offset is not None: offset_proj = np.array([np.dot(offset, a), np.dot(offset, b)]) product = product[:] - offset_proj return product def get_2d_slice(self, plane, depth): """ Gets a 2d Slice object from the contour in either sagittal or coronal plane. Contours will be concated. :param int plane: either self.sagittal or self.coronal :param float depth: position of plane :returns: a Slice object. """ self.concat_contour() points1 = [] points2 = [] for key in sorted(self.slice_z): slice = self.slices[key] if plane is self.sagittal: point = sorted( pytriplib.slice_on_plane(np.array(slice.contour[0].contour), plane, depth), key=lambda x: x[1]) elif plane is self.coronal: point = sorted( pytriplib.slice_on_plane(np.array(slice.contour[0].contour), plane, depth), key=lambda x: x[0]) if len(point) > 0: points2.append(point[-1]) if len(point) > 1: points1.append(point[0]) s = None if len(points1) > 0: points1.extend(reversed(points2)) points1.append(points1[0]) s = Slice(cube=self.cube) s.add_contour(Contour(points1)) return s def define_colors(self): """ Creates a list of default colours [R,G,B] in self.colours. """ self.colors = [] self.colors.append([0, 0, 255]) self.colors.append([0, 128, 0]) self.colors.append([0, 255, 0]) self.colors.append([255, 0, 0]) self.colors.append([0, 128, 128]) self.colors.append([255, 255, 0]) def calculate_center(self): """ Calculates the center of gravity for the VOI. :returns: A numpy array[x,y,z] with positions in [mm] """ if hasattr(self, "center_pos"): return self.center_pos self.concat_contour() tot_volume = 0.0 center_pos = np.array([0.0, 0.0, 0.0]) for key in self.slices: center, area = self.slices[key].calculate_center() tot_volume += area center_pos += area * center self.center_pos = center_pos / tot_volume return center_pos / tot_volume def get_color(self, i=None): """ :param int i: selects a colour, default if None. :returns: a [R,G,B] list. """ if i is None: return self.color return self.colors[i % len(self.colors)] def set_color(self, color): """ :param [3*int]: set a color [R,G,B]. """ self.color = color def create_dicom_label(self): """ Based on self.name and self.type, a Dicom ROI_LABEL is generated. :returns: a Dicom ROI_LABEL """ roi_label = Dataset() roi_label.ROIObservationLabel = self.name roi_label.RTROIInterpretedType = self.get_roi_type_name(self.type) return roi_label def create_dicom_structure_roi(self): """ Based on self.name, an empty Dicom ROI is generated. :returns: a Dicom ROI. """ roi = Dataset() roi.ROIName = self.name return roi def create_dicom_contour_data(self, i): """ Based on self.slices, Dicom conours are generated for the Dicom ROI. :returns: Dicom ROI_CONTOURS """ roi_contours = Dataset() contours = [] for slice in self.slices.values(): contours.extend(slice.create_dicom_contours()) roi_contours.Contours = Sequence(contours) roi_contours.ROIDisplayColor = self.get_color(i) return roi_contours def read_vdx_old(self, content, i): """ Reads a single VOI from Voxelplan .vdx data from 'content', assuming a legacy .vdx format. VDX format 1.2. :params [str] content: list of lines with the .vdx content :params int i: line number to the list. :returns: current line number, after parsing the VOI. """ line = content[i] items = line.split() self.name = items[1] self.type = int(items[3]) i += 1 while i < len(content): line = content[i] if re.match("voi", line) is not None: break if re.match("slice#", line) is not None: s = Slice(cube=self.cube) i = s.read_vdx_old(content, i) if self.cube is not None: for cont1 in s.contour: for cont2 in cont1.contour: cont2[2] = self.cube.slice_to_z(cont2[2]) # change from slice number to mm if s.get_position() is None: raise Exception("cannot calculate slice position") # TODO investigate why 100 multiplier is needed if self.cube is not None: key = 100 * int((float(s.get_position()) - min(self.cube.slice_pos))) else: key = 100 * int(s.get_position()) self.slice_z.append(key) self.slices[key] = s if re.match("#TransversalObjects", line) is not None: pass # slices = int(line.split()[1]) # TODO holds information about number of skipped slices i += 1 return i - 1 def read_vdx(self, content, i): """ Reads a single VOI from Voxelplan .vdx data from 'content'. Format 2.0 :params [str] content: list of lines with the .vdx content :params int i: line number to the list. :returns: current line number, after parsing the VOI. """ line = content[i] self.name = ' '.join(line.split()[1:]) number_of_slices = 10000 i += 1 while i < len(content): line = content[i] if re.match("key", line) is not None: self.key = line.split()[1] elif re.match("type", line) is not None: self.type = int(line.split()[1]) elif re.match("number_of_slices", line) is not None: number_of_slices = int(line.split()[1]) elif re.match("slice", line) is not None: s = Slice(cube=self.cube) i = s.read_vdx(content, i) if s.get_position() is None: raise Exception("cannot calculate slice position") if self.cube is None: raise Exception("cube not loaded") key = int((float(s.get_position()) - min(self.cube.slice_pos)) * 100) self.slice_z.append(key) self.slices[key] = s elif re.match("voi", line) is not None: break elif len(self.slices) >= number_of_slices: break i += 1 return i - 1 def get_roi_type_number(self, type_name): """ :returns: 1 if GTV or CTV, else 0. """ if type_name == 'EXTERNAL': return 0 # TODO: should be 10? elif type_name == 'AVOIDANCE': return 0 elif type_name == 'ORGAN': return 0 elif type_name == 'GTV': return 1 elif type_name == 'CTV': return 1 else: return 0 def get_roi_type_name(self, type_id): """ :returns: The type name of the ROI. """ if type_id == 10: return "EXTERNAL" elif type_id == 2: return 'AVOIDANCE' elif type_id == 1: return 'CTV' elif type_id == 0: return 'other' return '' def read_dicom(self, info, data): """ Reads a single ROI (= VOI) from a Dicom data set. :param info: (not used) :param Dicom data: Dicom ROI object which contains the contours. """ if "Contours" not in data.dir() and "ContourSequence" not in data.dir(): return self.type = self.get_roi_type_number(np.typename) self.color = data.ROIDisplayColor if "Contours" in data.dir(): contours = data.Contours else: contours = data.ContourSequence for i, contour in enumerate(contours): key = int((float(contour.ContourData[2]) - min(self.cube.slice_pos)) * 100) if key not in self.slices: self.slices[key] = Slice(cube=self.cube) self.slice_z.append(key) self.slices[key].add_dicom_contour(contour) def get_thickness(self): """ :returns: thickness of slice in [mm]. If there is only one slice, 3 mm is returned. """ if len(self.slice_z) <= 1: return 3 # TODO: what is this? And shoudn't it be float? return abs(float(self.slice_z[1]) - float(self.slice_z[0])) / 100 def to_voxel_string(self): """ Creates the Voxelplan formatted text, which can be written into a .vdx file (format 2.0). :returns: a str holding the all lines needed for a Voxelplan formatted file. """ if len(self.slices) is 0: return "" out = "\n" out += "voi %s\n" % (self.name.replace(" ", "_")) out += "key empty\n" out += "type %s\n" % self.type out += "\n" out += "contours\n" out += "reference_frame\n" out += " origin 0.000 0.000 0.000\n" out += " point_on_x_axis 1.000 0.000 0.000\n" out += " point_on_y_axis 0.000 1.000 0.000\n" out += " point_on_z_axis 0.000 0.000 1.000\n" out += "number_of_slices %d\n" % self.number_of_slices() out += "\n" i = 0 thickness = self.get_thickness() for k in self.slice_z: sl = self.slices[k] pos = sl.get_position() out += "slice %d\n" % i out += "slice_in_frame %.3f\n" % pos out += "thickness %.3f reference " \ "start_pos %.3f stop_pos %.3f\n" % \ (thickness, pos - 0.5 * thickness, pos + 0.5 * thickness) out += "number_of_contours %d\n" % \ self.slices[k].number_of_contours() out += self.slices[k].to_voxel_string() i += 1 return out def get_row_intersections(self, pos): """ (TODO: Documentation needed) """ slice = self.get_slice_at_pos(pos[2]) if slice is None: return None return np.sort(slice.get_intersections(pos)) def get_slice_at_pos(self, z): """ Returns nearest VOI Slice at position z. :param float z: position z in [mm] :returns: a Slice object found at position z. """ thickness = self.get_thickness() / 2 * 100 for key in self.slices.keys(): key = key low = z * 100 - thickness high = z * 100 + thickness if (low < key < 100 * z) or (high > key >= 100 * z): return self.slices[key] return None def number_of_slices(self): """ :returns: number of slices covered by this VOI. """ return len(self.slices) def concat_contour(self): """ Concat all contours in all slices found in this VOI. """ if not self.is_concated: for k in self.slices.keys(): self.slices[k].concat_contour() self.is_concated = True def get_min_max(self): """ Set self.temp_min and self.temp_max if they dont exist. :returns: minimum and maximum x y coordinates in Voi. """ temp_min, temp_max = None, None if hasattr(self, "temp_min"): return self.temp_min, self.temp_max for key in self.slices: if temp_min is None: temp_min, temp_max = self.slices[key].get_min_max() else: min1, max1 = self.slices[key].get_min_max() temp_min = pytrip.res.point.min_list(temp_min, min1) temp_max = pytrip.res.point.max_list(temp_max, max1) self.temp_min = temp_min self.temp_max = temp_max return temp_min, temp_max
def split_plan(self, plan=None): self.targets = [] self.oar_list = [] dose = 0 for voi in self.plan.get_vois(): if voi.is_oar(): self.oar_list.append(voi) if voi.is_target(): self.targets.append(voi) if voi.get_dose() > dose: dose = voi.get_dose() if not len(self.targets): raise InputError("No targets") if not len(self.plan.get_fields()): raise InputError("No fields") self.target_dose = dose if plan is None: plan = self.plan proj = [] self.projectile_dose_level = {} for field in plan.fields: if field.get_projectile() not in proj: proj.append(field.get_projectile()) self.projectile_dose_level[field.get_projectile()] = 0 if len(proj) > 1: self.mult_proj = True else: self.mult_proj = False if self.mult_proj: self.projectiles = {} for field in plan.fields: if field.get_projectile() not in self.projectiles.keys(): self.projectiles[field.get_projectile()] = { "target_dos": DosCube(self.images), "fields": [field], "name": field.get_projectile(), "projectile": field.get_projectile() } else: self.projectiles[field.get_projectile()]["fields"].append( field) self.target_dos = DosCube(self.images) for i, voi in enumerate(self.targets): temp = DosCube(self.images) voi_dose_level = int(voi.get_dose() / dose * 1000) temp.load_from_structure(voi.get_voi().get_voi_data(), 1) for projectile, data in self.projectiles.items(): dose_percent = self.plan.get_dose_percent(projectile) if not voi.get_dose_percent(projectile) is None: dose_percent = voi.get_dose_percent(projectile) proj_dose_lvl = int(voi.get_dose() / self.target_dose * dose_percent * 10) if self.projectile_dose_level[projectile] < proj_dose_lvl: self.projectile_dose_level[projectile] = proj_dose_lvl if proj_dose_lvl == 0: proj_dose_lvl = -1 if i == 0: data["target_dos"] = temp * proj_dose_lvl else: data["target_dos"].merge_zero(temp * proj_dose_lvl) if i == 0: self.target_dos = temp * voi_dose_level else: self.target_dos.merge_zero(temp * voi_dose_level) for projectile, data in self.projectiles.items(): data["target_dos"].cube[data["target_dos"].cube == -1] = int(0) self.plan.add_dose(data["target_dos"], "target_%s" % projectile) self.rest_dose = copy.deepcopy(self.target_dos)