Skip to content

Commit

Permalink
Merged in bugfix/RAM-3148_pf_leaf_inversion (pull request #341)
Browse files Browse the repository at this point in the history
Bugfix/RAM-3148 pf leaf inversion

Approved-by: Randy Taylor
  • Loading branch information
jrkerns committed Feb 13, 2024
2 parents b6aa0dd + e677822 commit a289a8e
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 58 deletions.
9 changes: 7 additions & 2 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^
Expand Down
38 changes: 19 additions & 19 deletions pylinac/picketfence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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 = []
Expand Down Expand Up @@ -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()}")
Expand Down
92 changes: 55 additions & 37 deletions tests_basic/test_picketfence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit a289a8e

Please sign in to comment.