Picket Fence module documentation

Overview

The picket fence module is meant for analyzing EPID images where a “picket fence” MLC pattern has been made. Physicists regularly check MLC positioning through this test. This test can be done using film and one can “eyeball” it, but this is the 21st century and we have numerous ways of quantifying such data. This module attains to be one of them. It can load in an EPID dicom image (or superimpose multiple images) and determine the MLC peaks, error of each MLC pair to the picket, and give a few visual indicators for passing/warning/failing.

Features:

  • Analyze either HD or regular MLCs - Just pass a flag and tell pylinac whether it’s HD or not.
  • Easy-to-read pass/warn/fail overlay - Analysis gives you easy-to-read tools for determining the status of an MLC pair.
  • Any Source-to-Image distance - Whatever your clinic uses as the SID for picket fence, pylinac can account for it.
  • Account for panel translation - Have an off-CAX setup? No problem. Translate your EPID and pylinac knows.
  • Account for panel sag - If your EPID sags at certain angles, just tell pylinac and the results will be shifted.

Concepts

Although most terminology will be familiar to a clinical physicist, it is still helpful to be clear about what means what. A “picket” is the line formed by several MLC pairs all at the same position. There is usually some ideal gap between the MLCs, such as 0.5, 1, or 2 mm. An “MLC position” is, for pylinac’s purposes, the center of the FWHM of the peak formed by one MLC pair at one picket. Thus, one picket fence image may have anywhere between a few to a dozen pickets, formed by as few as 10 MLC pairs up to all 60 pairs.

Pylinac presents the analyzed image in such a way that allows for quick assessment; additionally, all elements atop the image can optionally be turned off. Pylinac by default will plot the image, the determined MLC positions, two “guard rails”, and a semi-transparent overlay over the entire MLC pair region. The guard rails are two lines parallel to the fitted picket, offset by the tolerance passed to analyze(). Thus, if a tolerance of 0.5 mm is passed, each guard rail is 0.5 mm to the left and right of the invisible picket. Ideally, MLC positions will all be within these guard rails, i.e. within tolerance, and will be colored blue. If they are outside the tolerance they are turned red. If an “action tolerance” is also passed to analyze(), MLC positions that are below tolerance but above the action tolerance are turned magenta.

Additionally, pylinac provides a semi-transparent colored overlay so that an “all clear” or a “pair(s) failed” status is easily seen and not inadvertently overlooked. If any MLC position is outside the action tolerance or the absolute tolerance, the entire MLC pair area is colored the corresponding color. In this way, not every position needs be looked at. If all rows are green, then all positions passed.

Running the Demo

To run the picketfence demo, create a script or start in interpreter and input:

from pylinac import PicketFence

PicketFence.run_demo()

Results will be printed to the console and a figure showing the analyzed picket fence image will pop up:

Picket Fence Results:
100.0% Passed
Median Error: 0.062mm
Max Error: 0.208mm on Picket: 3, Leaf: 22

(Source code, png, hires.png, pdf)

_images/picketfence-1.png

Finally, you can save the results to a PDF report:

pf = PicketFence.from_demo()
pf.analyze()
pf.publish_pdf(filename='PF Oct-2018.pdf')

Acquiring the Image

The easiest way to acquire a picket fence image is using the EPID. In fact, pylinac will only analyze images acquired via an EPID, as the DICOM image it produces carries important information about the SID, pixel/mm conversion, etc. Depending on the EPID type and physicist, either the entire array of MLCs can be imaged at once, or only the middle leaves are acquired. Changing the SID can also change how many leaves are imaged. For analysis by pylinac, the SID does not matter, nor EPID type, nor panel translation.

Typical Use

Picket Fence tests are recommended to be done weekly. With automatic software analysis, this can be a trivial task. Once the test is delivered to the EPID, retrieve the DICOM image and save it to a known location. Then import the class:

from pylinac import PicketFence

The minimum needed to get going is to:

  • Load the image – As with most other pylinac modules, loading images can be done by passing the image string directly, or by using a UI dialog box to retrieve the image manually. The code might look like either of the following:

    pf_img = r"C:/QA Folder/June/PF_6_21.dcm"
    pf = PicketFence(pf_img)
    

    You may also load multiple images that become superimposed (e.g. an MLC & Jaw irradiation):

    img1 = r'path/to/image1.dcm'
    img2 = r'path/to/image2.dcm'
    pf = PicketFence.from_multiple_images([img1, img2])
    

    As well, you can use the demo image provided:

    pf = PicketFence.from_demo_image()
    
  • Analyze the image – Once the image is loaded, tell PicketFence to start analyzing the image. See the Algorithm section for details on how this is done. While defaults exist, you may pass in a tolerance as well as an “action” tolerance (meaning that while passing, action should be required above this tolerance):

    pf.analyze(tolerance=0.15, action_tolerance=0.03)  # tight tolerance to demo fail & warning overlay
    
  • View the results – The PicketFence class can print out the summary of results to the console as well as draw a matplotlib image to show the image, MLC peaks, guard rails, and a color overlay for quick assessment:

    # print results to the console
    print(pf.results())
    # view analyzed image
    pf.plot_analyzed_image()
    

    which results in:

    (Source code, png, hires.png, pdf)

    _images/picketfence-2.png

    The plot is also able to be saved to PNG:

    pf.save_analyzed_image('mypf.png')
    

    Or you may save to PDF:

    pf.publish_pdf('mypf.pdf')
    

Using a Machine Log

As of v1.4, you can load a machine log along with your picket fence image. The algorithm will use the expected fluence of the log to determine where the pickets should be instead of fitting to the MLC peaks. Usage looks like this:

from pylinac import PicketFence

pf = PicketFence('my/pf.dcm', log='my/pf_log.bin')
...

Everything else is the same except the measurements are absolute.

Warning

While using a machine log makes the MLC peak error absolute, there may be EPID twist or sag that will exaggerate differences that may or may not be real. Be sure to understand how your imager moves during your picket fence delivery. Even TrueBeams are not immune to EPID twist.

Results will look similar. Here’s an example of the results of using a log:

_images/PF_with_log.png

Tips & Tricks

Using the picketfence module in your own scripts? While the analysis results can be printed out, if you intend on using them elsewhere, they can be accessed through properties. Continuing from above:

pf.max_error  # max error in mm
pf.max_error_picket  # which picket contained the max error
pf.max_error_leaf  # which leaf contained the maximum error
pf.abs_median_error  # the absolute median error of all the leaves
pf.num_pickets  # how many pickets were found
pf.percent_passing  # the percent of MLC measurements below tolerance

The EPID can also sag at certain angles. Because pylinac assumes a perfect panel, sometimes the analysis will not be centered exactly on the MLC leaves. If you want to correct for this, simply pass the EPID sag in mm:

pf = PicketFence(r'C:/path/saggyPF.dcm')
pf.analyze(sag_adjustment=0.6)

Algorithm

The picket fence algorithm uses expected lateral positions of the MLCs and samples those regions for the center of the FWHM to determine the MLC positions:

Allowances

  • The image can be any size.
  • Various leaf sizes can be analyzed (e.g. 5 and 10mm leaves for standard Millennium).
  • Either standard or HD MLCs can be analyzed.
  • The image can be either orientation (pickets going up-down or left-right).
  • The image can be at any SSD.
  • Any EPID type can be used (aS500, aS1000, aS1200).
  • The EPID panel can have an x or y offset (i.e. translation).

Restrictions

Warning

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

  • The image must be a DICOM image acquired via the EPID.
  • Only Varian MLC models are supported (5/10mm or 2.5/5mm leaf combinations).
  • The delivery must be parallel to an image edge; i.e. the collimator should be at 0, 90, or 270 degrees.

Pre-Analysis

  • Check for noise – Dead pixels can cause wild values in an otherwise well-behaved image. These values can disrupt analysis, but pylinac will try to detect the presence of noise and will apply a median filter if detected.
  • Check image inversion – Upon loading, the image is sampled near all 4 corners for pixel values. If it is greater than the mean pixel value of the entire image the image is inverted.
  • Determine orientation – The image is summed along each axis. Pixel percentile values of each axis sum are sampled. The axis with a greater difference in percentile values is chosen as the orientation (The picket axis, it is argued, will have more pixel value variation than the axis parallel to leaf motion.)
  • Adjust for EPID sag – If a nonzero value is passed for the sag adjustment, the image is shifted along the axis of the pickets; i.e. a +1 mm adjustment for an Up-Down picket image will move expected MLC positions up 1 mm.

Analysis

  • Find the pickets – The mean profile of the image perpendicular to the MLC travel direction is taken. Major peaks are assumed to be pickets.
  • Find FWHM at each MLC position – For each picket, a sample of the image in the MLC travel direction is taken at each MLC position. The center of the FWHM of the picket for that MLC position is recorded.
  • Fit the picket to the positions & calculate error – Once all the MLC positions are determined, the positions from each peak of a picket are fitted to a 1D polynomial which is considered the ideal picket. Differences of each MLC position to the picket polynomial fit at that position are determined, which is the error. When plotted, errors are tested against the tolerance and action tolerance as appropriate.

Troubleshooting

First, check the general Troubleshooting section. Specific to the picket fence analysis, there are a few things you can do.

  • Ensure the HDMLC status - If your image is from an HD MLC, you need to set the hdmlc parameter in analyze() to True, and vic versa.

  • Apply a filter upon load - While pylinac tries to correct for unreasonable noise in the image before analysis, there may still be noise that causes analysis to fail. A way to check this is by applying a median filter upon loading the image:

    pf = PicketFence('mypf.dcm', filter=5)  # vary the filter size depending on the image
    

    Then try performing the analysis.

  • Check for streak artifacts - It is possible in certain scenarios (e.g. TrueBeam dosimetry mode) to have noteworthy artifacts in the image like so:

    _images/pf_with_streak.png

    If the artifacts are in the same direction as the pickets then it is possible pylinac is tripping on these artifacts. You can reacquire the image in another mode or simply try again in the same mode. You may also try cropping the image to exclude the artifact:

    pf = PicketFence('mypf.dcm')
    pf.image.array = mypf.image.array[200:400, 150:450]  # or whatever values you want
    
  • Set the number of pickets - If pylinac is catching too many pickets you can set the number of pickets to find with analyze().

  • Set the image inversion - If you get an error like this: ValueError: max() arg is an empty sequence, one issue may be that the image has the wrong inversion (negative values are positive, etc). Set the analyze flag invert to True to invert the image from the automatic detection.

  • Crop the image - For Elekta images, the 0th column is often an extreme value. For any Elekta image, it is suggested to crop the image. You can crop the image like so:

    pf = PicketFence(r'my/pf.dcm')
    pf.image.crop(pixels=3)
    pf.analyze()
    ...
    

API Documentation

class pylinac.picketfence.PicketFence(filename: str, filter: int = None, log: str = None, use_filename: bool = False)[source]

Bases: object

A class used for analyzing EPID images where radiation strips have been formed by the MLCs. The strips are assumed to be parallel to one another and normal to the image edge; i.e. a “left-right” or “up-down” orientation is assumed. Further work could follow up by accounting for any angle.

pickets
Type:PicketHandler
image
Type:DicomImage

Examples

Run the demo::
>>> PicketFence.run_demo()
Load the demo image:
>>> pf = PicketFence.from_demo_image()
Load an image along with its machine log:
>>> pf_w_log = PicketFence('my/pf.dcm', log='my/log.bin')
Typical session:
>>> img_path = r"C:/QA/June/PF.dcm"  # the EPID image
>>> mypf = PicketFence(img_path)
>>> mypf.analyze(tolerance=0.5, action_tolerance=0.3)
>>> print(mypf.results())
>>> mypf.plot_analyzed_image()
Parameters:
  • filename (str, None) – Name of the file as a string. If None, image must be loaded later.
  • filter (int, None) – If None (default), no filtering will be done to the image. If an int, will perform median filtering over image of size filter.
  • log (str) – Path to a log file corresponding to the delivery. The expected fluence of the log file is used to construct the pickets. MLC peaks are then compared to an absolute reference instead of a fitted picket.
  • use_filename (bool) – If False (default), no action will be performed. If True, the filename will be searched for keywords that describe the gantry and/or collimator angle. For example, if set to True and the file name was “PF_gantry45.dcm” the gantry would be interpreted as being at 45 degrees.
classmethod from_url(url: str, filter: int = None)[source]

Instantiate from a URL.

classmethod from_demo_image(filter: int = None)[source]

Construct a PicketFence instance using the demo image.

classmethod from_multiple_images(path_list: collections.abc.Sequence, dtype=<class 'numpy.uint16'>, **kwargs)[source]

Load and superimpose multiple images and instantiate a Starshot object.

Parameters:
  • path_list (iterable) – An iterable of path locations to the files to be loaded/combined.
  • kwargs – Passed to load_multiples().
passed

Boolean specifying if all MLC positions were within tolerance.

percent_passing

Return the percentage of MLC positions under tolerance.

max_error

Return the maximum error found.

max_error_picket

Return the picket number where the maximum error occurred.

max_error_leaf

Return the leaf that had the maximum error.

abs_median_error

Return the median error found.

num_pickets

Return the number of pickets determined.

static run_demo(tolerance: float = 0.5, action_tolerance: float = None)[source]

Run the Picket Fence demo using the demo image. See analyze() for parameter info.

analyze(tolerance: float = 0.5, action_tolerance: Optional[float] = None, hdmlc: bool = False, num_pickets: Optional[int] = None, sag_adjustment: Union[float, int] = 0, orientation: Optional[str] = None, invert: bool = False)[source]

Analyze the picket fence image.

Parameters:
  • tolerance (int, float) – The tolerance of difference in mm between an MLC pair position and the picket fit line.
  • action_tolerance (int, float, None) – If None (default), no action tolerance is set or compared to. If an int or float, the MLC pair measurement is also compared to this tolerance. Must be lower than tolerance. This value is usually meant to indicate that a physicist should take an “action” to reduce the error, but should not stop treatment.
  • hdmlc (bool) – If False (default), a standard (5/10mm leaves) Millennium MLC model is assumed. If True, an HD (2.5/5mm leaves) Millennium is assumed.
  • num_pickets (int, None) –

    New in version 0.8.

    The number of pickets in the image. A helper parameter to limit the total number of pickets, only needed if analysis is catching more pickets than there really are.

  • sag_adjustment (float, int) –

    New in version 0.8.

    The amount of shift in mm to apply to the image to correct for EPID sag. For Up-Down picket images, positive moves the image down, negative up. For Left-Right picket images, positive moves the image left, negative right.

  • orientation (None, str) –

    If None (default), the orientation is automatically determined. If for some reason the determined orientation is not correct, you can pass it directly using this parameter. If passed a string with ‘u’ (e.g. ‘up-down’, ‘u-d’, ‘up’) it will set the orientation of the pickets as going up-down. If passed a string with ‘l’ (e.g. ‘left-right’, ‘lr’, ‘left’) it will set it as going left-right.

  • invert (bool) –

    If False (default), the inversion of the image is automatically detected and used. If True, the image inversion is reversed from the automatic detection. This is useful when runtime errors are encountered.

plot_analyzed_image(guard_rails: bool = True, mlc_peaks: bool = True, overlay: bool = True, leaf_error_subplot: bool = True, show: bool = True)[source]

Plot the analyzed image.

Parameters:
  • guard_rails (bool) – Do/don’t plot the picket “guard rails” around the ideal picket
  • mlc_peaks (bool) – Do/don’t plot the MLC positions.
  • overlay (bool) – Do/don’t plot the alpha overlay of the leaf status.
  • leaf_error_subplot (bool) –

    New in version 1.0.

    If True, plots a linked leaf error subplot adjacent to the PF image plotting the average and standard deviation of leaf error.

save_analyzed_image(filename: str, guard_rails: bool = True, mlc_peaks: bool = True, overlay: bool = True, leaf_error_subplot: bool = False, **kwargs)[source]

Save the analyzed figure to a file. See plot_analyzed_image() for further parameter info.

results() → str[source]

Return results of analysis. Use with print().

publish_pdf(filename: str, notes: str = None, open_file: bool = False, metadata: dict = 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 ————–
orientation

The orientation of the image, either Up-Down or Left-Right.

Supporting Data Structures

class pylinac.picketfence.PicketManager(image: pylinac.picketfence.PFDicomImage, settings: pylinac.picketfence.Settings, num_pickets: Optional[int])[source]

Bases: object

Finds and handles the pickets of the image.

error_hist() → Tuple[List, ...][source]

Returns several lists of information about the MLC measurements. For use with plotting.

find_pickets()[source]

Find the pickets of the image.

passed

Whether all the pickets passed tolerance.

image_mlc_inplane_mean_profile

A profile of the image along the MLC travel direction.

mean_spacing

The average distance between pickets in mm.

class pylinac.picketfence.Settings(orientation, tolerance, action_tolerance, hdmlc, image, log_fits)[source]

Bases: object

Simple class to hold various settings and info for PF analysis/plotting.

figure_size

The size of the figure to draw; depends on the picket orientation.

small_leaf_width

The width of a “small” leaf in pixels.

large_leaf_width

The width of a “large” leaf in pixels.

number_small_leaves

The number of small leaves; depends on HDMLC status.

number_large_leaves

The number of large leaves; depends on HDMLC status.

leaf_centers

Return a set of leaf centers perpendicular to the leaf motion based on the position of the CAX.

class pylinac.picketfence.Picket(image: pylinac.picketfence.PFDicomImage, settings: pylinac.picketfence.Settings, approximate_idx: int, spacing)[source]

Bases: object

Holds picket information in a Picket Fence test.

mlc_meas

Holds MLCMeas objects.

Type:list
find_mlc_peak(mlc_center: int)[source]

Determine the center of the picket.

add_mlc_meas(mlc_center: float, mlc_position: float)[source]

Add an MLC measurement point.

sample_width

The width to sample the MLC leaf (~40% of the leaf width).

picket_array

A slice of the whole image that contains the area around the picket.

abs_median_error

The absolute median error of the MLC measurements.

max_error

The max error of the MLC measurements.

error_array

An array containing the error values of all the measurements.

passed

Whether or not all the measurements passed.

mlc_passed(mlc) → bool[source]

Return whether a specific MLC has passed tolerance.

mlc_passed_action(mlc) → bool[source]

Return whether a specific MLC has passed the action tolerance.

fit

The fit of a polynomial to the MLC measurements.

dist2cax

The distance from the CAX to the picket, in mm.

left_guard

The line representing the left side guard rail.

right_guard

The line representing the right side guard rail.

add_guards_to_axes(axis: matplotlib.axes._axes.Axes, color: str = 'g')[source]

Plot guard rails to the axis.

class pylinac.picketfence.MLCMeas(point1, point2, settings)[source]

Bases: pylinac.core.geometry.Line

Represents an MLC measurement.

plot2axes(axes: matplotlib.axes._axes.Axes, width: Union[int, float] = 1)[source]

Plot the measurement to the axes.

bg_color

The color of the measurement when the PF image is plotted, based on pass/fail status.

error

The error (difference) of the MLC measurement and the picket fit.

passed

Whether the MLC measurement was under tolerance.

passed_action

Whether the MLC measurement was under the action level tolerance.

leaf_pair

The leaf pair that formed the MLC measurement.

Returns:tuple
Return type:2 elements which are the two leaf numbers
class pylinac.picketfence.Overlay(image, settings, pickets)[source]

Bases: object

Class for handling the “overlay” feature of the plot.

add_to_axes(axes: matplotlib.axes._axes.Axes)[source]

Add the overlay to the axes.