Beispiel #1
0
    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
Beispiel #2
0
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
Beispiel #3
0
    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)