Winston-Lutz Multi-Target

Overview

New in version 3.9.

The Multi-Target Winston-Lutz (MTWL) is an advanced test category meant to measure multiple locations away from isocenter, typically to represent multi-lesion SRS cases. The MTWL module can analyze images with any number of BBs in any arrangement. It is generalizable such that new phantom analyses can be created quickly.

Technically, there are two flavors of multi-target WL: multi-field and single field. An example of a multi-field WL is the SNC MultiMet. Each field is centered around each BB. The BB position is compared to that of the field. This is closest to what the patient experiences since it incorporates both the gantry/coll/couch deviations as well as the MLCs.

An example of a single-field multi-target WL is Machine Performance Check. The BBs are compared to the known positions. This removes the error of the MLCs to isolate just the gantry/coll/couch.

Currently, only the multi-field flavor is supported, but work on the single-field flavor will occur to support things like secondary checks of MPC.

This is why the class is called WinstonLutzMultiTargetMultiField as there will be an anticipated WinstonLutzMultiTargetSingleField.

Differences from single-target WL

Warning

The MTWL algorithm is new and provisional. There are a number of limitations with the algorithm. Hopefully, these are removed in future updates. The algorithm is still considered valuable even with these limitations which is why it is released.

Important

In a nutshell, the MTWL analyzes BB positions only, whereas vanilla WL provides more machine-related data as well as BB position data.

Unlike the single-target WL algorithm (aka “vanilla” WL), there are more limitations to acquisition and outputs. This should improve over time, but for now you can think of the MTWL as a subset of the vanilla WL algorithm:

  • Utility methods such as loading images are the same.
  • Outputs related to the BBs are different.
  • BB size is not a parameter but is part of the BB arrangement.
  • Single images cannot be analyzed.
  • Axis deviations (Gantry wobble, etc) are not yet available.
  • Couch rotation images are dropped as they cannot yet be handled.
  • Interpreting filenames is not yet allowed.

See the following sections for more info.

Running the Demo

To run the multi-target Winston-Lutz demo, create a script or start an interpreter session and input:

from pylinac import WinstonLutzMultiTargetMultiField

WinstonLutzMultiTargetMultiField.run_demo()

Results will be printed to the console and a figure showing the zoomed-in images will be generated:

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

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

(Source code)

Image Acquisition

The Winston-Lutz module will only load EPID images. The images can be from any EPID however and any SID. To ensure the most accurate results the following should be noted:

  • Images with a rotated couch are dropped and not analyzed (yet) but will not cause an error.
  • The BBs should not occlude each other.
  • The BBs should be >5mm apart in any given image.
  • The radiation fields should have >5mm separation in any given image.
  • The BB and radiation field should be <=5 mm away from the nominal location given by the arrangement.

Coordinate Space

The MTWL algorithm uses the same coordinate system as the vanilla WL. Coordinate Space.

Passing a coordinate system

No coordinate system is passed or used (yet).

Note

This is a target for the MTWL algorithm, so expect this to change in the future.

Supported Phantoms

Currently, only the MultiMet-WL cube from SNC is supported. However, the algorithm is generalized and can be easily adapted to analyze other phantoms. See Custom BB Arrangements.

Typical Use

Analyzing a multi-target Winston-Lutz test is simple. First, let’s import the class:

from pylinac import WinstonLutzMultiTargetMultiField
from pylinac.winston_lutz import BBArrangement

From here, you can load a directory:

my_directory = 'path/to/wl_images'
wl = WinstonLutzMultiTargetMultiField(my_directory)

You can also load a ZIP archive with the images in it:

wl = WinstonLutzMultiTargetMultiField.from_zip('path/to/wl.zip')

Now, analyze it. Unlike the vanilla WL algorithm, we have to pass the BB arrangement to know where the BBs should be in space. Preset phantoms exist, or a custom arrangement can be passed.

wl.analyze(bb_arrangement=BBArrangement.SNC_MULTIMET)

And that’s it! You can now view images, print the results, or publish a PDF report:

# plot all the images
wl.plot_images()
# save figures of the image plots for each bb
wl.save_images(prefix='snc')
# print to PDF
wl.publish_pdf('mymtwl.pdf')

Changing BB detection size

To change the size of BB pylinac is expecting you must change it in the BB arrangement. This allows phantoms with multiple BB sizes to still be analyzed. See Custom BB Arrangements

Custom BB Arrangements

The MTWL algorithm uses a priori BB arrangements. I.e. you need to know where the BBs should exist in space relative to isocenter. The MTWL algorithm is flexible to accommodate any reasonable arrangement of BBs.

To create a custom arrangement, say for an in-house phantom or commercial phantom not yet supported, define the BB offsets and size like so. Use negative values to move the other direction:

my_special_phantom_bbs = [
    {'offset_left_mm': 0, 'offset_up_mm': 0, 'offset_in_mm': 0, 'bb_size_mm': 5, 'rad_size_mm': 20},  # 5mm BB at iso
    {'offset_left_mm': 30, 'offset_up_mm': 0, 'offset_in_mm': 0, 'bb_size_mm': 4, 'rad_size_mm': 20},  # 4mm BB 30mm to left of iso
    {'offset_left_mm': 0, 'offset_up_mm': -20, 'offset_in_mm': 10, 'bb_size_mm': 5, 'rad_size_mm': 20},  # BB DOWN 20mm and in 10mm
    ...  # keep going as needed
    )
]

Pass it to the algorithm like so:

wl = WinstonLutzMultiTargetMultiField(...)
wl.analyze(bb_arrangement=my_special_phantom_bbs)
...

Algorithm

The MTWL algorithm is based on the vanilla WL algorithm. For each BB and image combination, the image is searched at the nominal location for the BB and radiation field. If it’s not found it will be skipped for that combo. The BB must be detected in at least one image or an error will be raised.

The algorithm works like such:

Allowances

  • The images can be acquired with any EPID (aS500, aS1000, aS1200) at any SID.
  • The image can have any number of BBs.
  • The BBs can be at any 3D location.

Restrictions

Warning

Analysis can fail or give unreliable results if any Restriction is violated.

  • Each BB and radiation field must be within 5mm of the expected position in x and y in the EPID plane. I.e. it must be <=7mm in scalar distance.
  • BBs must not occlude or be <5 mm from each other in any 2D image.
  • Images with a rotated couch are dropped and not analyzed (yet) but will not cause an error.
  • The radiation fields should have >5mm separation in any given image.

Analysis

This algorithm is performed for each BB and image combination:

  • Find the field center – The spread in pixel values (max - min) is divided by 2, and any pixels above the threshold is associated with the open field. The pixels are converted to black & white and the center of mass of the pixels is assumed to be the field center.
  • Find the BB – The image is converted to binary based on pixel values both above the 50% threshold as above, and below the upper threshold. The upper threshold is an iterative value, starting at the image maximum value, that is lowered slightly when the BB is not found. If the binary image has a reasonably circular ROI, is approximately the right size, and is within 5mm of the expected BB position, the BB is considered found and the pixel-weighted center of mass of the BB is considered the BB location.
  • Evaluate against the field position – Once the measured BB and field positions are known, both the scalar distance and vector from the field position to the measured BB position is determined.

Benchmarking the Algorithm

With the image generator module we can create test images to test the WL algorithm on known results. This is useful to isolate what is or isn’t working if the algorithm doesn’t work on a given image and when commissioning pylinac. It is common, especially with the WL module, to question the accuracy of the algorithm. Since no linac is perfect and the results are sub-millimeter, discerning what is true error vs algorithmic error can be difficult. The image generator module is a perfect solution since it can remove or reproduce the former error.

Note

With the introduction of the MTWL algorithm, so to a multi-target synthetic image generator has been created: generate_winstonlutz_multi_bb_multi_field().

Warning

The image generator is limited in accuracy to ~1/2 pixel because creating the image requires a row or column to be set. E.g. a 5mm field with a 0.336mm pixel size means we need to create a field of 14.88 pixels wide. We can only set the field to be 14 or 15 pixels, so the nearest field size of 15 pixels or 5.04mm is set.

2-BB Perfect Delivery

Create a perfect set of fields with 1 BB at iso and another 20mm left, 20mm down, and 60mm inward (this is the same as the demo, but is good for explanation).

import pylinac
from pylinac.core.image_generator import simulators, layers, generate_winstonlutz_multi_bb_multi_field

wl_dir = 'wl_dir'
generate_winstonlutz_multi_bb_multi_field(
        simulator=simulators.AS1200Image(sid=1000),
        field_layer=layers.PerfectFieldLayer,
        final_layers=[layers.GaussianFilterLayer(sigma_mm=1),],
        dir_out=wl_dir,
        field_offsets=((0, 0, 0), (20, -20, 60)),
        field_size_mm=(20, 20),
        bb_offsets=[[0, 0, 0], [20, -20, 60]],
)
arrange = (
    {'name': 'Iso', 'offset_left_mm': 0, 'offset_up_mm': 0, 'offset_in_mm': 0, 'bb_size_mm': 5, 'rad_size_mm': 20},
    {'name': 'Left,Down,In', 'offset_left_mm': 20, 'offset_up_mm': -20, 'offset_in_mm': 60, 'bb_size_mm': 5, 'rad_size_mm': 20},)

wl = pylinac.WinstonLutzMultiTargetMultiField(wl_dir)
wl.analyze(bb_arrangement=arrange)
print(wl.results())

(Source code)

which has an output of:

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

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

As shown, we have perfect results.

Offset BBs

Let’s now offset both BBs by 1mm to the left:

import pylinac
from pylinac.core.image_generator import simulators, layers, generate_winstonlutz_multi_bb_multi_field

wl_dir = 'wl_dir'
generate_winstonlutz_multi_bb_multi_field(
        simulator=simulators.AS1200Image(sid=1000),
        field_layer=layers.PerfectFieldLayer,
        final_layers=[layers.GaussianFilterLayer(sigma_mm=1),],
        dir_out=wl_dir,
        field_offsets=((0, 0, 0), (20, -20, 60)),
        field_size_mm=(20, 20),
        bb_offsets=[[1, 0, 0], [19, -20, 60]],  # here's the offset
)
arrange = (
    {'name': 'Iso', 'offset_left_mm': 0, 'offset_up_mm': 0, 'offset_in_mm': 0, 'bb_size_mm': 5, 'rad_size_mm': 20},
    {'name': 'Left,Down,In', 'offset_left_mm': 20, 'offset_up_mm': -20, 'offset_in_mm': 60, 'bb_size_mm': 5, 'rad_size_mm': 20},)

wl = pylinac.WinstonLutzMultiTargetMultiField(wl_dir)
wl.analyze(bb_arrangement=arrange)
print(wl.results())

(Source code)

with an output of:

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

2D distances
============
Max 2D distance of any BB: 1.01 mm
Mean 2D distance of any BB: 1.01 mm
Median 2D distance of any BB: 1.01 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     1.01     1.01
=0, Gantry sag=0.dcm   90     0     0     0        0
=0, Gantry sag=0.dcm  180     0     0     1.01     1.01
=0, Gantry sag=0.dcm  270     0     0     0        0

Both BBs report a shift of 1mm. Note this is only in 0 and 180. A left shift would not be captured at 90/270.

Random error

Let’s now add random error:

Note

The error is random so performing this again will change the results slightly.

import pylinac
from pylinac.core.image_generator import simulators, layers, generate_winstonlutz_multi_bb_multi_field

wl_dir = 'wl_dir'
generate_winstonlutz_multi_bb_multi_field(
        simulator=simulators.AS1200Image(sid=1000),
        field_layer=layers.PerfectFieldLayer,
        final_layers=[layers.GaussianFilterLayer(sigma_mm=1),],
        dir_out=wl_dir,
        field_offsets=((0, 0, 0), (20, -20, 60)),
        field_size_mm=(20, 20),
        bb_offsets=[[0, 0, 0], [20, -20, 60]],
        jitter_mm=2  # here we add random noise
)
arrange = (
    {'name': 'Iso', 'offset_left_mm': 0, 'offset_up_mm': 0, 'offset_in_mm': 0, 'bb_size_mm': 5, 'rad_size_mm': 20},
    {'name': 'Left,Down,In', 'offset_left_mm': 20, 'offset_up_mm': -20, 'offset_in_mm': 60, 'bb_size_mm': 5, 'rad_size_mm': 20},)

wl = pylinac.WinstonLutzMultiTargetMultiField(wl_dir)
wl.analyze(bb_arrangement=arrange)
print(wl.results())

(Source code)

with an output of:

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

2D distances
============
Max 2D distance of any BB: 3.38 mm
Mean 2D distance of any BB: 2.82 mm
Median 2D distance of any BB: 2.82 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     2.25     0.34
=0, Gantry sag=0.dcm   90     0     0     2.15     2.77
=0, Gantry sag=0.dcm  180     0     0     2.13     3.38
=0, Gantry sag=0.dcm  270     0     0     2.25     2.42

API Documentation

class pylinac.winston_lutz.WinstonLutz(directory: str | list[str] | Path, use_filenames: bool = False, axis_mapping: dict[str, tuple[int, int, int]] | None = None)[source]

Bases: object

Class for performing a Winston-Lutz test of the radiation isocenter.

Parameters:
  • directory (str, list[str]) – Path to the directory of the Winston-Lutz EPID images or a list of the image paths
  • use_filenames (bool) – Whether to try to use the file name to determine axis values. Useful for Elekta machines that do not include that info in the DICOM data. This is mutually exclusive to axis_mapping. If True, axis_mapping is ignored.
  • axis_mapping (dict) – An optional way of instantiating by passing each file along with the axis values. Structure should be <filename>: (<gantry>, <coll>, <couch>).
machine_scale = None
image_type

alias of WinstonLutz2D

images = None
classmethod from_demo_images()[source]

Instantiate using the demo images.

classmethod from_zip(zfile: str | BinaryIO, use_filenames: bool = False, axis_mapping: dict[str, tuple[int, int, int]] | None = None)[source]

Instantiate from a zip file rather than a directory.

Parameters:
  • zfile – Path to the archive file.
  • use_filenames (bool) – Whether to interpret axis angles using the filenames. Set to true for Elekta machines where the gantry/coll/couch data is not in the DICOM metadata.
  • axis_mapping (dict) – An optional way of instantiating by passing each file along with the axis values. Structure should be <filename>: (<gantry>, <coll>, <couch>).
classmethod from_url(url: str, use_filenames: bool = False)[source]

Instantiate from a URL.

Parameters:
  • url (str) – URL that points to a zip archive of the DICOM images.
  • use_filenames (bool) – Whether to interpret axis angles using the filenames. Set to true for Elekta machines where the gantry/coll/couch data is not in the DICOM metadata.
static run_demo()[source]

Run the Winston-Lutz demo, which loads the demo files, prints results, and plots a summary image.

analyze(bb_size_mm: float = 5, machine_scale: pylinac.core.scale.MachineScale = <MachineScale.IEC61217: {'gantry_to_iec': <function noop>, 'collimator_to_iec': <function noop>, 'rotation_to_iec': <function noop>, 'gantry_from_iec': <function noop>, 'collimator_from_iec': <function noop>, 'rotation_from_iec': <function noop>}>, low_density_bb: bool = False)[source]

Analyze the WL images.

Parameters:
  • bb_size_mm – The expected size of the BB in mm. The actual size of the BB can be +/-2mm from the passed value.
  • machine_scale – The scale of the machine. Shift vectors depend on this value.
  • low_density_bb – Set this flag to True if the BB is lower density than the material surrounding it.
gantry_iso_size

The diameter of the 3D gantry isocenter size in mm. Only images where the collimator and couch were at 0 are used to determine this value.

gantry_coll_iso_size

The diameter of the 3D gantry isocenter size in mm including collimator and gantry/coll combo images. Images where the couch!=0 are excluded.

collimator_iso_size

The 2D collimator isocenter size (diameter) in mm. The iso size is in the plane normal to the gantry.

couch_iso_size

The diameter of the 2D couch isocenter size in mm. Only images where the gantry and collimator were at zero are used to determine this value.

bb_shift_vector

The shift necessary to place the BB at the radiation isocenter. The values are in the coordinates defined in the documentation.

The shift is based on the paper by Low et al. See online documentation for more.

bb_shift_instructions(couch_vrt: float | None = None, couch_lng: float | None = None, couch_lat: float | None = None) → str[source]

Returns a string describing how to shift the BB to the radiation isocenter looking from the foot of the couch. Optionally, the current couch values can be passed in to get the new couch values. If passing the current couch position all values must be passed.

Parameters:
  • couch_vrt (float) – The current couch vertical position in cm.
  • couch_lng (float) – The current couch longitudinal position in cm.
  • couch_lat (float) – The current couch lateral position in cm.
axis_rms_deviation(axis: Axis | tuple[Axis, ...] = <Axis.GANTRY: 'Gantry'>, value: str = 'all') → Iterable | float[source]

The RMS deviations of a given axis/axes.

Parameters:
  • axis (('Gantry', 'Collimator', 'Couch', 'Epid', 'GB Combo', 'GBP Combo')) – The axis desired.
  • value ({'all', 'range'}) – Whether to return all the RMS values from all images for that axis, or only return the maximum range of values, i.e. the ‘sag’.
cax2bb_distance(metric: str = 'max') → float[source]

The distance in mm between the CAX and BB for all images according to the given metric.

Parameters:metric ({'max', 'median', 'mean'}) – The metric of distance to use.
cax2epid_distance(metric: str = 'max') → float[source]

The distance in mm between the CAX and EPID center pixel for all images according to the given metric.

Parameters:metric ({'max', 'median', 'mean'}) – The metric of distance to use.
plot_axis_images(axis: Axis = <Axis.GANTRY: 'Gantry'>, show: bool = True, ax: plt.Axes | None = None)[source]

Plot all CAX/BB/EPID positions for the images of a given axis.

For example, axis=’Couch’ plots a reference image, and all the BB points of the other images where the couch was moving.

Parameters:
  • axis ({'Gantry', 'Collimator', 'Couch', 'GB Combo', 'GBP Combo'}) – The images/markers from which accelerator axis to plot.
  • show (bool) – Whether to actually show the images.
  • ax (None, matplotlib.Axes) – The axis to plot to. If None, creates a new plot.
plot_images(axis: Axis = <Axis.GANTRY: 'Gantry'>, show: bool = True, split: bool = False, **kwargs) -> (list[plt.Figure], list[str])[source]

Plot a grid of all the images acquired.

Four columns are plotted with the titles showing which axis that column represents.

Parameters:
  • axis ({'Gantry', 'Collimator', 'Couch', 'GB Combo', 'GBP Combo', 'All'}) –
  • show (bool) – Whether to show the image.
  • split (bool) – Whether to show/plot the images individually or as one large figure.
save_images(filename: str | BinaryIO, axis: Axis = <Axis.GANTRY: 'Gantry'>, **kwargs)[source]

Save the figure of plot_images() to file. Keyword arguments are passed to matplotlib.pyplot.savefig().

Parameters:
  • filename (str) – The name of the file to save to.
  • axis – The axis to save.
save_images_to_stream(**kwargs) → dict[source]

Save the individual image plots to stream

plot_summary(show: bool = True, fig_size: tuple | None = None)[source]

Plot a summary figure showing the gantry sag and wobble plots of the three axes.

save_summary(filename: str | BinaryIO, **kwargs)[source]

Save the summary image.

results(as_list: bool = False) → str[source]

Return the analysis results summary.

Parameters:as_list (bool) – Whether to return as a list of strings vs single string. Pretty much for internal usage.
results_data(as_dict=False) → WinstonLutzResult | dict[source]

Present the results data and metadata as a dataclass or dict. The default return type is a dataclass.

publish_pdf(filename: str, notes: str | list[str] | None = None, open_file: bool = False, metadata: dict | None = None, logo: Path | str | None = None)[source]

Publish (print) a PDF containing the analysis, images, and quantitative results.

Parameters:
  • filename ((str, file-like object}) – The file to write the results to.
  • notes (str, list of strings) – Text; if str, prints single line. If list of strings, each list item is printed on its own line.
  • open_file (bool) – Whether to open the file using the default program after creation.
  • metadata (dict) – Extra data to be passed and shown in the PDF. The key and value will be shown with a colon. E.g. passing {‘Author’: ‘James’, ‘Unit’: ‘TrueBeam’} would result in text in the PDF like: ————– Author: James Unit: TrueBeam ————–
  • logo (Path, str) – A custom logo to use in the PDF report. If nothing is passed, the default pylinac logo is used.
class pylinac.winston_lutz.WinstonLutzMultiTargetMultiField(*args, **kwargs)[source]

Bases: pylinac.winston_lutz.WinstonLutz

We cannot yet handle non-0 couch angles so we drop them. Analysis fails otherwise

analyzed_images = None
image_type

alias of WinstonLutz2DMultiTarget

bb_arrangement = None
images = None
classmethod from_demo_images()[source]

Instantiate using the demo images.

static run_demo()[source]

Run the Winston-Lutz MT MF demo, which loads the demo files, prints results, and plots a summary image.

analyze(bb_arrangement: Iterable[dict])[source]

Analyze the WL images.

Parameters:bb_arrangement – The arrangement of the BBs in the phantom. A dict with offset and BB size keys. See the BBArrangement class for keys and syntax.
plot_images(show: bool = True, **kwargs) -> (list[plt.Figure], list[str])[source]

Make a plot for each BB. Each plot contains the analysis of that BB on each image it was found.

save_images(prefix: str = '', **kwargs)[source]

Save the figure of plot_images() to file as PNG. Keyword arguments are passed to matplotlib.pyplot.savefig().

Parameters:prefix (str) – The prefix name of the file to save to. The BB name is appended to the prefix.
save_images_to_stream(**kwargs) → dict[source]

Save the individual image plots to stream

cax2bb_distance(bb: str, metric: str = 'max') → float[source]

The distance in mm between the CAX and BB for all images according to the given metric.

Parameters:
  • metric ({'max', 'median', 'mean'}) – The metric of distance to use.
  • bb (str) – The BB to analyze
results_data(as_dict: bool = False) → WinstonLutzMultiTargetMultiFieldResult | dict[source]

Present the results data and metadata as a dataclass or dict. The default return type is a dataclass.

plot_summary(show: bool = True, fig_size: tuple | None = None)[source]

Plot a summary figure showing the gantry sag and wobble plots of the three axes.

plot_axis_images(axis: Axis = <Axis.GANTRY: 'Gantry'>, show: bool = True, ax: plt.Axes | None = None)[source]

Plot all CAX/BB/EPID positions for the images of a given axis.

For example, axis=’Couch’ plots a reference image, and all the BB points of the other images where the couch was moving.

Parameters:
  • axis ({'Gantry', 'Collimator', 'Couch', 'GB Combo', 'GBP Combo'}) – The images/markers from which accelerator axis to plot.
  • show (bool) – Whether to actually show the images.
  • ax (None, matplotlib.Axes) – The axis to plot to. If None, creates a new plot.
max_bb_deviation_2d

The maximum distance from any measured BB to its nominal position

mean_bb_deviation_2d

The mean distance from any measured BB to its nominal position

median_bb_deviation_2d

The median distance from any measured BB to its nominal position

results(as_list: bool = False) → str[source]

Return the analysis results summary.

Parameters:as_list (bool) – Whether to return as a list of strings vs single string. Pretty much for internal usage.
publish_pdf(filename: str, notes: str | list[str] | None = None, open_file: bool = False, metadata: dict | None = None, logo: Path | str | None = None)[source]

Publish (print) a PDF containing the analysis, images, and quantitative results.

Parameters:
  • filename ((str, file-like object)) – The file to write the results to.
  • notes (str, list of strings) – Text; if str, prints single line. If list of strings, each list item is printed on its own line.
  • open_file (bool) – Whether to open the file using the default program after creation.
  • metadata (dict) – Extra data to be passed and shown in the PDF. The key and value will be shown with a colon. E.g. passing {‘Author’: ‘James’, ‘Unit’: ‘TrueBeam’} would result in text in the PDF like: ————– Author: James Unit: TrueBeam ————–
  • logo (Path, str) – A custom logo to use in the PDF report. If nothing is passed, the default pylinac logo is used.
class pylinac.winston_lutz.WinstonLutz2D(file: str | BinaryIO | Path, use_filenames: bool = False, **kwargs)[source]

Bases: pylinac.core.image.LinacDicomImage

Holds individual Winston-Lutz EPID images, image properties, and automatically finds the field CAX and BB.

Parameters:
  • file (str) – Path to the image file.
  • use_filenames (bool) – Whether to try to use the file name to determine axis values. Useful for Elekta machines that do not include that info in the DICOM data.
analyze(bb_size_mm: float = 5, low_density_bb: bool = False) → None[source]

Analyze the image.

to_axes() → str[source]

Give just the axes values as a human-readable string

epid

Center of the EPID panel

cax_line_projection

The projection of the field CAX through space around the area of the BB. Used for determining gantry isocenter size.

Returns:The virtual line in space made by the beam CAX.
Return type:Line
cax2bb_vector

The vector in mm from the CAX to the BB.

cax2bb_distance

The scalar distance in mm from the CAX to the BB.

cax2epid_vector

The vector in mm from the CAX to the EPID center pixel

cax2epid_distance

The scalar distance in mm from the CAX to the EPID center pixel

plot(ax: plt.Axes | None = None, show: bool = True, clear_fig: bool = False)[source]

Plot the image, zoomed-in on the radiation field, along with the detected BB location and field CAX location.

Parameters:
  • ax (None, matplotlib Axes instance) – The axis to plot to. If None, will create a new figure.
  • show (bool) – Whether to actually show the image.
  • clear_fig (bool) – Whether to clear the figure first before drawing.
save_plot(filename: str, **kwargs)[source]

Save the image plot to file.

variable_axis

The axis that is varying.

There are five types of images:

  • Reference : All axes are at 0.
  • Gantry: All axes but gantry at 0.
  • Collimator : All axes but collimator at 0.
  • Couch : All axes but couch at 0.
  • Combo : More than one axis is not at 0.
results_data(as_dict=False) → WinstonLutz2DResult | dict[source]

Present the results data and metadata as a dataclass or dict. The default return type is a dataclass.

class pylinac.winston_lutz.WinstonLutz2DMultiTarget(*args, **kwargs)[source]

Bases: pylinac.winston_lutz.WinstonLutz2D

A 2D image of a WL delivery, but where multiple BBs are in use.

as_analyzed(bb_location: dict) → pylinac.winston_lutz.WinstonLutz2DMultiTarget[source]

Analyze the image of the multi-BB setup. We return a copy of the WL image because we analyze images more than once. Each “analyzed” image is really the analysis of a BB/image combo.

Parameters:bb_location – An iterable of dictionaries. Each dict contains keys for the offsets and size of the BB in mm. Use the BBArrangement class as a guide.
plot(ax: plt.Axes | None = None, show: bool = True, clear_fig: bool = False)[source]

Plot the image, zoomed-in on the radiation field, along with the detected BB location and field CAX location.

Parameters:
  • ax (None, matplotlib Axes instance) – The axis to plot to. If None, will create a new figure.
  • show (bool) – Whether to actually show the image.
  • clear_fig (bool) – Whether to clear the figure first before drawing.
location_near_nominal(region: skimage.measure._regionprops.RegionProperties, location: dict) → bool[source]

Determine whether the given BB ROI is near where the BB is expected to be

results_data(as_dict: bool = False) → WinstonLutz2DResult | dict[source]

Present the results data and metadata as a dataclass or dict. The default return type is a dataclass.

class pylinac.winston_lutz.WinstonLutzResult(num_gantry_images: int, num_gantry_coll_images: int, num_coll_images: int, num_couch_images: int, num_total_images: int, max_2d_cax_to_bb_mm: float, median_2d_cax_to_bb_mm: float, mean_2d_cax_to_bb_mm: float, max_2d_cax_to_epid_mm: float, median_2d_cax_to_epid_mm: float, mean_2d_cax_to_epid_mm: float, gantry_3d_iso_diameter_mm: float, max_gantry_rms_deviation_mm: float, max_epid_rms_deviation_mm: float, gantry_coll_3d_iso_diameter_mm: float, coll_2d_iso_diameter_mm: float, max_coll_rms_deviation_mm: float, couch_2d_iso_diameter_mm: float, max_couch_rms_deviation_mm: float, image_details: list)[source]

Bases: pylinac.core.utilities.ResultBase

This class should not be called directly. It is returned by the results_data() method. It is a dataclass under the hood and thus comes with all the dunder magic.

Use the following attributes as normal class attributes.

num_gantry_images = None
num_gantry_coll_images = None
num_coll_images = None
num_couch_images = None
num_total_images = None
max_2d_cax_to_bb_mm = None
median_2d_cax_to_bb_mm = None
mean_2d_cax_to_bb_mm = None
max_2d_cax_to_epid_mm = None
median_2d_cax_to_epid_mm = None
mean_2d_cax_to_epid_mm = None
gantry_3d_iso_diameter_mm = None
max_gantry_rms_deviation_mm = None
max_epid_rms_deviation_mm = None
gantry_coll_3d_iso_diameter_mm = None
coll_2d_iso_diameter_mm = None
max_coll_rms_deviation_mm = None
couch_2d_iso_diameter_mm = None
max_couch_rms_deviation_mm = None
image_details = None
class pylinac.winston_lutz.WinstonLutz2DResult(variable_axis: 'str', cax2epid_vector: 'Vector', cax2epid_distance: 'float', cax2bb_distance: 'float', cax2bb_vector: 'Vector', bb_location: 'Point', field_cax: 'Point')[source]

Bases: pylinac.core.utilities.ResultBase

variable_axis = None
cax2epid_vector = None
cax2epid_distance = None
cax2bb_distance = None
cax2bb_vector = None
bb_location = None
field_cax = None
class pylinac.winston_lutz.WinstonLutzMultiTargetMultiFieldResult(num_total_images: int, max_2d_field_to_bb_mm: float, median_2d_field_to_bb_mm: float, mean_2d_field_to_bb_mm: float, bb_arrangement: Iterable[dict], bb_maxes: dict)[source]

Bases: pylinac.core.utilities.ResultBase

This class should not be called directly. It is returned by the results_data() method. It is a dataclass under the hood and thus comes with all the dunder magic.

Use the following attributes as normal class attributes.

num_total_images = None
max_2d_field_to_bb_mm = None
median_2d_field_to_bb_mm = None
mean_2d_field_to_bb_mm = None
bb_arrangement = None
bb_maxes = None