def _calc_symmetry(self, method: str, vert_position: float, horiz_position: float): vert_profile = SingleProfile( self.image. array[:, int(round(self.image.array.shape[1] * vert_position))]) horiz_profile = SingleProfile(self.image.array[ int(round(self.image.array.shape[0] * horiz_position)), :]) # calc sym from profile symmetry_calculation = SYMMETRY_EQUATIONS[method.lower()] vert_sym, vert_sym_array, vert_lt, vert_rt = symmetry_calculation( vert_profile) horiz_sym, horiz_sym_array, horiz_lt, horiz_rt = symmetry_calculation( horiz_profile) return { 'method': method, 'horizontal': { 'profile': horiz_profile, 'value': horiz_sym, 'array': horiz_sym_array, 'profile left': horiz_lt, 'profile right': horiz_rt, }, 'vertical': { 'profile': vert_profile, 'value': vert_sym, 'array': vert_sym_array, 'profile left': vert_lt, 'profile right': vert_rt, }, }
def test_interpolation(self): # centered field field = generate_open_field() # no interp p = SingleProfile(field.image[:, int(field.shape[1] / 2)], interpolation=Interpolation.NONE) self.assertEqual(len(p.values), len(field.image[:, int(field.shape[1] / 2)])) # linear interp p = SingleProfile(field.image[:, int(field.shape[1] / 2)], interpolation=Interpolation.LINEAR, interpolation_factor=10) self.assertEqual(len(p.values), len(field.image[:, int(field.shape[1] / 2)])*10) p = SingleProfile(field.image[:, int(field.shape[1] / 2)], interpolation=Interpolation.LINEAR, dpmm=1/field.pixel_size, interpolation_resolution_mm=0.1) # right length self.assertEqual(len(p.values), len(field.image[:, int(field.shape[1] / 2)])*field.pixel_size/0.1) # right dpmm self.assertEqual(p.dpmm, 10) # spline interp p = SingleProfile(field.image[:, int(field.shape[1] / 2)], interpolation=Interpolation.SPLINE, interpolation_factor=10) self.assertEqual(len(p.values), len(field.image[:, int(field.shape[1] / 2)])*10) p = SingleProfile(field.image[:, int(field.shape[1] / 2)], interpolation=Interpolation.SPLINE, dpmm=1/field.pixel_size, interpolation_resolution_mm=0.1) # right length self.assertEqual(len(p.values), len(field.image[:, int(field.shape[1] / 2)])*field.pixel_size/0.1) # right dpmm self.assertEqual(p.dpmm, 10)
def _calc_flatness(self, method: str, vert_position: float, horiz_position: float): vert_profile = SingleProfile( self.image. array[:, int(round(self.image.array.shape[1] * vert_position))]) horiz_profile = SingleProfile(self.image.array[ int(round(self.image.array.shape[0] * horiz_position)), :]) # calc flatness from profile flatness_calculation = FLATNESS_EQUATIONS[method.lower()] vert_flatness, vert_max, vert_min, vert_lt, vert_rt = flatness_calculation( vert_profile) horiz_flatness, horiz_max, horiz_min, horiz_lt, horiz_rt = flatness_calculation( horiz_profile) return { 'method': method, 'horizontal': { 'value': horiz_flatness, 'profile': horiz_profile, 'profile max': horiz_max, 'profile min': horiz_min, 'profile left': horiz_lt, 'profile right': horiz_rt, }, 'vertical': { 'value': vert_flatness, 'profile': vert_profile, 'profile max': vert_max, 'profile min': vert_min, 'profile left': vert_lt, 'profile right': vert_rt, }, }
def _get_reasonable_start_point(self): """Set the algorithm starting point automatically. Notes ----- The determination of an automatic start point is accomplished by finding the Full-Width-80%-Max. Finding the maximum pixel does not consistently work, esp. in the presence of a pin prick. The FW80M is a more consistent metric for finding a good start point. """ # sum the image along each axis within the central 1/3 (avoids outlier influence from say, gantry shots) top_third = int(self.image.array.shape[0] / 3) bottom_third = int(top_third * 2) left_third = int(self.image.array.shape[1] / 3) right_third = int(left_third * 2) central_array = self.image.array[top_third:bottom_third, left_third:right_third] x_sum = np.sum(central_array, 0) y_sum = np.sum(central_array, 1) # Calculate Full-Width, 80% Maximum center fwxm_x_point = SingleProfile(x_sum).fwxm_center(80) + left_third fwxm_y_point = SingleProfile(y_sum).fwxm_center(80) + top_third center_point = Point(fwxm_x_point, fwxm_y_point) return center_point
def _find_bb(self): """Find the BB within the radiation field. Iteratively searches for a circle-like object by lowering a low-pass threshold value until found. Returns ------- Point The weighted-pixel value location of the BB. """ def is_boxlike(array): """Whether the binary object's dimensions are symmetric, i.e. box-like""" ymin, ymax, xmin, xmax = get_bounding_box(array) y = abs(ymax - ymin) x = abs(xmax - xmin) if x > max(y * 1.05, y+3) or x < min(y * 0.95, y-3): return False return True # get initial starting conditions hmin = np.percentile(self.array, 5) hmax = self.array.max() spread = hmax - hmin max_thresh = hmax # search for the BB by iteratively lowering the low-pass threshold value until the BB is found. found = False while not found: try: lower_thresh = hmax - spread / 2 t = np.where((max_thresh > self) & (self >= lower_thresh), 1, 0) labeled_arr, num_roi = ndimage.measurements.label(t) roi_sizes, bin_edges = np.histogram(labeled_arr, bins=num_roi + 1) bw_node_cleaned = np.where(labeled_arr == np.argsort(roi_sizes)[-3], 1, 0) expected_fill_ratio = np.pi / 4 actual_fill_ratio = get_filled_area_ratio(bw_node_cleaned) if (expected_fill_ratio * 1.1 < actual_fill_ratio) or (actual_fill_ratio < expected_fill_ratio * 0.9): raise ValueError if not is_boxlike(bw_node_cleaned): raise ValueError except (IndexError, ValueError): max_thresh -= 0.05 * spread if max_thresh < hmin: raise ValueError("Unable to locate the BB") else: found = True # determine the center of mass of the BB inv_img = Image.load(self.array) inv_img.invert() x_arr = np.abs(np.average(bw_node_cleaned, weights=inv_img, axis=0)) x_com = SingleProfile(x_arr).fwxm_center(interpolate=True) y_arr = np.abs(np.average(bw_node_cleaned, weights=inv_img, axis=1)) y_com = SingleProfile(y_arr).fwxm_center(interpolate=True) return Point(x_com, y_com)
def find_mlc_peak(self, mlc_center): """Determine the center of the picket.""" mlc_rows = np.arange(mlc_center - self.sample_width, mlc_center + self.sample_width + 1) if self.settings.orientation == orientations['UD']: pix_vals = np.median(self.picket_array[mlc_rows, :], axis=0) else: pix_vals = np.median(self.picket_array[:, mlc_rows], axis=1) if max(pix_vals) > np.percentile(self.picket_array, 80): prof = SingleProfile(pix_vals) fw80mc = prof.get_FWXM_center(70, interpolate=True) return fw80mc + self.approximate_idx - self.spacing
def _determine_measured_gap(profile: np.ndarray) -> float: """Return the measured gap based on profile height""" mid_value = profile[int(len(profile) / 2)] prof = SingleProfile(profile, normalization_method=Normalization.NONE) if mid_value < profile.mean(): prof.invert() _, props = find_peaks(prof.values, max_number=1) if mid_value < profile.mean(): return -props['prominences'][0] else: return props['prominences'][0]
def flatness_elekta(profile: SingleProfile): """The Elekta specification for calculating flatness""" try: dmax = profile.field_calculation(field_width=0.8, calculation='max') dmin = profile.field_calculation(field_width=0.8, calculation='min') except ValueError: raise ValueError( "An error was encountered in the flatness calculation. The image is likely inverted. Try inverting the image before analysis with <instance>.image.invert()." ) flatness = 100 * (dmax / dmin) lt_edge, rt_edge = profile.field_edges() return flatness, dmax, dmin, lt_edge, rt_edge
def find_mlc_peak(self, mlc_center): """Determine the center of the picket.""" mlc_rows = np.arange(mlc_center - self.sample_width, mlc_center + self.sample_width + 1) if self.settings.orientation == orientations['UD']: pix_vals = np.median(self.picket_array[mlc_rows, :], axis=0) else: pix_vals = np.median(self.picket_array[:, mlc_rows], axis=1) if max(pix_vals) > np.percentile(self.picket_array, 80): prof = SingleProfile(pix_vals) fw80mc = prof.fwxm_center(70, interpolate=True) return fw80mc + self.approximate_idx - self.spacing
def find_bb(self): """Find the BB within the radiation field. Dervived from pylinac WL test. Looks at the central 60x60 pixels and finds a bb Returns ------- Point The weighted-pixel value location of the BB. """ span=20 bbox=[int(self.cax.y-span),int(self.cax.x-span),int(self.cax.y+span),int(self.cax.x+span)] subim=ndimage.gaussian_filter(np.array(self.array[bbox[0]:bbox[2],bbox[1]:bbox[3]],dtype=np.float),sigma=(1,1),order=0) self._bbox_max=np.max(subim) self._bbox_min=np.min(subim) hmin, hmax = np.percentile(subim, [10, 100.0]) spread = hmax - hmin max_thresh = hmax lower_thresh = hmax - spread*.2 # search for the BB by iteratively lowering the low-pass threshold value until the BB is found. found = False while not found: try: binary_arr = np.logical_and((subim <= max_thresh), (subim >= lower_thresh)) labeled_arr, num_roi = ndimage.measurements.label(binary_arr) roi_sizes, bin_edges = np.histogram(labeled_arr, bins=num_roi + 1) bw_bb_img = np.where(labeled_arr == np.argsort(roi_sizes)[-2], 1, 0) if not self._is_round(bw_bb_img): raise ValueError if not self._is_modest_size(bw_bb_img,subim.shape): raise ValueError if not self._is_symmetric(bw_bb_img): raise ValueError except (IndexError, ValueError): lower_thresh -= 0.05 * spread if lower_thresh < hmin: raise ValueError("Unable to locate the BB. Make sure the field edges do not obscure the BB and that there is no artifact in the images.") else: found = True # determine the center of mass of the BB x_arr = np.abs(np.average(subim*bw_bb_img, axis=0)) #plt.figure() #plt.plot(x_arr) #plt.figure() #plt.imshow(bw_bb_img) x_com = SingleProfile(np.array(x_arr,dtype=np.float)).fwxm_center(interpolate=True) y_arr = np.abs(np.average(subim*bw_bb_img, axis=1)) y_com = SingleProfile(y_arr).fwxm_center(interpolate=True) self.bb=Point(x_com+bbox[1], y_com+bbox[0])
def symmetry_pdq_iec(profile: SingleProfile): """Symmetry calculation by way of PDQ IEC""" values = profile.field_values(field_width=0.8) lt_edge, rt_edge = profile.field_edges(field_width=0.8) max_val = 0 sym_array = [] for lt_pt, rt_pt in zip(values, values[::-1]): val = max(abs(lt_pt / rt_pt), abs(rt_pt / lt_pt)) sym_array.append(val) if val > max_val: max_val = val symmetry = 100 * max_val return symmetry, sym_array, lt_edge, rt_edge
def symmetry_point_difference(profile: SingleProfile): """Calculation of symmetry by way of point difference equidistant from the CAX""" values = profile.field_values(field_width=0.8) lt_edge, rt_edge = profile.field_edges(field_width=0.8) cax = profile.fwxm_center() dcax = profile.values[cax] max_val = 0 sym_array = [] for lt_pt, rt_pt in zip(values, values[::-1]): val = 100 * abs(lt_pt - rt_pt) / dcax sym_array.append(val) if val > max_val: max_val = val symmetry = max_val return symmetry, sym_array, lt_edge, rt_edge
def median_profiles(self): """Return two median profiles from the open and dmlc image. For visual comparison.""" # dmlc median profile dmlc_prof = SingleProfile(np.median(self.image_dmlc, axis=0)) dmlc_prof.stretch() # open median profile open_prof = SingleProfile(np.median(self.image_open, axis=0)) open_prof.stretch() # normalize the profiles to near the same value norm_val = np.percentile(dmlc_prof.values, 90) dmlc_prof.normalize(norm_val) norm_val = np.percentile(open_prof.values, 90) open_prof.normalize(norm_val) return dmlc_prof, open_prof
def _get_profile(self, plane, position): """Get a profile at the given position along the specified plane.""" if not self._img_is_loaded: raise AttributeError("An image has not yet been loaded") position = self._convert_position(position, plane) # if position == 'auto': # y, x = self._determine_center() # else: # if _is_crossplane(plane): # self._check_position_inbounds(position, plane) # y = position # elif _is_inplane(plane): # self._check_position_inbounds(position, plane) # x = position if _is_crossplane(plane): prof = SingleProfile(self.image[position[0], :]) elif _is_inplane(plane): prof = SingleProfile(self.image[:, position[0]]) return prof
def to_profiles( self, n_detectors_row: int = 63, **kwargs ) -> Tuple[SingleProfile, SingleProfile, SingleProfile, SingleProfile]: """Convert the SNC data to SingleProfiles. These can be analyzed directly or passed to other modules like flat/sym. Parameters ---------- n_detectors_row : int The number of detectors in a given row. Note that they Y profile includes 2 extra detectors from the other 3. """ x_prof = SingleProfile(self.integrated_dose[:n_detectors_row], **kwargs) y_prof = SingleProfile( self.integrated_dose[n_detectors_row:2 * n_detectors_row + 2], **kwargs) pos_prof = SingleProfile( self.integrated_dose[2 * n_detectors_row + 2:3 * n_detectors_row + 2], **kwargs) neg_prof = SingleProfile( self.integrated_dose[3 * n_detectors_row + 2:4 * n_detectors_row + 2], **kwargs) return x_prof, y_prof, pos_prof, neg_prof
def test_normalization(self): array = np.random.rand(1, 100).squeeze() # don't apply normalization max_v = array.max() p = SingleProfile(array, normalization_method=Normalization.NONE, interpolation=Interpolation.NONE, ground=False) self.assertEqual(max_v, p.values.max()) # apply max norm p = SingleProfile(array, normalization_method=Normalization.MAX, interpolation=Interpolation.NONE) self.assertEqual(1.0, p.values.max()) # make sure interpolation doesn't affect the norm p = SingleProfile(array, normalization_method=Normalization.MAX, interpolation=Interpolation.LINEAR) self.assertEqual(1.0, p.values.max()) # test out a real field field = generate_open_field() p = SingleProfile(field.image[:, 500], normalization_method=Normalization.MAX) self.assertEqual(1.0, p.values.max()) # filtered beam center is less than max value p = SingleProfile(field.image[:, 500], normalization_method=Normalization.BEAM_CENTER) self.assertGreaterEqual(p.values.max(), 1.0)
def test_geometric_center(self): # centered field field = generate_open_field() p = SingleProfile(field.image[:, int(field.shape[1]/2)], interpolation=Interpolation.NONE) self.assertAlmostEqual(p.geometric_center()['index (exact)'], field.shape[0]/2, delta=1) # offset field should still be centered field = generate_open_field(center=(20, 20)) p = SingleProfile(field.image[:, int(field.shape[1]/2)], interpolation=Interpolation.NONE) self.assertAlmostEqual(p.geometric_center()['index (exact)'], field.shape[0]/2, delta=1)
def test_beam_center(self): # centered field field = generate_open_field() p = SingleProfile(field.image[:, int(field.shape[1]/2)], interpolation=Interpolation.NONE) self.assertAlmostEqual(p.beam_center()['index (exact)'], field.shape[0]/2, delta=1) # offset field field = generate_open_field(center=(10, 10)) p = SingleProfile(field.image[:, int(field.shape[1]/2)], interpolation=Interpolation.NONE) self.assertAlmostEqual(p.beam_center()['index (exact)'], 422, delta=1)
def _determine_center(self, plane): """Automatically find the center of the field based on FWHM.""" if not self._img_is_loaded: raise AttributeError("An image has not yet been loaded") self.image.check_inversion() self.image.ground() col_prof = np.median(self.image, 0) col_prof = SingleProfile(col_prof) row_prof = np.median(self.image, 1) row_prof = SingleProfile(row_prof) x_cen = col_prof.fwxm_center() y_cen = row_prof.fwxm_center() if _is_crossplane(plane): return y_cen elif _is_inplane(plane): return x_cen elif _is_both_planes(plane): return y_cen, x_cen
def picket_fence_helperf(args): '''This function is used in order to prevent memory problems''' temp_folder = args["temp_folder"] file_path = args["file_path"] clip_box = args["clip_box"] py_filter = args["py_filter"] num_pickets = args["num_pickets"] sag = args["sag"] mlc = args["mlc"] invert = args["invert"] orientation = args["orientation"] w = args["w"] imgdescription = args["imgdescription"] station = args["station"] displayname = args["displayname"] acquisition_datetime = args["acquisition_datetime"] general_functions.set_configuration( args["config"]) # Transfer to this process # Chose module: if mlc in ["Varian_80", "Elekta_80", "Elekta_160"]: use_original_pylinac = "False" else: use_original_pylinac = "True" # Collect data for "save results" dicomenergy = general_functions.get_energy_from_imgdescription( imgdescription) user_machine, user_energy = general_functions.get_user_machine_and_energy( station, dicomenergy) machines_and_energies = general_functions.get_machines_and_energies( general_functions.get_treatmentunits_picketfence()) tolerances = general_functions.get_tolerance_user_machine_picketfence( user_machine) # If user_machne has specific tolerance if not tolerances: action_tolerance, tolerance, generate_pdf_report = general_functions.get_settings_picketfence( ) else: action_tolerance, tolerance, generate_pdf_report = tolerances[0] tolerance = float(tolerance) action_tol = float(action_tolerance) save_results = { "user_machine": user_machine, "user_energy": user_energy, "machines_and_energies": machines_and_energies, "displayname": displayname } # Import either original pylinac module or the modified module if use_original_pylinac == "True": from pylinac import PicketFence as PicketFence # Original pylinac analysis else: if __name__ == '__main__' or parent_module.__name__ == '__main__': from python_packages.pylinac.picketfence_modified import PicketFence as PicketFence else: from .python_packages.pylinac.picketfence_modified import PicketFence as PicketFence try: pf = PicketFence(file_path, filter=py_filter) except Exception as e: return template("error_template", { "error_message": "Module PicketFence cannot calculate. " + str(e) }) # Here we force pixels to background outside of box: if clip_box != 0: try: pf.image.check_inversion_by_histogram(percentiles=[ 4, 50, 96 ]) # Check inversion otherwise this might not work general_functions.clip_around_image(pf.image, clip_box) except Exception as e: return template( "error_template", {"error_message": "Unable to apply clipbox. " + str(e)}) # Now invert if needed if invert: try: pf.image.invert() except Exception as e: return template( "error_template", {"error_message": "Unable to invert the image. " + str(e)}) # Now analyze try: if use_original_pylinac == "True": hdmlc = True if mlc == "Varian_120HD" else False pf.analyze(tolerance=tolerance, action_tolerance=action_tol, hdmlc=hdmlc, sag_adjustment=float(sag), num_pickets=num_pickets, orientation=orientation) else: pf.analyze(tolerance=tolerance, action_tolerance=action_tol, mlc_type=mlc, sag_adjustment=float(sag), num_pickets=num_pickets, orientation=orientation) except Exception as e: return template( "error_template", {"error_message": "Picket fence module cannot analyze. " + str(e)}) # Added an if clause to tell if num of mlc's are not the same on all pickets: num_mlcs = len(pf.pickets[0].mlc_meas) for p in pf.pickets: if len(p.mlc_meas) != num_mlcs: return template( "error_template", { "error_message": "Not all pickets have the same number of leaves. " + "Probably your image si too skewed. Rotate your collimator a bit " + "and try again. Use the jaws perpendicular to MLCs to set the right " + "collimator angle." }) error_array = np.array([]) max_error = [] max_error_leaf = [] passed_tol = [] picket_offsets = [] picket_nr = pf.num_pickets for k in pf.pickets.pickets: error_array = np.concatenate((error_array, k.error_array)) max_error.append(k.max_error) max_err_leaf_ind = np.argmax(k.error_array) max_error_leaf.append(max_err_leaf_ind) passed_tol.append("Passed" if k.passed else "Failed") picket_offsets.append(k.dist2cax) # Plot images if pf.settings.orientation == "Left-Right": fig_pf = Figure(figsize=(9, 10), tight_layout={"w_pad": 0}) else: fig_pf = Figure(figsize=(9.5, 7), tight_layout={"w_pad": 0}) img_ax = fig_pf.add_subplot(1, 1, 1) img_ax.imshow(pf.image.array, cmap=matplotlib.cm.gray, interpolation="none", aspect="equal", origin='upper') # Taken from pylinac: leaf_error_subplot: tol_line_height = [pf.settings.tolerance, pf.settings.tolerance] tol_line_width = [0, max(pf.image.shape)] # make the new axis divider = make_axes_locatable(img_ax) if pf.settings.orientation == 'Up-Down': axtop = divider.append_axes('right', 1.75, pad=0.2, sharey=img_ax) else: axtop = divider.append_axes('bottom', 1.75, pad=0.5, sharex=img_ax) # get leaf positions, errors, standard deviation, and leaf numbers pos, vals, err, leaf_nums = pf.pickets.error_hist() # Changed leaf_nums to sequential numbers: leaf_nums = list(np.arange(0, len(leaf_nums), 1)) # plot the leaf errors as a bar plot if pf.settings.orientation == 'Up-Down': axtop.barh(pos, vals, xerr=err, height=pf.pickets[0].sample_width * 2, alpha=0.4, align='center') # plot the tolerance line(s) axtop.plot(tol_line_height, tol_line_width, 'r-', linewidth=3) if pf.settings.action_tolerance is not None: tol_line_height_action = [ pf.settings.action_tolerance, pf.settings.action_tolerance ] tol_line_width_action = [0, max(pf.image.shape)] axtop.plot(tol_line_height_action, tol_line_width_action, 'y-', linewidth=3) # reset xlims to comfortably include the max error or tolerance value axtop.set_xlim([0, max(max(vals), pf.settings.tolerance) + 0.1]) else: axtop.bar(pos, vals, yerr=err, width=pf.pickets[0].sample_width * 2, alpha=0.4, align='center') axtop.plot(tol_line_width, tol_line_height, 'r-', linewidth=3) if pf.settings.action_tolerance is not None: tol_line_height_action = [ pf.settings.action_tolerance, pf.settings.action_tolerance ] tol_line_width_action = [0, max(pf.image.shape)] axtop.plot(tol_line_width_action, tol_line_height_action, 'y-', linewidth=3) axtop.set_ylim([0, max(max(vals), pf.settings.tolerance) + 0.1]) # add formatting to axis axtop.grid(True) axtop.set_title("Average Error (mm)") # add tooltips if interactive # Copied this from previous version of pylinac interactive = True if interactive: if pf.settings.orientation == 'Up-Down': labels = [[ 'Leaf pair: {0} <br> Avg Error: {1:3.3f} mm <br> Stdev: {2:3.3f} mm' .format(leaf_num, err, std) ] for leaf_num, err, std in zip(leaf_nums, vals, err)] voffset = 0 hoffset = 20 else: labels = [[ 'Leaf pair: {0}, Avg Error: {1:3.3f} mm, Stdev: {2:3.3f} mm'. format(leaf_num, err, std) ] for leaf_num, err, std in zip(leaf_nums, vals, err)] if pf.settings.orientation == 'Up-Down': for num, patch in enumerate(axtop.axes.patches): ttip = mpld3.plugins.PointHTMLTooltip(patch, labels[num], voffset=voffset, hoffset=hoffset) mpld3.plugins.connect(fig_pf, ttip) mpld3.plugins.connect(fig_pf, mpld3.plugins.MousePosition(fontsize=14)) else: for num, patch in enumerate(axtop.axes.patches): ttip = mpld3.plugins.PointLabelTooltip(patch, labels[num], location='top left') mpld3.plugins.connect(fig_pf, ttip) mpld3.plugins.connect(fig_pf, mpld3.plugins.MousePosition(fontsize=14)) for p_num, picket in enumerate(pf.pickets): picket.add_guards_to_axes(img_ax.axes) for idx, mlc_meas in enumerate(picket.mlc_meas): mlc_meas.plot2axes(img_ax.axes, width=1.5) # plot CAX img_ax.plot(pf.settings.image_center.x, pf.settings.image_center.y, 'r+', ms=12, markeredgewidth=3) # tighten up the plot view img_ax.set_xlim([0, pf.image.shape[1]]) img_ax.set_ylim([pf.image.shape[0], 0]) img_ax.axis('off') img_ax.set_xticks([]) img_ax.set_yticks([]) # Histogram of all errors and average profile plot upper_bound = pf.settings.tolerance upper_outliers = np.sum(error_array.flatten() >= upper_bound) fig_pf2 = Figure(figsize=(10, 4), tight_layout={"w_pad": 2}) ax2 = fig_pf2.add_subplot(1, 2, 1) ax3 = fig_pf2.add_subplot(1, 2, 2) n, bins = np.histogram(error_array.flatten(), density=False, bins=10, range=(0, upper_bound)) ax2.bar(bins[0:-1], n, width=np.diff(bins)[0], facecolor='green', alpha=0.75) ax2.bar([upper_bound, upper_bound * 1.1], upper_outliers, width=0.1 * upper_bound, facecolor='red', alpha=0.75) ax2.plot([pf.settings.action_tolerance, pf.settings.action_tolerance], [0, max(n) / 2], color="orange") ax2.annotate("Action Tol.", (pf.settings.action_tolerance, 1.05 * max(n) / 2), color='black', fontsize=6, ha='center', va='bottom') ax2.plot([pf.settings.tolerance, pf.settings.tolerance], [0, max(n) / 2], color="darkred") ax2.annotate("Tol.", (pf.settings.tolerance, 1.05 * max(n) / 2), color='black', fontsize=6, ha='center', va='bottom') # Plot mean inplane profile and calculate FWHM: mlc_mean_profile = pf.pickets.image_mlc_inplane_mean_profile ax3.plot(mlc_mean_profile.values, "b-") picket_fwhm = [] fwhm_mean = 0 try: peaks = mlc_mean_profile.find_peaks(max_number=picket_nr, min_distance=0.02, threshold=0.5) peaks = np.sort(peaks) ax3.plot(peaks, mlc_mean_profile[peaks], "ro") separation = int(np.mean(np.diff(peaks)) / 3) mmpd = 1 / pf.image.dpmm # Get valleys valleys = [] for p in np.arange(0, len(peaks) - 1, 1): prof_partial = mlc_mean_profile[peaks[p]:peaks[p + 1]] valleys.append(peaks[p] + np.argmin(prof_partial)) edge_points = [peaks[0] - separation ] + valleys + [peaks[-1] + separation] ax3.plot(edge_points, mlc_mean_profile[edge_points], "yo") for k in np.arange(0, len(edge_points) - 1, 1): pr = PylinacSingleProfile( mlc_mean_profile[edge_points[k]:edge_points[k + 1]]) left = pr[0] right = pr[-1] amplitude = mlc_mean_profile[peaks[k]] if left < right: x = 100 * ((amplitude - left) * 0.5 + left - right) / (amplitude - right) a = pr._penumbra_point(x=50, side="left", interpolate=True) b = pr._penumbra_point(x=x, side="right", interpolate=True) else: x = 100 * ((amplitude - right) * 0.5 + right - left) / (amplitude - left) a = pr._penumbra_point(x=x, side="left", interpolate=True) b = pr._penumbra_point(x=50, side="right", interpolate=True) left_point = edge_points[k] + a right_point = edge_points[k] + b ax3.plot([left_point, right_point], [ np.interp(left_point, np.arange(0, len(mlc_mean_profile.values), 1), mlc_mean_profile.values), np.interp(right_point, np.arange(0, len(mlc_mean_profile.values), 1), mlc_mean_profile.values) ], "-k", alpha=0.5) picket_fwhm.append(np.abs(a - b) * mmpd) fwhm_mean = np.mean(picket_fwhm) except: picket_fwhm = [np.nan] * picket_nr fwhm_mean = np.nan if len(picket_fwhm) != picket_nr: fwhm_mean = np.mean(picket_fwhm) picket_fwhm = [np.nan] * picket_nr ax2.set_xlim([-0.025, pf.settings.tolerance * 1.15]) ax3.set_xlim([0, pf.image.shape[1]]) ax2.set_title("Leaf error") ax3.set_title("MLC mean profile") ax2.set_xlabel("Error [mm]") ax2.set_ylabel("Counts") ax3.set_xlabel("Pixel") ax3.set_ylabel("Grey value") passed = "Passed" if pf.passed else "Failed" script = mpld3.fig_to_html(fig_pf, d3_url=D3_URL, mpld3_url=MPLD3_URL) script2 = mpld3.fig_to_html(fig_pf2, d3_url=D3_URL, mpld3_url=MPLD3_URL) variables = { "script": script, "script2": script2, "passed": passed, "max_error": max_error, "max_error_leaf": max_error_leaf, "passed_tol": passed_tol, "picket_nr": picket_nr, "tolerance": pf.settings.tolerance, "perc_passing": pf.percent_passing, "max_error_all": pf.max_error, "max_error_picket_all": pf.max_error_picket, "max_error_leaf_all": pf.max_error_leaf, "median_error": pf.abs_median_error, "spacing": pf.pickets.mean_spacing, "picket_offsets": picket_offsets, "fwhm_mean": fwhm_mean, "picket_fwhm": picket_fwhm, "pdf_report_enable": generate_pdf_report, "save_results": save_results, "acquisition_datetime": acquisition_datetime } # Generate pylinac report: if generate_pdf_report == "True": pdf_file = tempfile.NamedTemporaryFile(delete=False, prefix="PicketFence_", suffix=".pdf", dir=config.PDF_REPORT_FOLDER) metadata = RestToolbox.GetInstances(config.ORTHANC_URL, [w]) try: patient = metadata[0]["PatientName"] except: patient = "" try: stationname = metadata[0]["StationName"] except: stationname = "" try: date_time = RestToolbox.get_datetime(metadata[0]) date_var = datetime.datetime.strptime( date_time[0], "%Y%m%d").strftime("%d/%m/%Y") except: date_var = "" pf.publish_pdf(pdf_file, notes=[ "Date = " + date_var, "Patient = " + patient, "Station = " + stationname ]) variables["pdf_report_filename"] = os.path.basename(pdf_file.name) #gc.collect() general_functions.delete_files_in_subfolders([temp_folder]) # Delete image return template("picket_fence_results", variables)
def setUpClass(cls): cls.profile = SingleProfile(cls.ydata, normalize_sides=cls.normalize_sides)
def flatsym_helper(args): calc_definition = args["calc_definition"] center_definition = args["center_definition"] center_x = args["center_x"] center_y = args["center_y"] invert = args["invert"] w = args["instance"] station = args["station"] imgdescription = args["imgdescription"] displayname = args["displayname"] acquisition_datetime = args["acquisition_datetime"] general_functions.set_configuration( args["config"]) # Transfer to this process # Collect data for "save results" dicomenergy = general_functions.get_energy_from_imgdescription( imgdescription) user_machine, user_energy = general_functions.get_user_machine_and_energy( station, dicomenergy) machines_and_energies = general_functions.get_machines_and_energies( general_functions.get_treatmentunits_flatsym()) tolerances = general_functions.get_tolerance_user_machine_flatsym( user_machine) # If user_machne has specific tolerance if not tolerances: tolerance_flat, tolerance_sym, pdf_report_enable = general_functions.get_settings_flatsym( ) else: tolerance_flat, tolerance_sym, pdf_report_enable = tolerances[0] tolerance_flat = float(tolerance_flat) tolerance_sym = float(tolerance_sym) save_results = { "user_machine": user_machine, "user_energy": user_energy, "machines_and_energies": machines_and_energies, "displayname": displayname } temp_folder, file_path = RestToolbox.GetSingleDcm(config.ORTHANC_URL, w) try: flatsym = FlatSym(file_path) except Exception as e: return template("error_template", { "error_message": "The FlatSym module cannot calculate. " + str(e) }) # Define the center pixel where to get profiles: def find_field_centroid(img): '''taken from pylinac WL module''' min, max = np.percentile(img.image.array, [5, 99.9]) # min, max = np.amin(img.array), np.max(img.array) threshold_img = img.image.as_binary((max - min) / 2 + min) # clean single-pixel noise from outside field coords = ndimage.measurements.center_of_mass(threshold_img) return (int(coords[-1]), int(coords[0])) flatsym.image.crop(pixels=2) flatsym.image.check_inversion() if invert: flatsym.image.invert() flatsym.image.array = np.flipud(flatsym.image.array) vmax = flatsym.image.array.max() if center_definition == "Automatic": center_int = [ int(flatsym.image.array.shape[1] / 2), int(flatsym.image.array.shape[0] / 2) ] center = [0.5, 0.5] elif center_definition == "CAX": center_int = find_field_centroid( flatsym) # Define as mechanical isocenter center = [ int(center_int[0]) / flatsym.image.array.shape[1], int(center_int[1]) / flatsym.image.array.shape[0] ] else: center_int = [int(center_x), int(center_y)] center = [ int(center_x) / flatsym.image.array.shape[1], int(center_y) / flatsym.image.array.shape[0] ] fig = Figure(figsize=(9, 9), tight_layout={"w_pad": 1}) ax = fig.add_subplot(1, 1, 1) ax.imshow(flatsym.image.array, cmap=matplotlib.cm.jet, interpolation="none", vmin=0.9 * vmax, vmax=vmax, aspect="equal", origin='lower') ax.autoscale(tight=True) # Plot lines along which the profiles are calculated: ax.plot([0, flatsym.image.array.shape[1]], [center_int[1], center_int[1]], "b-", linewidth=2) ax.plot([center_int[0], center_int[0]], [0, flatsym.image.array.shape[0]], c="darkgreen", linestyle="-", linewidth=2) ax.set_xlim([0, flatsym.image.array.shape[1]]) ax.set_ylim([0, flatsym.image.array.shape[0]]) # Get profiles crossplane = PylinacSingleProfile(flatsym.image.array[center_int[1], :]) inplane = PylinacSingleProfile(flatsym.image.array[:, center_int[0]]) # Do some filtering: crossplane.filter(kind='median', size=0.01) inplane.filter(kind='median', size=0.01) # Normalize profiles norm_val_crossplane = crossplane.values[center_int[0]] norm_val_inplane = inplane.values[center_int[1]] crossplane.normalize(norm_val=norm_val_crossplane) inplane.normalize(norm_val=norm_val_inplane) # Get index of CAX of both profiles(different than center) to be used for mirroring: fwhm_crossplane = crossplane.fwxm_center(interpolate=True) fwhm_inplane = inplane.fwxm_center(interpolate=True) # Plot profiles divider = make_axes_locatable(ax) ax_crossplane = divider.append_axes("bottom", size="40%", pad=0.25, sharex=ax) ax_crossplane.set_xlim([0, flatsym.image.array.shape[1]]) ax_crossplane.set_xticks([]) ax_crossplane.set_title("Crossplane") ax_inplane = divider.append_axes("right", size="40%", pad=0.25, sharey=ax) ax_inplane.set_ylim([0, flatsym.image.array.shape[0]]) ax_inplane.set_yticks([]) ax_inplane.set_title("Inplane") ax_crossplane.plot(crossplane._indices, crossplane, "b-") ax_crossplane.plot(2 * fwhm_crossplane - crossplane._indices, crossplane, "b--") ax_inplane.plot(inplane, inplane._indices, c="darkgreen", linestyle="-") ax_inplane.plot(inplane, 2 * fwhm_inplane - inplane._indices, c="darkgreen", linestyle="--") ax_inplane.grid(alpha=0.5) ax_crossplane.grid(alpha=0.5) mpld3.plugins.connect(fig, mpld3.plugins.MousePosition(fontsize=14, fmt=".2f")) script = mpld3.fig_to_html(fig, d3_url=D3_URL, mpld3_url=MPLD3_URL) if calc_definition == "Elekta": method = "elekta" else: method = "varian" try: flatsym.analyze(flatness_method=method, symmetry_method=method, vert_position=center[0], horiz_position=center[1]) except Exception as e: return template("error_template", {"error_message": "Analysis failed. " + str(e)}) symmetry_hor = round(flatsym.symmetry['horizontal']['value'], 2) symmetry_vrt = round(flatsym.symmetry['vertical']['value'], 2) flatness_hor = round(flatsym.flatness['horizontal']['value'], 2) flatness_vrt = round(flatsym.flatness['vertical']['value'], 2) horizontal_width = round( flatsym.symmetry['horizontal']['profile'].fwxm(interpolate=True) / flatsym.image.dpmm, 2) vertical_width = round( flatsym.symmetry['vertical']['profile'].fwxm(interpolate=True) / flatsym.image.dpmm, 2) horizontal_penumbra_width = round( flatsym.symmetry['horizontal']['profile'].penumbra_width( interpolate=True) / flatsym.image.dpmm, 2) vertical_penumbra_width = round( flatsym.symmetry['vertical']['profile'].penumbra_width( interpolate=True) / flatsym.image.dpmm, 2) # Check if passed if method == "varian": if (flatness_hor < tolerance_flat) and (flatness_vrt < tolerance_flat) and ( symmetry_hor < tolerance_sym) and (symmetry_vrt < tolerance_sym): passed = True else: passed = False else: if (abs(flatness_hor - 100) < tolerance_flat) and ( abs(flatness_vrt - 100) < tolerance_flat) and ( abs(symmetry_hor - 100) < tolerance_sym) and ( abs(symmetry_vrt - 100) < tolerance_sym): passed = True else: passed = False variables = { "symmetry_hor": symmetry_hor, "symmetry_vrt": symmetry_vrt, "flatness_hor": flatness_hor, "flatness_vrt": flatness_vrt, "horizontal_width": horizontal_width, "vertical_width": vertical_width, "horizontal_penumbra_width": horizontal_penumbra_width, "vertical_penumbra_width": vertical_penumbra_width, "passed": passed, "pdf_report_enable": pdf_report_enable, "script": script, "save_results": save_results, "acquisition_datetime": acquisition_datetime, "calc_definition": calc_definition } # Generate pylinac report: if pdf_report_enable == "True": pdf_file = tempfile.NamedTemporaryFile(delete=False, prefix="FlatSym", suffix=".pdf", dir=config.PDF_REPORT_FOLDER) flatsym.publish_pdf(pdf_file) variables["pdf_report_filename"] = os.path.basename(pdf_file.name) general_functions.delete_files_in_subfolders([temp_folder]) # Delete image return template("flatsym_results", variables)