diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 8df2095d..bb971549 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -13,11 +13,16 @@ Core E.g. ``image.load_multiples("my_image.dcm", loader=MyDicomImage)``. Default behavior still uses ``load``. -PicketFence -^^^^^^^^^^^ +Picket Fence +^^^^^^^^^^^^ * The ``from_multiple_images`` method signature added the ``mlc`` keyword argument. Previously, only the default MLC could be used. +* Picket fence plots were being plotted upside down. They will now be plotted right-side up. +* The MLC arrangement for Varian machines was inverted. Leaf 1 was assumed to be at the + top of the image, but it is actually at the bottom. This will affect both the combined + and separated leaf analysis. An error that would've shown, e.g., A20 will now show A40. +* The MLC skew is now reported in the ``.results()`` method. Winston-Lutz ^^^^^^^^^^^^ diff --git a/pylinac/picketfence.py b/pylinac/picketfence.py index d8f8f7fa..4434a707 100644 --- a/pylinac/picketfence.py +++ b/pylinac/picketfence.py @@ -38,11 +38,10 @@ from .core import image, pdf from .core.geometry import Line, Point, Rectangle from .core.io import get_url, retrieve_demo_file -from .core.metrics import SizedDiskLocator from .core.profile import FWXMProfilePhysical, MultiProfile from .core.utilities import ResultBase, convert_to_enum from .log_analyzer import load_log -from .settings import get_dicom_cmap +from .metrics.image import SizedDiskLocator LEFT_MLC_PREFIX = "A" RIGHT_MLC_PREFIX = "B" @@ -82,6 +81,13 @@ def __init__(self, leaf_arrangement: list[tuple[int, float]], offset: float = 0) self.widths += [width] * leaf_num self.centers = [c - np.mean(self.centers) + offset for c in self.centers] + @property + def leaves(self) -> list[int]: + """The leaf numbers; index pairs with the centers. Assumes that + the first leaf center is toward the target and the last leaf center is towards the gun. + """ + return np.arange(1, len(self.centers) + 1, dtype=int)[::-1].tolist() + class MLC(enum.Enum): """The pre-built MLC types""" @@ -811,31 +817,28 @@ def _get_mlc_window( def _leaves_in_view(self, analysis_width) -> list[tuple[int, int, int]]: """Crop the leaves if not all leaves are in view.""" - range = ( + pixel_range = ( self.image.shape[0] / 2 if self.orientation == Orientation.UP_DOWN else self.image.shape[1] / 2 ) # cut off the edge so that we're not halfway through a leaf. - range -= ( + pixel_range -= ( max( self.mlc.widths[0] * analysis_width, self.mlc.widths[-1] * analysis_width, ) * self.image.dpmm ) - leaves = [ - i - for i, c in enumerate(self.mlc.centers) - if abs(c) < (range / self.image.dpmm) - ] + # include the leaf if the center is within the pixel range return [ (leaf_num, center, width) for leaf_num, center, width in zip( - leaves, - self.mlc.centers[leaves[0] : leaves[-1] + 1], - self.mlc.widths[leaves[0] : leaves[-1] + 1], + self.mlc.leaves, + self.mlc.centers, + self.mlc.widths, ) + if abs(center) < pixel_range / self.image.dpmm ] def plot_analyzed_image( @@ -876,7 +879,7 @@ def plot_analyzed_image( else: figure_size = (9, 9) fig, ax = plt.subplots(figsize=figure_size) - ax.imshow(self.image.array, cmap=get_dicom_cmap()) + self.image.plot(ax=ax, show=False) if leaf_error_subplot: self._add_leaf_error_subplot(ax) @@ -896,10 +899,6 @@ def plot_analyzed_image( ax.plot( self.image.center.x, self.image.center.y, "r+", ms=12, markeredgewidth=3 ) - - # tighten up the plot view - ax.set_xlim([0, self.image.shape[1]]) - ax.set_ylim([0, self.image.shape[0]]) ax.axis("off") if show: @@ -920,12 +919,12 @@ def _add_leaf_error_subplot(self, ax: plt.Axes) -> None: pos = [ position.marker_lines[0].center.y for position in self.pickets[0].mlc_meas - ] + ][::-1] else: pos = [ position.marker_lines[0].center.x for position in self.pickets[0].mlc_meas - ] + ][::-1] # calculate the error and stdev values per MLC pair error_stdev = [] @@ -1014,6 +1013,7 @@ def results(self, as_list: bool = False) -> str: f"Mean picket spacing (mm): {self.mean_picket_spacing:2.1f}mmn", f"Picket offsets from CAX (mm): {offsets}", f"Max Error: {self.max_error:2.3f}mm on Picket: {self.max_error_picket}, Leaf: {self.max_error_leaf}", + f"MLC Skew: {self.mlc_skew():2.3f} degrees", ] if self.failed_leaves(): results.append(f"Failing leaves: {self.failed_leaves()}") diff --git a/tests_basic/test_picketfence.py b/tests_basic/test_picketfence.py index d299e34f..0c89dbd9 100644 --- a/tests_basic/test_picketfence.py +++ b/tests_basic/test_picketfence.py @@ -186,77 +186,94 @@ def test_failed_leaves_before_analyzed(self): def test_failed_leaves_traditional(self): pf = PicketFence.from_demo_image() pf.analyze(separate_leaves=False, tolerance=0.05) - self.assertEqual( - set(pf.failed_leaves()), - {12, 14, 16, 17, 18, 19, 26, 29, 31, 32, 33, 35, 39, 42, 43, 47}, + self.assertCountEqual( + pf.failed_leaves(), + [ + 34, + 41, + 44, + 13, + 46, + 48, + 21, + 25, + 27, + 28, + 42, + 43, + 17, + 18, + 29, + 31, + ], ) def test_failed_leaves_separate(self): pf = PicketFence.from_demo_image() pf.analyze(separate_leaves=True, tolerance=0.15, nominal_gap_mm=3) - self.assertEqual( - set(pf.failed_leaves()), - { - "A12", - "B12", + self.assertCountEqual( + pf.failed_leaves(), + [ "A13", - "B13", "A14", - "B14", "A15", + "A16", + "A17", + "A19", + "A20", + "A21", + "A22", + "A23", + "A24", + "A25", + "A27", + "A28", + "A32", + "A33", + "A35", + "A36", + "A37", + "A40", + "A41", + "A42", + "A45", + "A46", + "A47", + "A48", + "B13", + "B14", "B15", "B16", "B17", - "A18", "B18", "B19", - "A19", - "A20", "B20", + "B21", "B22", - "A23", "B23", - "A24", "B24", - "A25", + "B25", "B26", - "A27", - "B27", - "A28", "B28", "B29", "B30", "B31", - "A32", "B32", - "A33", + "B33", "B34", - "B35", - "A35", - "A36", "B36", - "A37", "B37", - "A38", "B38", - "B39", - "A39", - "A40", "B40", "B41", - "A41", "B42", - "A43", "B43", "B44", - "A44", "B45", - "A45", - "A46", "B46", "B47", - "A47", - }, + "B48", + ], ) @@ -360,6 +377,7 @@ def test_publish_pdf(self): def test_results(self): data = self.pf.results() self.assertIsInstance(data, str) + self.assertIn("Skew", data) data = self.pf.results(as_list=True) self.assertIsInstance(data, list) @@ -484,7 +502,7 @@ class PFDemo(PFTestMixin, TestCase): max_error = 0.08 abs_median_error = 0.06 max_error_picket = 0 - max_error_leaf = 31 + max_error_leaf = 29 @classmethod def setUpClass(cls):