def overlap_volume(oar, tv): """ Calculate the overlap volume of two rois :param oar: organ-at-risk as a "sets of points" formatted dictionary :type oar: dict :param tv: treatment volume as a "sets of points" formatted dictionary :type tv: dict :rtype: float """ intersection_volume = 0. all_z_values = [round(float(z), 2) for z in list(tv)] all_z_values = np.sort(all_z_values) thicknesses = np.abs(np.diff(all_z_values)) thicknesses = np.append(thicknesses, np.min(thicknesses)) all_z_values = all_z_values.tolist() for z in list(tv): # z in coord will not necessarily go in order of z, convert z to float to lookup thickness # also used to check for top and bottom slices, to add area of those contours if z in list(oar): thickness = thicknesses[all_z_values.index(round(float(z), 2))] shapely_tv = points_to_shapely_polygon(tv[z]) shapely_oar = points_to_shapely_polygon(oar[z]) if shapely_oar and shapely_tv: intersection_volume += shapely_tv.intersection( shapely_oar).area * thickness return round(intersection_volume / 1000., 2)
def union(rois): """ Calculate the geometric union of the provided rois :param rois: rois formatted as "sets of points" dictionaries :type rois: list :return: a "sets of points" dictionary representing the union of the rois :rtype: dict """ new_roi = {} all_z_values = [] for roi in rois: for z in list(roi): if z not in all_z_values: all_z_values.append(z) for z in all_z_values: if z not in list(new_roi): new_roi[z] = [] # Convert to shapely objects current_slice = None for roi in rois: # Make sure current roi has at least 3 points in z plane if z in list(roi) and len(roi[z][0]) > 2: if not current_slice: current_slice = points_to_shapely_polygon(roi[z]) else: current_slice = current_slice.union(points_to_shapely_polygon(roi[z])) if current_slice: if current_slice.type != 'MultiPolygon': current_slice = [current_slice] for polygon in current_slice: xy = polygon.exterior.xy x_coord = xy[0] y_coord = xy[1] points = [] for i in range(len(x_coord)): points.append([x_coord[i], y_coord[i], round(float(z), 2)]) new_roi[z].append(points) if hasattr(polygon, 'interiors'): for interior in polygon.interiors: xy = interior.coords.xy x_coord = xy[0] y_coord = xy[1] points = [] for i in range(len(x_coord)): points.append([x_coord[i], y_coord[i], round(float(z), 2)]) new_roi[z].append(points) else: # print('WARNING: no contour found for slice %s' % z) pass return new_roi
def centroid(roi): """ :param roi: a "sets of points" formatted dictionary :return: centroid or the roi in x, y, z dicom coordinates (mm) :rtype: list """ centroids = {'x': [], 'y': [], 'z': [], 'area': []} for z in list(roi): shapely_roi = points_to_shapely_polygon(roi[z]) if shapely_roi and shapely_roi.area > 0: slice_centroid = shapely_roi.centroid polygon_count = len(slice_centroid.xy[0]) for i in range(polygon_count): centroids['x'].append(slice_centroid.xy[0][i]) centroids['y'].append(slice_centroid.xy[1][i]) centroids['z'].append(float(z)) if polygon_count > 1: centroids['area'].append(shapely_roi[i].area) else: centroids['area'].append(shapely_roi.area) x = np.array(centroids['x']) y = np.array(centroids['y']) z = np.array(centroids['z']) w = np.array(centroids['area']) w_sum = np.sum(w) volumetric_centroid = [ float(np.sum(x * w) / w_sum), float(np.sum(y * w) / w_sum), float(np.sum(z * w) / w_sum) ] return volumetric_centroid
def volume(roi): """ :param roi: a "sets of points" formatted dictionary :return: volume in cm^3 of roi :rtype: float """ # oar and ptv are lists using str(z) as keys # each item is an ordered list of points representing a polygon # polygon n is inside polygon n-1, then the current accumulated polygon is # polygon n subtracted from the accumulated polygon up to and including polygon n-1 # Same method DICOM uses to handle rings and islands vol = 0. all_z_values = [round(float(z), 2) for z in list(roi)] all_z_values = np.sort(all_z_values) thicknesses = np.abs(np.diff(all_z_values)) thicknesses = np.append(thicknesses, np.min(thicknesses)) all_z_values = all_z_values.tolist() for z in list(roi): # z in coord will not necessarily go in order of z, convert z to float to lookup thickness # also used to check for top and bottom slices, to add area of those contours thickness = thicknesses[all_z_values.index(round(float(z), 2))] shapely_roi = points_to_shapely_polygon(roi[z]) if shapely_roi: vol += shapely_roi.area * thickness return round(vol / 1000., 2)
def cross_section(roi): """ Calculate the cross section of a given roi :param roi: a "sets of points" formatted dictionary :type roi: dict :return: max and median cross-sectional area of all slices in cm^2 :rtype: dict """ areas = [] for z in list(roi): shapely_roi = points_to_shapely_polygon(roi[z]) if shapely_roi and shapely_roi.area > 0: slice_centroid = shapely_roi.centroid polygon_count = len(slice_centroid.xy[0]) for i in range(polygon_count): if polygon_count > 1: areas.append(shapely_roi[i].area) else: areas.append(shapely_roi.area) areas = np.array(areas) area = { 'max': float(np.max(areas) / 100.), 'median': float(np.median(areas) / 100.) } return area
def is_point_inside_roi(point, roi): """ Check if a point is within an ROI :param point: x, y, z :type point: list :param roi:roi: a "sets of points" formatted dictionary :type roi: dict :return: Whether or not the poin is within the roi :rtype: bool """ z_keys = list(roi.keys()) roi_z = np.array([float(z) for z in z_keys]) if np.max(roi_z) > point[2] > np.min(roi_z): nearest_z_index = (np.abs(roi_z - point[2])).argmin() nearest_z_key = z_keys[nearest_z_index] if abs(float(nearest_z_key) - point[2]) < 0.5: # make sure point is within 0.5mm if len(roi[nearest_z_key]) > 2: # make sure there are 3 points to make a polygon shapely_roi = points_to_shapely_polygon(roi[nearest_z_key]) shapely_point = Point(point[0], point[1]) return shapely_point.within(shapely_roi) return False