Skip to content

Commit

Permalink
Merged in feature/RAM-3445_2d_couch_yaw_error_mtmf (pull request #369)
Browse files Browse the repository at this point in the history
add bb yaw error for couch-kick images

Approved-by: Randy Taylor
  • Loading branch information
jrkerns committed Apr 8, 2024
2 parents 2b26544 + 970aa41 commit f749f5a
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 46 deletions.
13 changes: 12 additions & 1 deletion docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,25 @@ Winston Lutz
For multi-target/multi-field WL, the plots will now be zoomed to fit all the detected BBs and fields.
This can be turned off by passing ``zoom=False`` to the ``plot_images`` method.
* When using custom BB arrangements, use the new :class:`~pylinac.winston_lutz.BBConfig` class instead
of a dictionary. See the updated :ref:``custom-bb-arrangements`` section for more.
of a dictionary. See the updated :ref:`custom-bb-arrangements` section for more.
* A bug was fixed for the BB shift vector/instructions when analyzing images with couch kicks.
The Low paper which contains the mathematical transforms appears to have incorrect signs in equation 6. This
has been fixed and validated using the new image generator ability to create images with couch kicks.
The bug was causing the BB shift vector to be incorrect when analyzing images with couch kicks. The shift errors
were always in the LAT/LONG plane and for the most part underestimated the shift that would be needed.
* For regular WL analyses, a virtual shift can be automatically applied to the BB to see what the 2D errors would be
if the BB were shifted to the optimal position. Read more in the :ref:`wl_virtual_shift` section.
* For multi-target/multi-field analyses, the BB shift vector is now available as the ``~pylinac.winston_lutz.WinstonLutzMultiTargetMultiField.bb_shift_vector`` property.
This provides a 6DOF shift vector that can be applied to the BB to move to the ideal position.
These shifts are also included in the ``results_data()`` call.
* The 3D plotting of BBs in virtual space for both single-target and multi-target analyses has been reworked.
For single-target WL, the green isocenter lines used to always be at the origin. The lines represented the
field-determined isocenter. To better represent the field isocenter, bb isocenter, and the EPID isocenter, and their
relationships to each other, the origin is now the EPID-based isocenter and the green x/y/z lines are the field isocenter.
This makes it possible to see the BB and field isocenters in relation to the EPID isocenter as well.
* Couch-kick images are now supported for multi-target analyses. They are included in the BB shift vector calculations as well.
* Couch-kick images are also analyzed for the 2D yaw error on each image. These are included in the ``results()`` call.
* The multi-target/multi-field demo dataset was changed to purposefully introduce error for a more realistic demonstration.

Image Generator
^^^^^^^^^^^^^^^
Expand Down
135 changes: 118 additions & 17 deletions docs/source/winston_lutz_multi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,25 +75,39 @@ Results will be printed to the console and a figure showing the zoomed-in images

Winston-Lutz Multi-Target Multi-Field Analysis
==============================================
Number of images: 4
Number of images: 8

2D distances
============
Max 2D distance of any BB: 0.00 mm
Mean 2D distance of any BB: 0.00 mm
Median 2D distance of any BB: 0.00 mm

BB # Description
------ ---------------------------------------------
0 'Iso': Left 0mm, Up 0mm, In 0mm
1 'Left,Down,In': Left 20mm, Down 20mm, In 60mm

Image G Co Ch BB #0 BB #1
-------------------- --- ---- ---- ------- -------
=0, Gantry sag=0.dcm 0 0 0 0 0
=0, Gantry sag=0.dcm 90 0 0 0 0
=0, Gantry sag=0.dcm 180 0 0 0 0
=0, Gantry sag=0.dcm 270 0 0 0 0
Max 2D distance of any BB->Field: 5.27 mm
Mean 2D distance of any BB->Field: 2.00 mm
Median 2D distance of any BB->Field: 2.53 mm

BB # Description
-------- ----------------------------------
Iso Left 0.0mm, Up 5.0mm, In 0.0mm
Out Left 0.0mm, Up 0.0mm, Out 60.0mm
In Left 0.0mm, Up 0.0mm, In 30.0mm
Left/Out Left 10.0mm, Up 10.0mm, Out 30.0mm

Image G C P Iso Out In Left/Out
-------------------- --- --- --- ----- ----- ---- ----------
=0, Gantry sag=0.dcm 0 0 0 0 5.27 2.61 2.61
=0, Gantry sag=0.dcm 0 0 45 0.01 5.24 2.61 2.53
=0, Gantry sag=0.dcm 0 0 90 0 5.27 2.61 2.61
=0, Gantry sag=0.dcm 0 0 270 0 5.27 2.61 2.61
=0, Gantry sag=0.dcm 0 0 315 0.01 5.24 2.61 2.53
=0, Gantry sag=0.dcm 90 0 0 0.05 0.07 0.1 0.38
=0, Gantry sag=0.dcm 180 0 0 0 5.27 2.61 2.46
=0, Gantry sag=0.dcm 270 0 0 0.05 0.67 0.07 0.1

Image Couch Angle Yaw Error (°)
-------------------- ------------- ---------------
=0, Gantry sag=0.dcm 0 4.96
=0, Gantry sag=0.dcm 45 4.91
=0, Gantry sag=0.dcm 90 4.96
=0, Gantry sag=0.dcm 270 4.96
=0, Gantry sag=0.dcm 315 4.91

.. plot::
:include-source: false
Expand Down Expand Up @@ -181,6 +195,93 @@ And that's it! You can now view images, print the results, or publish a PDF repo
# print to PDF
wl.publish_pdf("mymtwl.pdf")
Visualizing BBs in space
------------------------

The BBs can be visualized by using the :meth:`~pylinac.winston_lutz.WinstonLutzMultiTargetMultiField.plot_location` method
and will show all the measured BB locations and their nominal locations.

.. plot::
:include-source: false

from pylinac.core.geometry import sin, cos
from pylinac.core.image_generator import AS1200Image, PerfectFieldLayer, GaussianFilterLayer, \
generate_winstonlutz_multi_bb_multi_field
from pylinac.winston_lutz import WinstonLutzMultiTargetMultiField, BBConfig

mtmf = 'mtmf'
generate_winstonlutz_multi_bb_multi_field(
simulator=AS1200Image(1000),
field_layer=PerfectFieldLayer,
final_layers=[GaussianFilterLayer(sigma_mm=1),],
dir_out=mtmf,
field_offsets=( # left, up, in
(0, 5, 0),
(0, 0, -60),
(10, 10, -30),
(0, 0, 30),
),
bb_offsets=(
(0, 5, 0),
(-60*sin(5), 0, -60*cos(5)),
(30*sin(5), 0, 30*cos(5)),
(10-30*sin(5), 10, -30/cos(5)),
),
field_size_mm=(20, 20),
bb_size_mm=5,
align_to_pixels=False,
image_axes=(
(0, 0, 0),
(90, 0, 0),
(180, 0, 0),
(270, 0, 0),
(0, 0, 90),
(0, 0, 45),
(0, 0, 270),
(0, 0, 315),
)
)

BBA = (
BBConfig(
name='Iso',
offset_left_mm=0,
offset_up_mm=5,
offset_in_mm=0,
bb_size_mm=5,
rad_size_mm=20,
),
BBConfig(
name="Out",
offset_left_mm=0,
offset_up_mm=00,
offset_in_mm=-60,
bb_size_mm=5,
rad_size_mm=20,
),
BBConfig(
name="In",
offset_left_mm=0,
offset_up_mm=00,
offset_in_mm=30,
bb_size_mm=5,
rad_size_mm=20,
),
BBConfig(
name="Left/Out",
offset_left_mm=10,
offset_up_mm=10,
offset_in_mm=-30,
bb_size_mm=5,
rad_size_mm=20,
),
)

wl = WinstonLutzMultiTargetMultiField(mtmf)
wl.analyze(bb_arrangement=BBA)
wl.plot_location()


Changing BB detection size
--------------------------

Expand Down Expand Up @@ -297,7 +398,7 @@ is true error vs algorithmic error can be difficult. The image generator module

.. note::

With the introduction of the MTWL algorithm, so to a multi-target synthetic image generator has been created: :func:`~pylinac.core.image_generator.utils.generate_winstonlutz_multi_bb_multi_field`.
With the introduction of the MTWL algorithm, so too a multi-target synthetic image generator has been created: :func:`~pylinac.core.image_generator.utils.generate_winstonlutz_multi_bb_multi_field`.

.. warning::

Expand Down
100 changes: 77 additions & 23 deletions pylinac/winston_lutz.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,16 +183,32 @@ class BBArrangement:
BBConfig(
name="Iso",
offset_left_mm=0,
offset_up_mm=0,
offset_up_mm=5,
offset_in_mm=0,
bb_size_mm=5,
rad_size_mm=20,
),
BBConfig(
name="Left,Down,In",
offset_left_mm=20,
offset_up_mm=-20,
offset_in_mm=60,
name="Out",
offset_left_mm=0,
offset_up_mm=00,
offset_in_mm=-60,
bb_size_mm=5,
rad_size_mm=20,
),
BBConfig(
name="In",
offset_left_mm=0,
offset_up_mm=00,
offset_in_mm=30,
bb_size_mm=5,
rad_size_mm=20,
),
BBConfig(
name="Left/Out",
offset_left_mm=10,
offset_up_mm=10,
offset_in_mm=-30,
bb_size_mm=5,
rad_size_mm=20,
),
Expand Down Expand Up @@ -415,6 +431,10 @@ class WinstonLutzMultiTargetMultiFieldResult(ResultBase):
mean_2d_field_to_bb_mm: float #:
bb_arrangement: tuple[BBConfig, ...] #:
bb_maxes: dict[str, float] #:
bb_shift_vector: VectorSerialized #:
bb_shift_yaw: float #:
bb_shift_pitch: float #:
bb_shift_roll: float #:


def is_near_center(region: RegionProperties, *args, **kwargs) -> bool:
Expand Down Expand Up @@ -2210,25 +2230,10 @@ def plot_location(
plt.show()
return fig, ax

def bb_shift_vector(
self, axes_order: str = "roll,pitch,yaw"
) -> (Vector, float, float, float):
@property
def bb_shift_vector(self) -> (Vector, float, float, float):
"""Calculate the ideal shift in 6 degrees of freedom to place the BB at the isocenter.
Parameters
----------
axes_order : str
The order of the axes to calculate the shift in. The order is the order of the rotations.
The default is 'roll,pitch,yaw'. The rule of thumb is to rotate about axes
with the smallest expected shift first. E.g. for a 4D couch the default value works well
because the roll and pitch should be small.
.. warning::
This order matters more than you think it would. Results for the yaw, pitch, and roll can
vary by a significant or unrealistic amount depending on the order and/or could result
in gimbal lock
Returns
-------
Vector
Expand All @@ -2248,9 +2253,42 @@ def bb_shift_vector(
return align_points(
measured_points=[bb.measured_bb_position for bb in self.bbs],
ideal_points=[bb.measured_field_position for bb in self.bbs],
axes_order=axes_order,
)

def bb_shift_instructions(self) -> str:
"""Return a string that provides instructions on how to shift the BB to the isocenter."""
translation, yaw, pitch, roll = self.bb_shift_vector
x_dir = "LEFT" if translation.x < 0 else "RIGHT"
y_dir = "IN" if translation.y > 0 else "OUT"
z_dir = "UP" if translation.z > 0 else "DOWN"
move = f"{x_dir} {abs(translation.x):2.2f}mm; {y_dir} {abs(translation.y):2.2f}mm; {z_dir} {abs(translation.z):2.2f}mm; Rotation {yaw:2.2f}°; Pitch {pitch:2.2f}°; Roll {roll:2.2f}°"
return move

def _couch_rotation_error(self) -> dict[str, dict[str, float]]:
"""Calculate the couch rotation error in degrees for reference and couch-kicked images.
This just for feature parity with SNC 🤦; the BB shift vector is more important.
Returns
-------
dict
A dictionary where the keys are the image paths and the values are a dictionary with the yaw error and nominal couch angle.
"""
couch_results = {}
couch_images = [
img
for img in self.images
if img.variable_axis in (Axis.COUCH, Axis.REFERENCE)
]
for img in couch_images:
measured_points = [m.bb for m in img.arrangement_matches.values()]
ideal_points = [m.field for m in img.arrangement_matches.values()]
_, yaw, _, _ = align_points(measured_points, ideal_points)
couch_results[img.base_path] = {
"yaw error": yaw,
"couch angle": img.couch_angle,
}
return couch_results

@property
def gantry_coll_iso_size(self) -> float:
raise NotImplementedError("Not yet implemented")
Expand Down Expand Up @@ -2322,13 +2360,18 @@ def _generate_results_data(self) -> WinstonLutzMultiTargetMultiFieldResult:
)
bb_maxes[bb.name] = max_d

translation, yaw, pitch, roll = self.bb_shift_vector
return WinstonLutzMultiTargetMultiFieldResult(
num_total_images=len(self.images),
max_2d_field_to_bb_mm=self.max_bb_deviation_2d,
mean_2d_field_to_bb_mm=self.mean_bb_deviation_2d,
median_2d_field_to_bb_mm=self.median_bb_deviation_2d,
bb_maxes=bb_maxes,
bb_arrangement=self.bb_arrangement,
bb_shift_vector=translation,
bb_shift_yaw=yaw,
bb_shift_pitch=pitch,
bb_shift_roll=roll,
)

def plot_summary(self, show: bool = True, fig_size: tuple | None = None):
Expand Down Expand Up @@ -2406,6 +2449,17 @@ def results(self, as_list: bool = False) -> str:
result += tabulate(data, headers=["Image", "G", "C", "P", *bb_names]).split(
"\n"
)
result += [""]

# calculate couch-kick errors
couch_results = self._couch_rotation_error()
couch_data = [
[name[-20:], v["couch angle"], f"{v['yaw error']:.2f}"]
for name, v in couch_results.items()
]
result += tabulate(
couch_data, headers=["Image", "Couch Angle", "Yaw Error (\N{DEGREE SIGN})"]
).split("\n")
if not as_list:
result = "\n".join(result)
return result
Expand Down
Loading

0 comments on commit f749f5a

Please sign in to comment.