class TestDRGSDemo(VMATMixin, TestCase): """Tests of the result values of the DRGS demo images.""" test_type = DRGS segment_positions = {0: Point(161, 192), 4: Point(314, 192)} segment_values = { 0: { 'r_dev': 0.965, 'r_corr': 101.85 }, 4: { 'r_dev': -0.459, 'r_corr': 100.42 }, } avg_abs_r_deviation = 0.46 avg_r_deviation = 0 max_r_deviation = 0.96 x_offset = 20 def setUp(self): self.vmat = VMAT.from_demo_images('drgs') self.vmat.analyze(self.test_type, x_offset=self.x_offset) def test_demo(self): """Run the demo; no errors should arise.""" self.vmat.run_demo_drgs()
def subdivide(self, interpolation_factor=100, interpolation_type='linear'): """Subdivide the profile data into SingleProfiles. Returns ------- list SingleProfiles """ # append the peak list to include the endpoints of the profile peaks = self.peaks.copy() peaks.insert(0, Point(idx=0)) peaks.append(Point(idx=len(self.values))) # create a list of single profiles from segments of original profile data. # New profiles are segmented by initial peak locations. subprofiles = [] for idx in range(len(peaks) - 2): left_end = peaks[idx].idx peak_idx = peaks[idx + 1].idx - left_end right_end = peaks[idx + 2].idx values = self.values[int(left_end):int(right_end)] subprofile = SingleProfile(values, initial_peak=peak_idx) subprofile.interpolation_factor = interpolation_factor subprofile.interpolation_type = interpolation_type subprofiles.append(subprofile) return subprofiles
class TestDRGS105(VMATMixin, TestCase): """Tests of the result values of DRMLC images at 105cm SID.""" filepaths = ('DRGSopen-105-example.dcm', 'DRGSdmlc-105-example.dcm') klass = DRGS segment_positions = {0: Point(371, 384), 2: Point(478, 384)} segment_values = { 0: {'r_dev': 1.385, 'r_corr': 15.12}, 4: {'r_dev': -0.8, 'r_corr': 14.8}, } avg_abs_r_deviation = 0.68 max_r_deviation = 1.38
class TestDRGS2(VMATMixin, TestCase): """Tests of the result values of DRMLC images at 105cm SID.""" filepaths = ('DRGS#2_open.dcm', 'DRGS#2_dmlc.dcm') klass = DRGS segment_positions = {0: Point(191, 192), 2: Point(242, 192)} segment_values = { 0: {'r_dev': 1.5, 'r_corr': 6.4}, 4: {'r_dev': -0.7, 'r_corr': 6.3}, } avg_abs_r_deviation = 0.7 max_r_deviation = 1.5
class TestDRMLC105(VMATMixin, TestCase): """Tests of the result values of MLCS images at 105cm SID.""" klass = DRMLC filepaths = ('DRMLCopen-105-example.dcm', 'DRMLCdmlc-105-example.dcm') segment_positions = {0: Point(391, 384), 2: Point(552, 384)} segment_values = { 0: {'r_dev': -2.1, 'r_corr': 13.6}, 2: {'r_dev': 0.22, 'r_corr': 14}, } avg_abs_r_deviation = 1.06 max_r_deviation = 2.11 passes = False
class TestDRMLC2(VMATMixin, TestCase): """Tests of the result values of MLCS images at 105cm SID.""" filepaths = ('DRMLC#2_open.dcm', 'DRMLC#2_dmlc.dcm') klass = DRMLC segment_positions = {0: Point(199, 192), 2: Point(275, 192)} segment_values = { 0: {'r_dev': 0.77, 'r_corr': 6.1}, 2: {'r_dev': -1.1, 'r_corr': 6}, } avg_abs_r_deviation = 1.4 max_r_deviation = 1.98 passes = False
class TestDRMLC2(VMATMixin, TestCase): """Tests of the result values of MLCS images at 105cm SID.""" filepaths = (osp.join(TEST_DIR, 'DRMLC#2_open.dcm'), osp.join(TEST_DIR, 'DRMLC#2_dmlc.dcm')) test_type = DRMLC segment_positions = {0: Point(199, 192), 2: Point(275, 192)} segment_values = { 0: {'r_dev': 0.40, 'r_corr': 101.06}, 2: {'r_dev': -0.49, 'r_corr': 100.16}, } avg_abs_r_deviation = 0.4 avg_r_deviation = 0 max_r_deviation = -0.49
class TestDRMLC105(VMATMixin, TestCase): """Tests of the result values of MLCS images at 105cm SID.""" filepaths = (osp.join(TEST_DIR, 'DRMLCopen-105-example.dcm'), osp.join(TEST_DIR, 'DRMLCdmlc-105-example.dcm')) test_type = DRMLC segment_positions = {0: Point(391, 384), 2: Point(552, 384)} segment_values = { 0: {'r_dev': -0.040, 'r_corr': 100.83}, 2: {'r_dev': -0.021, 'r_corr': 100.85}, } avg_abs_r_deviation = 0.03 avg_r_deviation = 0 max_r_deviation = 0.04
class TestDRGS2(VMATMixin, TestCase): """Tests of the result values of DRMLC images at 105cm SID.""" filepaths = (osp.join(TEST_DIR, 'DRGS#2_open.dcm'), osp.join(TEST_DIR, 'DRGS#2_dmlc.dcm')) test_type = DRGS x_offset = 12 segment_positions = {0: Point(191, 192), 2: Point(242, 192)} segment_values = { 0: {'r_dev': 1.3, 'r_corr': 103.0}, 4: {'r_dev': -0.8, 'r_corr': 100.86}, } avg_abs_r_deviation = 0.7 avg_r_deviation = 0 max_r_deviation = 1.3
class TestDRGS105(VMATMixin, TestCase): """Tests of the result values of DRMLC images at 105cm SID.""" filepaths = (osp.join(TEST_DIR, 'DRGSopen-105-example.dcm'), osp.join(TEST_DIR, 'DRGSdmlc-105-example.dcm')) test_type = DRGS x_offset = 20 segment_positions = {0: Point(371, 384), 2: Point(478, 384)} segment_values = { 0: {'r_dev': 0.780, 'r_corr': 102.43}, 4: {'r_dev': -0.282, 'r_corr': 101.357}, } avg_abs_r_deviation = 0.34 avg_r_deviation = 0 max_r_deviation = 0.78
class TestDRMLCWideGaps(VMATMixin, TestCase): """Tests of the result values of a perfect DRMLC but with very wide gaps.""" filepaths = ('vmat-drgs-open-wide-gaps.dcm', 'vmat-drgs-open-wide-gaps.dcm') klass = DRMLC segment_positions = {0: Point(439, 640), 2: Point(707, 640)} segment_values = { 0: {'r_dev': 0, 'r_corr': 100}, 2: {'r_dev': 0, 'r_corr': 100}, } avg_abs_r_deviation = 0 max_r_deviation = 0.0 passes = True def test_fail_with_tight_tolerance(self): pass
class TestDRMLCOverlapGaps(VMATMixin, TestCase): """Tests of the result values of a perfect DRMLC but with gaps that are overlapping (e.g. from a poor DLG).""" filepaths = ('vmat-drgs-open-overlap.dcm', 'vmat-drgs-open-overlap.dcm') klass = DRMLC segment_positions = {0: Point(439, 640), 2: Point(707, 640)} segment_values = { 0: {'r_dev': 0, 'r_corr': 100}, 2: {'r_dev': 0, 'r_corr': 100}, } avg_abs_r_deviation = 0 max_r_deviation = 0.0 passes = True def test_fail_with_tight_tolerance(self): pass
class TestDRMLCDemo(VMATMixin, TestCase): """Tests of the result values of the DRMLC demo images.""" segment_positions = {0: Point(170, 192), 2: Point(285, 192)} segment_values = { 0: {'r_dev': -0.7, 'r_corr': 5.7}, 2: {'r_dev': -0.405, 'r_corr': 5.8}, } avg_abs_r_deviation = 0.44 max_r_deviation = 0.89 def setUp(self): self.vmat = DRMLC.from_demo_images() self.vmat.analyze() def test_demo(self): self.vmat.run_demo()
def find_valleys(self, threshold=0.3, min_distance=0.05, max_number=None, search_region=(0.0, 1.0), kind='index'): """Find the valleys (minimums) of the profile using a simple minimum value search. Returns ------- ndarray Either the values or indices of the peaks. See Also -------- :meth:`~pylinac.core.profile.MultiProfile.find_peaks` : Further parameter info. """ valley_vals, valley_idxs = peak_detect(self.values, threshold, min_distance, max_number, search_region=search_region, find_min_instead=True) self.valleys = [ Point(value=valley_val, idx=valley_idx) for valley_idx, valley_val in zip(valley_idxs, valley_vals) ] return valley_idxs if kind == INDEX else valley_vals
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
class VMATMixin: klass = object filepaths = Union[str, List] is_zip = False segment_positions = {1: Point(100, 200)} segment_values = { 0: {'r_dev': 0, 'r_corr': 100}, 4: {'r_dev': 0, 'r_corr': 100}, } avg_abs_r_deviation = 0 avg_r_deviation = 0 max_r_deviation = 0 passes = True print_debug = False @classmethod def absolute_path(cls): if cls.is_zip: path = osp.join(TEST_DIR, cls.filepaths) else: path = [osp.join(TEST_DIR, path) for path in cls.filepaths] return path def setUp(self): if self.is_zip: self.vmat = self.klass.from_zip(self.absolute_path()) else: self.vmat = self.klass(self.absolute_path()) self.vmat.analyze() if self.print_debug: print(self.vmat.results()) print(f"Segment 0: rdev {self.vmat.segments[0].r_dev:2.3f}, rcorr {self.vmat.segments[0].r_corr:2.3f}") if self.klass == DRGS: print(f"Segment 4: rdev {self.vmat.segments[4].r_dev:2.3f}, rcorr {self.vmat.segments[4].r_corr:2.3f}") else: print(f"Segment 2: rdev {self.vmat.segments[2].r_dev:2.3f}, rcorr {self.vmat.segments[2].r_corr:2.3f}") print("Max dev", self.vmat.max_r_deviation) def test_overall_passed(self): self.assertEqual(self.vmat.passed, self.passes) def test_fail_with_tight_tolerance(self): self.vmat.analyze(tolerance=0.01) self.assertFalse(self.vmat.passed) def test_segment_positions(self): for key, value in self.segment_positions.items(): within_5(self.vmat.segments[key].center.x, value.x) within_5(self.vmat.segments[key].center.y, value.y) def test_segment_values(self): for key, value in self.segment_values.items(): within_1(self.vmat.segments[key].r_dev, value['r_dev']) within_1(self.vmat.segments[key].r_corr, value['r_corr']) def test_deviations(self): self.assertAlmostEqual(self.vmat.avg_abs_r_deviation, self.avg_abs_r_deviation, delta=0.05) self.assertAlmostEqual(self.vmat.avg_r_deviation, self.avg_r_deviation, delta=0.02) self.assertAlmostEqual(self.vmat.max_r_deviation, self.max_r_deviation, delta=0.1)
class VMATMixin: filepaths = ('open', 'dmlc') is_zip = False test_type = '' x_offset = 0 segment_positions = {1: Point(100, 200)} segment_values = { 0: { 'r_dev': 0, 'r_corr': 100 }, 4: { 'r_dev': 0, 'r_corr': 100 }, } avg_abs_r_deviation = 0 avg_r_deviation = 0 max_r_deviation = 0 passes = True def setUp(self): if self.is_zip: self.vmat = VMAT.from_zip(self.filepaths) else: self.vmat = VMAT(self.filepaths) self.vmat.analyze(self.test_type, x_offset=self.x_offset) def test_overall_passed(self): self.vmat.analyze(self.test_type, x_offset=self.x_offset) self.assertEqual(self.vmat.passed, self.passes) def test_fail_with_tight_tolerance(self): self.vmat.analyze(self.test_type, tolerance=0.01, x_offset=self.x_offset) self.assertFalse(self.vmat.passed) def test_segment_positions(self): for key, value in self.segment_positions.items(): within_1(self.vmat.segments[key].center.x, value.x) within_1(self.vmat.segments[key].center.y, value.y) def test_segment_values(self): for key, value in self.segment_values.items(): within_01(self.vmat.segments[key].r_dev, value['r_dev']) within_01(self.vmat.segments[key].r_corr, value['r_corr']) def test_deviations(self): self.assertAlmostEqual(self.vmat.avg_abs_r_deviation, self.avg_abs_r_deviation, delta=0.05) self.assertAlmostEqual(self.vmat.avg_r_deviation, self.avg_r_deviation, delta=0.02) self.assertAlmostEqual(self.vmat.max_r_deviation, self.max_r_deviation, delta=0.1)
def mm2dots(self, point): """Wandelt Point Angaben von mm nach dot. Parameters ---------- point : Point """ return Point(self.mm2dots_X(point.x), self.mm2dots_Y(point.y))
def dots2mm(self, point): """Wandelt Point Angaben von dot nach mm Parameters ---------- point : Point """ return Point(self.dots2mm_X(point.x), self.dots2mm_Y(point.y))
class TestDRGSDemo(VMATMixin, TestCase): """Tests of the result values of the DRGS demo images.""" segment_positions = {0: Point(161, 192), 4: Point(314, 192)} segment_values = { 0: {'r_dev': 0.965, 'r_corr': 6.2}, 4: {'r_dev': -0.459, 'r_corr': 6}, } avg_abs_r_deviation = 0.66 max_r_deviation = 1.8 passes = False def setUp(self): self.vmat = DRGS.from_demo_images() self.vmat.analyze() def test_demo(self): """Run the demo; no errors should arise.""" self.vmat.run_demo()
class Demo(StarMixin, TestCase): """Specific tests for the demo image""" wobble_diameter_mm = 0.30 wobble_center = Point(1270, 1437) num_rad_lines = 4 @classmethod def construct_star(cls): return Starshot.from_demo_image()
class TestDRMLCDemo(VMATMixin, TestCase): """Tests of the result values of the DRMLC demo images.""" test_type = DRMLC segment_positions = {0: Point(170, 192), 2: Point(285, 192)} segment_values = { 0: {'r_dev': 0.437, 'r_corr': 100.89}, 2: {'r_dev': -0.405, 'r_corr': 100.04}, } avg_abs_r_deviation = 0.38 avg_r_deviation = 0 max_r_deviation = 0.44 def setUp(self): self.vmat = VMAT.from_demo_images('drmlc') self.vmat.analyze(self.test_type, x_offset=self.x_offset) def test_demo(self): self.vmat.run_demo_drmlc()
def _flip_image_data(self) -> None: """Flip the image left->right and invert the center, and angle as appropriate. Sometimes the Leeds phantom is set upside down on the imaging panel. Pylinac's analysis goes counter-clockwise, so this method flips the image and coordinates to make the image ccw. Quicker than flipping the image and reanalyzing. """ self.image.array = np.fliplr(self.image.array) new_x = self.image.shape[1] - self.phantom_center.x self._phantom_center = Point(new_x, self.phantom_center.y)
def test_phan_center(self): """Test locations of the phantom center.""" known_phan_center = Point(257, 255) self.cbct.analyze() self.assertAlmostEqual(self.cbct.ctp404.phan_center.x, known_phan_center.x, delta=0.7) self.assertAlmostEqual(self.cbct.ctp404.phan_center.y, known_phan_center.y, delta=0.7)
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)
class Demo(StarMixin, TestCase): """Specific tests_basic for the demo image""" wobble_diameter_mm = 0.30 wobble_center = Point(1270, 1437) num_rad_lines = 4 wobble_tolerance = 0.15 # independently verified: 0.24-0.26mm @classmethod def construct_star(cls): return Starshot.from_demo_image()
def cax(self): """The position of the beam central axis. If no DICOM translation tags are found then the center is returned.""" try: x = self.center.x - self.dicom_dataset.XRayImageReceptorTranslation[ 0] y = self.center.y - self.dicom_dataset.XRayImageReceptorTranslation[ 1] except AttributeError: return self.center else: return Point(x, y)
def test_phan_center(self): """Test locations of the phantom center.""" self.cbct.load_demo_images() known_phan_center = Point(257, 255) self.cbct.analyze() self.assertAlmostEqual(self.cbct.hu.phan_center.x, known_phan_center.x, delta=0.7) self.assertAlmostEqual(self.cbct.hu.phan_center.y, known_phan_center.y, delta=0.7)
def _findArrayIsoCalCenter(self, imageArray, debug=False): """ Zentrum des ISO Cal (Einschub) Phantom im array bestimmen Parameters ---------- imageArray : array array der Bilddaten debug : boolean debug Bilder augeben Returns ------- Point gefundenes Zentrum in pixel """ # imageArray invertieren img_inv = -imageArray + imageArray.max() + imageArray.min() # eine Bildmaske des großen kreises erstellen labeled_foreground = (img_inv > filters.threshold_otsu(img_inv)).astype(int) # eine Auswertung der Bildmaske vornehmen properties = measure.regionprops(labeled_foreground) # es könnten mehrere objekte vorhanden sein # in unseren Bildern ist aber nur eins deshalb das erste verwenden # das gefundene Zentrum des ISO Cal Phantomstiftes in dots isoCalDot = Point(properties[0].centroid[1], properties[0].centroid[0]) if self.debug: # plot images zum debuggen plots = {'Original': imageArray, 'Labels': labeled_foreground} fig, ax = plt.subplots(1, len(plots)) for n, (title, img) in enumerate(plots.items()): cmap = plt.cm.gnuplot if n == len(plots) - 1 else plt.cm.gray ax[n].imshow(img, cmap=cmap) ax[n].axis('off') ax[n].set_title(title) ax[n].plot(isoCalDot.x, isoCalDot.y, 'r+', ms=80, markeredgewidth=1) ax[n].plot(len(img) / 2, len(img) / 2, 'y+', ms=100, markeredgewidth=1) plt.show(fig) #print("isoCalCenter", isoCalDot ) return isoCalDot
class StarMixin: """Mixin for testing a starshot image.""" star_file = '' wobble_diameter_mm = 0 wobble_center = Point() num_rad_lines = 0 recursive = True passes = True min_peak_height = 0.25 test_all_radii = True fwxm = True wobble_tolerance = 0.2 @classmethod def setUpClass(cls): cls.star = Starshot(cls.star_file) cls.star.analyze(recursive=cls.recursive, min_peak_height=cls.min_peak_height, fwhm=cls.fwxm) @classmethod def tearDownClass(cls): plt.close('all') def test_passed(self): """Test that the demo image passed""" self.star.analyze(recursive=self.recursive, min_peak_height=self.min_peak_height) self.assertEqual(self.star.passed, self.passes, msg="Wobble was not within tolerance") def test_wobble_diameter(self): """Test than the wobble radius is similar to what it has been shown to be).""" self.assertAlmostEqual(self.star.wobble.diameter_mm, self.wobble_diameter_mm, delta=self.wobble_tolerance) def test_wobble_center(self): """Test that the center of the wobble circle is close to what it's shown to be.""" # test y-coordinate y_coord = self.star.wobble.center.y self.assertAlmostEqual(y_coord, self.wobble_center.y, delta=3) # test x-coordinate x_coord = self.star.wobble.center.x self.assertAlmostEqual(x_coord, self.wobble_center.x, delta=3) def test_num_rad_lines(self): """Test than the number of radiation lines found is what is expected.""" self.assertEqual(len(self.star.lines), self.num_rad_lines, msg="The number of radiation lines found was not the number expected") def test_all_radii_give_same_wobble(self): """Test that the wobble stays roughly the same for all radii.""" if self.test_all_radii: star = Starshot(self.star_file) for radius in np.linspace(0.9, 0.25, 8): star.analyze(radius=float(radius), min_peak_height=self.min_peak_height, recursive=self.recursive) self.assertAlmostEqual(star.wobble.diameter_mm, self.wobble_diameter_mm, delta=self.wobble_tolerance)
def cax_line_projection(self): """The projection of the field CAX through space around the area of the BB. Used for determining gantry isocenter size. Returns ------- Line The virtual line in space made by the beam CAX. """ p1 = Point() p2 = Point() UPPER_QUADRANT = self.gantry_angle <= 45 or self.gantry_angle >= 315 or 225 >= self.gantry_angle > 135 LR_QUADRANT = 45 < self.gantry_angle <= 135 or 225 < self.gantry_angle < 315 if UPPER_QUADRANT: p1.y = 2 p2.y = -2 p1.z = self.z_offset p2.z = self.z_offset p1.x = 2 * tan(self.gantry_angle) + self.x_offset * cos(self.gantry_angle) p2.x = - 2 * tan(self.gantry_angle) + self.x_offset * cos(self.gantry_angle) elif LR_QUADRANT: p1.x = 2 p2.x = -2 p1.z = self.z_offset p2.z = self.z_offset p1.y = 2 / tan(self.gantry_angle) + self.y_offset * cos(self.gantry_angle - 90) p2.y = - 2 / tan(self.gantry_angle) + self.y_offset * cos(self.gantry_angle - 90) l = Line(p1, p2) return l