# Picket Fence¶

## 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 any MLC type - Both default MLCs and custom MLCs can be used.
• 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


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()


You can also change the MLC type:

pf = PicketFence(pf_img, mlc="HD")


In this case, we’ve set the MLCs to be HD Millennium. For more options and to customize the MLC configuration, see Customizing MLCs.

• 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:

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:

## Customizing MLCs¶

As of v2.5, MLC configuration is set a priori (vs empirical determination as before) and the user can also create custom MLC types. Pylinac was only able to handle Millennium and HD Millennium previously.

### Preset configurations¶

Use a specific preset config:

from pylinac.picketfence import PicketFence, MLCs

pf = PicketFence(pf_img, mlc=MLC.Millennium)


The built-in presets can be seen in attrs of the MLC class.

### Creating and using a custom configuration¶

Using a custom configuration is very easy. You must create and then pass in a custom MLCArrangement. Leaf arrangements are sets of tuples with the leaf number and leaf width. An example will make this clear:

from pylinac.picketfence import PicketFence, MLCArrangement

# recreate a standard Millennium MLC with 10 leaves of 10mm width, then 40 leaves of 5mm, then 10 of 10mm again.
mlc_setup = MLCArrangement(leaf_arrangement=[(10, 10), (40, 5), (10, 10)])
# add an offset for Halcyon-style or odd-numbered leaf setups
mlc_setup_offset = MLCArrangement(leaf_arrangement=..., offset=2.5)  # offset is in mm

# pass it in to the mlc parameter
pf = PicketFence('path/to/img', mlc=mlc_setup)

# proceed as normal
pf.analyze(...)
...


## Acquiring good images¶

The following are general tips on getting good images that pylinac will analyze easily. These are in addition to the algorithm allowances and restrictions:

• Keep your pickets away from the edges. That is, in the direction parallel to leaf motion keep the pickets at least 1-2cm from the edge.
• If you use wide-gap pickets, try to make the spacing between pickets wider than the picket gaps. E.g. 1cm picket widths should use 2cm or more spacing between pickets.
• If you use Y-jaws, leave them open 1-2 leaves more than the leaves you want to measure. For example. if you’re just analyze the “central” leaves and set Y-jaws to something like +/-20cm, the leaves at the edge may not be caught by the algorithm (although see the edge_threshold parameter of analyze). To avoid having to tweak the algorithm, just open the jaws a bit more.
• Don’t put anything else in the beam path. This might sound obvious, but I’m continually surprised at the types of images people try to use/take. No, pylinac cannot account for the MV phantom you left on the couch when you took your PF image.
• Keep the leaves parallel to an edge. I.e. as close to 0, 90, 270 as possible.

## Tips & Tricks¶

### Use results_data¶

Using the picketfence module in your own scripts? While the analysis results can be printed out, if you intend on using them elsewhere (e.g. in an API), they can be accessed the easiest by using the analyze() method which returns a PFResult instance.

Note

While the pylinac tooling may change under the hood, this object should remain largely the same and/or expand. Thus, using this is more stable than accessing attrs directly.

Continuing from above:

data = pf.results_data()
data.max_error_mm
data.tolerance_mm
# and more

# return as a dict
data_dict = pf.results_data(as_dict=True)
data_dict['max_error_mm']
...


### EPID sag¶

For older linacs, 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')


## 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).
• Any MLC can be analyzed. See Customizing MLCs
• 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.
• The delivery must be parallel or nearly-parallel (<~5°) 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.

• Crop the edges - This is far and away the most common problem. Elekta is notorious for having noisy/bad edges. Pass a larger value into the constructor:

pf = PicketFence(..., crop_mm=7)

• 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:

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¶

### Main classes¶

These are the classes a typical user may interface with.

class pylinac.picketfence.PicketFence(filename: str, filter: Optional[int] = None, log: Optional[str] = None, use_filename: bool = False, mlc: Union[pylinac.picketfence.MLC, pylinac.picketfence.MLCArrangement, str] = <MLC.MILLENNIUM: {'name': 'Millennium', 'arrangement': <pylinac.picketfence.MLCArrangement object>}>, crop_mm: int = 3)[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.

Parameters: filename – Name of the file as a string. If None, image must be loaded later. filter – If None (default), no filtering will be done to the image. If an int, will perform median filtering over image of size filter. log – 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 – 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. mlc – The MLC model of the image. Must be an option from the enum MLCs or an MLCArrangement. crop_mm – The number of mm to crop from all edges. Elekta is infamous for having columns of dead pixels on the side of their images. These need to be cleaned up first. For Varian images, this really shouldn’t make a difference unless the pickets are very close to the edge. Generally speaking, they shouldn’t be for the best accuracy.
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.

mean_picket_spacing

The average distance between pickets in mm.

static run_demo(tolerance: float = 0.5, action_tolerance: float = None) → 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, num_pickets: Optional[int] = None, sag_adjustment: Union[float, int] = 0, orientation: Union[pylinac.picketfence.Orientation, str, None] = None, invert: bool = False, leaf_analysis_width_ratio: float = 0.4, picket_spacing: Optional[float] = None, height_threshold: float = 0.5, edge_threshold: float = 1.5, peak_sort: str = 'peak_heights', required_prominence: float = 0.2, fwxm: float = 50) → None[source]

Analyze the picket fence image.

Parameters: tolerance – The tolerance of difference in mm between an MLC pair position and the picket fit line. action_tolerance – 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. num_pickets – 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 – 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 – 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 – 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. leaf_analysis_width_ratio – The ratio of the leaf width to use as part of the evaluation. E.g. if the ratio is 0.5, the center half of the leaf will be used. This helps avoid tongue and groove influence. picket_spacing – If None (default), the spacing between pickets is determined automatically. If given, it should be an int or float specifying the number of PIXELS apart the pickets are. height_threshold – The threshold that the MLC peak needs to be above to be considered a picket (vs background). Lower if not all leaves are being caught. Note that for FFF beams this would very likely need to be lowered. edge_threshold – The threshold of pixel value standard deviation within the analysis window of the MLC leaf to be considered a full leaf. This is how pylinac removes MLCs that are eclipsed by the jaw. This also is how to omit or catch leaves at the edge of the field. Raise to catch more edge leaves. peak_sort – Either ‘peak_heights’ or ‘prominences’. This is the method for determining the peaks. Usually not needed unless the wrong number of pickets have been detected. See the scipy.signal.find_peaks function for more information. required_prominence – The required height of the picket (not individual MLCs) to be considered a peak. Pylinac takes a mean of the image axis perpendicular to the leaf motion to get an initial guess of the peak locations and also to determine picket spacing. Changing this can be useful for wide-gap tests where the shape of the beam horns can form two or more local maximums in the picket area. Increase if for wide-gap images that are catching too many pickets. Consider lowering for FFF beams if there are analysis issues. Warning We do not recommend performing FFF wide-gap PF tests. Make your FFF pickets narrow or measure with a flat beam instead. fwxm – For each MLC kiss, the profile is a curve from low to high to low. The FWXM (0-100) is the height to use to measure to determine the center of the curve, which is the surrogate for MLC kiss position. I.e. for each MLC kiss, what height of the picket should you use to actually determine the center location? It is unusual to change this. If you have something in the way (we’ve seen crazy examples with a BB in the way) you may want to increase this.
plot_analyzed_image()[source]

Plot the analyzed image.

Parameters: guard_rails – Do/don’t plot the picket “guard rails” around the ideal picket mlc_peaks – Do/don’t plot the detected MLC peak positions. overlay – Do/don’t plot the alpha overlay of the leaf status. leaf_error_subplot – If True, plots a linked leaf error subplot adjacent to the PF image plotting the average and standard deviation of leaf error. show – Whether to display the plot. Set to false for saving to a figure, etc. figure_size – Either ‘auto’ or a tuple. If auto, the figure size is set depending on the orientation. If a tuple, this is the figure size to use.
save_analyzed_image(filename: Union[str, _io.BytesIO], guard_rails: bool = True, mlc_peaks: bool = True, overlay: bool = True, leaf_error_subplot: bool = False, **kwargs) → None[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().

results_data(as_dict=False) → Union[pylinac.picketfence.PFResult, dict][source]

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

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

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

class pylinac.picketfence.MLCArrangement(leaf_arrangement: List[Tuple[int, float]], offset: float = 0)[source]

Bases: object

Construct an MLC array

Parameters: leaf_arrangement (Description of the leaf arrangement. List of tuples containing the number of leaves and leaf width.) – E.g. (10, 5) is 10 leaves with 5mm widths. offset (The offset in mm of the leaves. Used for asymmetric arrangements. E.g. -2.5mm will shift the arrangement 2.5mm to the left.) –
class pylinac.picketfence.Orientation[source]

Bases: enum.Enum

Possible orientations of the image

UP_DOWN = 'Up-Down'
LEFT_RIGHT = 'Left-Right'
class pylinac.picketfence.MLC[source]

Bases: enum.Enum

The pre-built MLC types

MILLENNIUM = {'arrangement': <pylinac.picketfence.MLCArrangement object>, 'name': 'Millennium'}
HD_MILLENNIUM = {'arrangement': <pylinac.picketfence.MLCArrangement object>, 'name': 'HD Millennium'}
HALCYON_DISTAL = {'arrangement': <pylinac.picketfence.MLCArrangement object>, 'name': 'Halcyon distal'}
HALCYON_PROXIMAL = {'arrangement': <pylinac.picketfence.MLCArrangement object>, 'name': 'Halcyon proximal'}
class pylinac.picketfence.PFResult(tolerance_mm: float, action_tolerance_mm: float, percent_leaves_passing: float, number_of_pickets: int, absolute_median_error_mm: float, max_error_mm: float, mean_picket_spacing_mm: float, offsets_from_cax_mm: List[float], passed: bool)[source]

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.

tolerance_mm = None
action_tolerance_mm = None
percent_leaves_passing = None
number_of_pickets = None
absolute_median_error_mm = None
max_error_mm = None
mean_picket_spacing_mm = None
offsets_from_cax_mm = None
passed = None

### Supporting Classes¶

You generally won’t have to interface with these unless you’re doing advanced behavior.

class pylinac.picketfence.PFDicomImage(path: str, **kwargs)[source]

A subclass of a DICOM image that checks for noise and inversion when instantiated. Can also adjust for EPID sag.

adjust_for_sag(sag: int, orientation: Union[str, pylinac.picketfence.Orientation]) → None[source]

Roll the image to adjust for EPID sag.

class pylinac.picketfence.Picket(mlc_measurements: List[pylinac.picketfence.MLCValue], log_fits, orientation: pylinac.picketfence.Orientation, image: pylinac.picketfence.PFDicomImage, tolerance: float)[source]

Bases: object

Holds picket information in a Picket Fence test.

get_fit() → numpy.poly1d[source]

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') → None[source]

Plot guard rails to the axis.

class pylinac.picketfence.MLCValue(picket_num, approx_idx, leaf_width, leaf_center, picket_spacing, orientation, leaf_analysis_width_ratio, tolerance, action_tolerance, leaf_num, approx_peak_val, image_window, image, fwxm)[source]

Bases: object

Representation of an MLC kiss.

plot2axes(axes: matplotlib.axes._axes.Axes, width: Union[float, int] = 1) → None[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.