Winston-Lutz¶
Overview¶
The Winston-Lutz module loads and processes EPID images that have acquired Winston-Lutz type images.
Features:
Couch shift instructions - After running a WL test, get immediate feedback on how to shift the couch. Couch values can also be passed in and the new couch values will be presented so you don’t have to do that pesky conversion. “Do I subtract that number or add it?”
Automatic field & BB positioning - When an image or directory is loaded, the field CAX and the BB are automatically found, along with the vector and scalar distance between them.
Isocenter size determination - Using backprojections of the EPID images, the 3D gantry isocenter size and position can be determined independent of the BB position. Additionally, the 2D planar isocenter size of the collimator and couch can also be determined.
Image plotting - WL images can be plotted separately or together, each of which shows the field CAX, BB and scalar distance from BB to CAX.
Axis deviation plots - Plot the variation of the gantry, collimator, couch, and EPID in each plane as well as RMS variation.
File name interpretation - Rename DICOM filenames to include axis information for linacs that don’t include such information in the DICOM tags. E.g. “myWL_gantry45_coll0_couch315.dcm”.
Running the Demo¶
To run the Winston-Lutz demo, create a script or start an interpreter session and input:
from pylinac import WinstonLutz
WinstonLutz.run_demo()
Results will be printed to the console and a figure showing the zoomed-in images will be generated:
Winston-Lutz Analysis
=================================
Number of images: 17
Maximum 2D CAX->BB distance: 1.23mm
Median 2D CAX->BB distance: 0.69mm
Shift to iso: facing gantry, move BB: RIGHT 0.36mm; OUT 0.36mm; DOWN 0.20mm
Gantry 3D isocenter diameter: 1.05mm (9/17 images considered)
Maximum Gantry RMS deviation (mm): 1.03mm
Maximum EPID RMS deviation (mm): 1.31mm
Gantry+Collimator 3D isocenter diameter: 1.11mm (13/17 images considered)
Collimator 2D isocenter diameter: 1.09mm (7/17 images considered)
Maximum Collimator RMS deviation (mm): 0.79
Couch 2D isocenter diameter: 2.32mm (7/17 images considered)
Maximum Couch RMS deviation (mm): 1.23
(Source code
, png
, hires.png
, pdf
)
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, a few simple tips should be followed. Note that these are not unique to pylinac; most Winston-Lutz analyses require these steps:
The BB should be fully within the field of view.
The MLC field should be symmetric.
The BB should be <2cm from the isocenter.
Axis Values¶
Pylinac uses the Image types & output definitions definition to bin images. Regardless of the axis values, pylinac will calculate some values like max/median BB->CAX distance. Other values such as gantry iso size will only use Reference and Gantry image types as defined in the linked section. We recommend reviewing the analysis definitions and acquiring images according to the values you are interested in. Some examples are below. Note that these are not mutually exclusive:
Simple max distance to BB: Any axis values; any combination of axis values are allowed.
Gantry iso size: Gantry value can be any; all other axes must be 0.
Collimator iso size: Collimator value can be any; all other axes must be 0.
If, e.g., all axis values are combinations of axes then gantry iso size will not be calculated. Further, the plot_analyzed_image
method assumes Gantry, Collimator, and/or Couch image sets. If only combinations are passed, this image will be empty.
A good practice is also to acquire a reference image if possible, meaning all axes at 0.
Coordinate Space¶
Note
In pylinac 2.3, the coordinates changed to be compliant with IEC 61217. Compared to previous versions, the Y and Z axis have been swapped. The new Z axis has also flipped which way is positive.
When interpreting results from a Winston-Lutz test, it’s important to know the coordinates, origin, etc. Pylinac uses IEC 61217 coordinate space. Colloquial descriptions are as if standing at the foot of the couch looking at the gantry.
X-axis - Lateral, or left-right, with right being positive.
Y-axis - Superior-Inferior, or in-out, with sup/in being positive.
Z-axis - Anterior-Posterior, or up-down, with up/anterior being positive.
Passing a coordinate system¶
Added in version 3.6.
It is possible to pass in your machine’s coordinate scale/system to the analyze parameter like so:
from pylinac.winston_lutz import WinstonLutz, MachineScale
wl = WinstonLutz(...)
wl.analyze(..., machine_scale=MachineScale.VARIAN_IEC)
...
This will change the BB shift vector and shift instructions accordingly. If you don’t use the shift vector or instructions then you won’t need to worry about this parameter.
Typical Use¶
Analyzing a Winston-Lutz test is simple. First, let’s import the class:
from pylinac import WinstonLutz
From here, you can load a directory:
my_directory = "path/to/wl_images"
wl = WinstonLutz(my_directory)
You can also load a ZIP archive with the images in it:
wl = WinstonLutz.from_zip("path/to/wl.zip")
Now, analyze it:
wl.analyze(bb_size_mm=5)
And that’s it! You can now view images, print the results, or publish a PDF report:
# plot all the images
wl.plot_images()
# plot an individual image
wl.images[3].plot()
# save a figure of the image plots
wl.save_plots("wltest.png")
# print to PDF
wl.publish_pdf("mywl.pdf")
If you want to shift the BB based on the results and perform the test again there is a method for that:
print(wl.bb_shift_instructions())
# LEFT: 0.1mm, DOWN: 0.22mm, ...
You can also pass in your couch coordinates and the new values will be generated:
print(wl.bb_shift_instructions(couch_vrt=0.41, couch_lng=96.23, couch_lat=0.12))
# New couch coordinates (mm): VRT: 0.32; LNG: 96.11; LAT: 0.11
Visualizing the Isocenter-to-BB¶
Added in version 3.17.
The isocenter and BB can be visualized together after analysis by calling plot_location
:
wl = WinstonLutz(...)
wl.analyze(...)
wl.plot_location()
This will result in a 3D plot visualizing the BB (true physical size) and the isocenter (true physical size) in the room coordinates like so:
Accessing data¶
Changed in version 3.0.
Using the WL 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 results_data()
method
which returns a WinstonLutzResult
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 = wl.results_data()
data.num_total_images
data.max_2d_cax_to_bb_mm
# and more
# return as a dict
data_dict = wl.results_data(as_dict=True)
data_dict["num_total_images"]
...
Accessing individual images¶
Each image can be plotted and otherwise accessed easily:
wl = WinstonLutz(...)
# access first image
wl.images[
0
] # these are subclasses of the pylinac.core.image.DicomImage class, with a few special props
# plot 3rd image
wl.images[
0
].plot() # the plot method is special to the WL module and shows the BB, EPID, and Field CAX.
# get 2D x/y vector of an image
wl.images[
4
].cax2bb_vector # this is a Vector with a .x and .y attribute. Note that x and y are in respect to the image, not the fixed room coordinates.
Analyzing a single image¶
You may optionally analyze a single image if that is your preference. Obviously, no 3D computations are performed.
Note
This is the same class used under the hood for the WinstonLutz
images, so any attribute you currently use with something
like wl.images[2].cax2bb_vector
will work for the below with a direct call: wl2d.cax2bb_vector
.
from pylinac import WinstonLutz2D
wl2d = WinstonLutz2D("my/path/...")
wl2d.analyze(bb_size_mm=4) # same as WinstonLutz class
wl2d.plot()
...
This class does not have all the methods that WinstonLutz
has for mostly obvious reasons and lower likelihood of being used directly.
Passing in Axis values¶
If your linac EPID images do not include axis information (such as Elekta) there are two ways to pass the data in. First, you can specify it in the file name. Any and all of the three axes can be defined. If one is not defined and is not in the DICOM tags, it will default to 0. The syntax to define the axes: “<*>gantry0<*>coll0<*>couch0<*>”. There can be any text before, after, or in between each axis definition. However, the axes numerical value must immediately follow the axis name. Axis names are also fixed. The following examples are valid:
MyWL-gantry0-coll90-couch315.dcm
gantry90_stuff_coll45-couch0.dcm
abc-couch45-gantry315-coll0.dcm
01-gantry0-abcd-coll30couch10abc.dcm
abc-gantry30.dcm
coll45abc.dcm
The following are invalid:
mywl-gantry=0-coll=90-couch=315.dcm
gan45_collimator30-table270.dcm
Using the filenames within the code is done by passing the use_filenames=True
flag to the init method:
my_directory = "path/to/wl_images"
wl = WinstonLutz(my_directory, use_filenames=True)
Note
If using filenames any relevant axes must be defined, otherwise they will default to zero. For example, if the acquisition was at gantry=45, coll=15, couch=0 then the filename must include both the gantry and collimator in the name (<…gantry45…coll15….dcm>). For this example, the couch need not be defined since it is 0.
The other way of inputting axis information is passing the axis_mapping
parameter to the constructor. This is a
dictionary with the filenames as keys and a tuple of ints for the gantry, coll, and couch:
directory = "path/to/wl/dir"
mapping = {
"file1.dcm": (0, 0, 0),
"file2.dcm": (90, 315, 45),
} # add more as needed
wl = WinstonLutz(directory=directory, axis_mapping=mapping)
# analyze as normal
wl.analyze(...)
Note
The filenames should be local to the directory. In the above example the full paths would be path/to/wl/dir/file1.dcm
, and path/to/wl/dir/file2.dcm
.
Changing BB detection size¶
To change the size of BB pylinac is expecting you can pass the size to the analyze method:
import pylinac
wl = WinstonLutz(...)
wl.analyze(bb_size_mm=3)
...
Low-density BBs¶
If using a phantom with a BB that has a lower density that than the surrounding material, pass the low_density_bb
parameter:
import pylinac
wl = WinstonLutz(...)
wl.analyze(..., low_density_bb=True)
...
kV Analysis/Imaging-only iso evaluation¶
It is possible to analyze kV WL images and/or analyze a WL set and only focus on the imaging iso.
In this case there are two parameters you likely need to adjust:
open_field
and low_density_bb
. The first will set the field center to the image center. It is assumed
the field is not of interest or the field cannot be measured, such as a fully-open kV image. Use this anytime
the radiation iso is not of interest. For large-field WL images, you may need to set the low_density_bb
parameter to True. This is because the automatic inversion of the WL module assumes a small field is being delivered.
For large field deliveries, kV or MV, see about flipping this parameter if the analysis fails.
CBCT Analysis¶
Added in version 3.16.
Warning
This feature is still experimental. Use with caution.
It’s possible to take and load a CBCT dataset of a BB using the from_cbct
and from_cbct_zip
class methods.
The CBCT dataset is
loaded as a 3D numpy array. Projections at the 4 faces
of the array (top, left, bottom, right) are created into pseudo-cardinal angle DICOMs. These DICOMs are then
loaded as normal images and analyzed.
wl = WinstonLutz.from_cbct("my/cbct/dir")
# OR
wl = WinstonLutz.from_cbct_zip("my/cbct.zip")
# ensure to set low density and open field to True
wl.analyze(low_density_bb=True, open_field=True, bb_size_mm=3)
# use as normal
print(wl.results())
print(wl.results_data())
print(wl.bb_shift_instructions())
wl.plot_images()
Warning
The CBCT analysis comes with a few caveats:
Analyzing the image will override the
low_density_bb
andopen_field
flags to always be True; it does not matter what is passed inanalyze
.No axis deviation information is available, i.e. couch/coll/gantry walkout.
There are always 4 images generated.
The generated images are not true DICOMs and thus do not have all the DICOM tags.
Using TIFF images¶
Added in version 3.12.
The WL module can handle TIFF images on a provisional basis.
Warning
This is still experimental and caution is warranted. Even though there is an automatic noise/edge cleaner, cropping images to remove markers and/or film scan artifacts is encouraged.
To load TIFF images, extra parameters must be passed. Specifically,
the sid
and potentially the dpi
parameters must be added. Additionally,
axis_mapping
must be populated. This is how pylinac can convert
the images into rudimentary dicom images. The dpi
parameter is only needed if the TIFF images do not have a resolution tag.
Pylinac will give a specific error if dpi
wasn’t passed and also wasn’t
in the TIFF tags.
Note
Although it is technically possible to load both DICOM and TIFF together in one dataset it is not encouraged.
from pylinac import WinstonLutz
my_tiff_images = list(Path(...), Path(...))
wl_tiff = WinstonLutz(
my_tiff_images,
sid=1000,
dpi=212,
axis_mapping={"g0.tiff": (0, 0, 0), "g270.tiff": (270, 0, 0)},
)
# now analyze as normal
wl_tiff.analyze(...)
print(wl_tiff.results())
Note that other .from...
methods are available such as .from_zip
:
from pylinac import WinstonLutz
my_tiff_zip = "../files/tiffs.zip"
# same inputs as above
wl_tiff = WinstonLutz.from_zip(my_tiff_zip, dpi=...)
Image types & output definitions¶
The following terms are used in pylinac’s WL module and are worth defining.
Image axis definitions/Image types Images are classified into 1 of 6 image types, depending on the position of the axes. The image type is then used for determining whether to use the image for the given calculation. Image types allow the module to isolate the analysis to a given axis if needed. E.g. for gantry iso size, as opposed to overall iso size, only the gantry should be moving so that no other variables influence it’s calculation.
Reference: This is when all axes are at value 0 (gantry=coll=couch=0).
Gantry: This is when all axes but gantry are at value 0, e.g. gantry=45, coll=0, couch=0.
Collimator: This is when all axes but collimator are at value 0.
Couch: This is when all axes but the couch are at value 0.
GB Combo: This is when either the gantry or collimator are non-zero but the couch is 0.
GBP Combo: This is where the couch is kicked and the gantry and/or collimator are rotated.
Analysis definitions Given the above terms, the following calculations are performed.
Maximum 2D CAX->BB distance (scalar, mm): Analyzes all images individually for the maximum 2D distance from rad field center to the BB.
Median 2D CAX->BB distance (scalar, mm): Same as above but the median.
Shift of BB to isocenter (vector, mm): The instructions of how to move the BB/couch in order to place the BB at the determined isocenter.
Gantry 3D isocenter diameter (scalar, mm): Analyzes only the gantry axis images (see above image types). Applies backprojection of the CAX in 3D and then minimizes a sphere that touches all the 3D backprojection lines.
Gantry+Collimator 3D isocenter diameter (scalar, mm): Same as above but also considers Collimator and GB Combo images.
[Couch, Collimator] 2D isocenter diameter (scalar, mm): Analyzes only the collimator or couch images to determine the size of the isocenter according to the axis in question. The maximum distance between any of the points is the isocenter size. The couch and collimator are treated separately for obvious reasons. If no images are given that rotate about the axis in question (e.g. cardinal gantry angles only) the isocenter size will default to 0.
[Maximum, All][Gantry, Collimator, Couch, GB Combo, GBP Combo, EPID] RMS deviation (array of scalars, mm): Analyzes the images for the axis in question to determine the overall RMS inclusive of all 3 coordinate axes (vert, long, lat). I.e. this is the overall displacement as a function of the axis in question. For EPID, the displacement is calculated as the distance from image center to BB for all images with couch=0. If no images are given that rotate about the axis in question (e.g. cardinal gantry angles only) the isocenter size will default to 0.
Algorithm¶
The Winston-Lutz algorithm is based on the works of Winkler et al, Du et al, and Low et al. Winkler found that the collimator and couch iso could be found using a minimum optimization of the field CAX points. They also found that the gantry isocenter could by found by “backprojecting” the field CAX as a line in 3D coordinate space, with the BB being the reference point. This method is used to find the gantry isocenter size.
Low determined the geometric transformations to apply to 2D planar images to calculate the shift to apply to the BB. This method is used to determine the shift instructions. Specifically, equations 6 and 9.
Note
If doing research, it is very important to note that Low implicitly used the “Varian” coordinate system.
This is an old coordinate system and any new Varian linac actually uses IEC 61217. However, because the
gantry and couch definitions are different, the matrix definitions are technically incorrect when using
IEC 61217. By default, Pylinac assumes the images are in IEC 61217 scale and will internally convert it to varian scale
to be able to use Low’s equations.
To use a different scale use the machine_scale
parameter, shown here Passing a coordinate system.
Also see Machine Scale.
The algorithm works like such:
Allowances
The images can be acquired with any EPID (aS500, aS1000, aS1200) at any SID.
The BB does not need to be near the real isocenter to determine isocenter sizes, but does affect the 2D image analysis.
Restrictions
Warning
Analysis can fail or give unreliable results if any Restriction is violated.
The BB must be fully within the field of view.
The BB must be within 2.0cm of the real isocenter.
The images must be acquired with the EPID.
The linac scale should be IEC 61217.
Analysis
Find the field CAX – 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 CAX.
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, the BB is considered found and the pixel-weighted center of mass of the BB is considered the BB location.
Note
Strictly speaking, the following aren’t required analyses, but are explained for fullness and clarity.
Backproject the CAX for gantry images – Based on the vector of the BB to the field CAX and the gantry angle, a 3D line projection of the CAX is constructed. The BB is considered at the origin. Only images where the couch was at 0 are used for CAX projection lines.
Determine gantry isocenter size - Using the backprojection lines, an optimization function is run to minimize the maximum distance to any line. The optimized distance is the isocenter radius.
Determine collimator isocenter size - The maximum distance between any two field CAX locations is calculated for all collimator images.
Determine couch isocenter size - Instead of using the BB as the non-moving reference point, which is now moving with the couch, the Reference image (gantry = collimator = couch = 0) CAX location is the reference. The maximum distance between any two BB points is calculated and taken as the isocenter size.
Note
Collimator iso size is always in the plane normal to the gantry, while couch iso size is always in the x-z plane.
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.
Perfect Delivery¶
Let’s deliver a set of perfect images. This should result in near-0 deviations and isocenter size. The utility function used here will produce 4 images at the 4 cardinal gantry angles with all other axes at 0, with a BB of 4mm diameter, and a field size of 4x4cm:
import pylinac
from pylinac.core.image_generator import (
GaussianFilterLayer,
FilteredFieldLayer,
AS1200Image,
RandomNoiseLayer,
generate_winstonlutz,
)
wl_dir = 'wl_dir'
generate_winstonlutz(
AS1200Image(),
FilteredFieldLayer,
dir_out=wl_dir,
final_layers=[GaussianFilterLayer(),],
bb_size_mm=4,
field_size_mm=(40, 40),
)
wl = pylinac.WinstonLutz(wl_dir)
wl.analyze(bb_size_mm=4)
wl.plot_images()
(Source code
, png
, hires.png
, pdf
)
which has an output of:
Winston-Lutz Analysis
=================================
Number of images: 4
Maximum 2D CAX->BB distance: 0.00mm
Median 2D CAX->BB distance: 0.00mm
Shift to iso: facing gantry, move BB: RIGHT 0.00mm; IN 0.00mm; UP 0.00mm
Gantry 3D isocenter diameter: 0.00mm (4/4 images considered)
Maximum Gantry RMS deviation (mm): 0.00mm
Maximum EPID RMS deviation (mm): 0.00mm
Gantry+Collimator 3D isocenter diameter: 0.00mm (4/4 images considered)
Collimator 2D isocenter diameter: 0.00mm (1/4 images considered)
Maximum Collimator RMS deviation (mm): 0.00
Couch 2D isocenter diameter: 0.00mm (1/4 images considered)
Maximum Couch RMS deviation (mm): 0.00
As shown, we have perfect results.
Offset BB¶
Let’s now offset the BB by 1mm to the left:
import pylinac
from pylinac.core.image_generator import (
GaussianFilterLayer,
FilteredFieldLayer,
AS1200Image,
RandomNoiseLayer,
generate_winstonlutz,
)
wl_dir = 'wl_dir'
generate_winstonlutz(
AS1200Image(),
FilteredFieldLayer,
dir_out=wl_dir,
final_layers=[GaussianFilterLayer(),],
bb_size_mm=4,
field_size_mm=(40, 40),
offset_mm_left=1,
)
wl = pylinac.WinstonLutz(wl_dir)
wl.analyze(bb_size_mm=4)
wl.plot_images()
(Source code
, png
, hires.png
, pdf
)
with an output of:
Winston-Lutz Analysis
=================================
Number of images: 4
Maximum 2D CAX->BB distance: 1.01mm
Median 2D CAX->BB distance: 0.50mm
Shift to iso: facing gantry, move BB: RIGHT 1.01mm; IN 0.00mm; UP 0.00mm
Gantry 3D isocenter diameter: 0.00mm (4/4 images considered)
Maximum Gantry RMS deviation (mm): 1.01mm
Maximum EPID RMS deviation (mm): 0.00mm
Gantry+Collimator 3D isocenter diameter: 0.00mm (4/4 images considered)
Collimator 2D isocenter diameter: 0.00mm (1/4 images considered)
Maximum Collimator RMS deviation (mm): 0.00
Couch 2D isocenter diameter: 0.00mm (1/4 images considered)
Maximum Couch RMS deviation (mm): 0.00
We have correctly found that the max distance is 1mm and the required shift to iso is 1mm to the right (since we placed the bb to the left).
Gantry Tilt¶
We can simulate gantry tilt, where at 0 and 180 the gantry tilts forward and backward respectively. We use a realistic value of 1mm. Note that everything else is perfect:
import pylinac
from pylinac.core.image_generator import (
GaussianFilterLayer,
FilteredFieldLayer,
AS1200Image,
RandomNoiseLayer,
generate_winstonlutz,
)
wl_dir = 'wl_dir'
generate_winstonlutz(
AS1200Image(),
FilteredFieldLayer,
dir_out=wl_dir,
final_layers=[GaussianFilterLayer(),],
bb_size_mm=4,
field_size_mm=(40, 40),
gantry_tilt=1,
)
wl = pylinac.WinstonLutz(wl_dir)
wl.analyze(bb_size_mm=4)
wl.plot_images()
(Source code
, png
, hires.png
, pdf
)
with output of:
Winston-Lutz Analysis
=================================
Number of images: 4
Maximum 2D CAX->BB distance: 0.90mm
Median 2D CAX->BB distance: 0.45mm
Shift to iso: facing gantry, move BB: LEFT 0.00mm; IN 0.00mm; UP 0.00mm
Gantry 3D isocenter diameter: 1.79mm (4/4 images considered)
Maximum Gantry RMS deviation (mm): 0.90mm
Maximum EPID RMS deviation (mm): 0.90mm
Gantry+Collimator 3D isocenter diameter: 1.79mm (4/4 images considered)
Collimator 2D isocenter diameter: 0.00mm (1/4 images considered)
Maximum Collimator RMS deviation (mm): 0.00
Couch 2D isocenter diameter: 0.00mm (1/4 images considered)
Maximum Couch RMS deviation (mm): 0.00
Note that since the tilt is symmetric the shift to iso is 0 despite our non-zero median distance. I.e. we are at iso, the iso just isn’t perfect and we are thus at the best possible position.
Perfect Multi-Axis¶
We can also vary the axis data for the images produced. Below we create a typical multi-axis WL with varying gantry, collimator, and couch (cardinal values for axis of interest with all other axes at 0):
import pylinac
from pylinac.core.image_generator import (
GaussianFilterLayer,
FilteredFieldLayer,
AS1200Image,
RandomNoiseLayer,
generate_winstonlutz,
)
wl_dir = 'wl_dir'
generate_winstonlutz(
AS1200Image(),
FilteredFieldLayer,
dir_out=wl_dir,
final_layers=[GaussianFilterLayer(),],
bb_size_mm=4,
field_size_mm=(40, 40),
image_axes=[(0, 0, 0), (0, 90, 0), (0, 270, 0),
(90, 0, 0), (180, 0, 0), (270, 0, 0),
(0, 0, 90), (0, 0, 270)]
)
wl = pylinac.WinstonLutz(wl_dir)
wl.analyze(bb_size_mm=4)
wl.plot_images()
(Source code
, png
, hires.png
, pdf
)
Perfect Cone¶
We can also look at simulated cone WL images. Here we use the 17.5mm cone:
import pylinac
from pylinac.core.image_generator import (
GaussianFilterLayer,
FilteredFieldLayer,
AS1200Image,
RandomNoiseLayer,
generate_winstonlutz, generate_winstonlutz_cone, FilterFreeConeLayer,
)
wl_dir = 'wl_dir'
generate_winstonlutz_cone(
AS1200Image(),
FilterFreeConeLayer,
dir_out=wl_dir,
final_layers=[GaussianFilterLayer(),],
bb_size_mm=4,
cone_size_mm=17.5,
)
wl = pylinac.WinstonLutz(wl_dir)
wl.analyze(bb_size_mm=4)
wl.plot_images()
(Source code
, png
, hires.png
, pdf
)
Low-density BB¶
Simulate a low-density BB surrounded by higher-density material:
import pylinac
from pylinac.core.image_generator import (
GaussianFilterLayer,
FilteredFieldLayer,
AS1200Image,
RandomNoiseLayer,
generate_winstonlutz,
)
wl_dir = 'wl_dir'
generate_winstonlutz(
AS1200Image(),
FilteredFieldLayer,
dir_out=wl_dir,
final_layers=[GaussianFilterLayer(),],
bb_size_mm=4,
field_size_mm=(40, 40),
field_alpha=0.6, # set the field to not max out
bb_alpha=0.3 # normally this is negative to attenuate the beam, but here we increase the signal
)
wl = pylinac.WinstonLutz(wl_dir)
wl.analyze(bb_size_mm=4, low_density_bb=True)
wl.plot_images()
(Source code
, png
, hires.png
, pdf
)
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, axes_precision: int | None = None, dpi: float | None = None, sid: float | None = None)[source]¶
Bases:
object
Class for performing a Winston-Lutz test of the radiation isocenter.
Parameters¶
- directorystr, 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>).
- axes_precision: int | None
How many significant digits to represent the axes values. If None, no precision is set and the input/DICOM values are used raw. If set to an integer, rounds the axes values (gantry, coll, couch) to that many values. E.g. gantry=0.1234 => 0.1 with precision=1. This is mostly useful for plotting/rounding (359.9=>0) and if using the
keyed_image_details
withresults_data
.- dpi
The dots-per-inch setting. Only needed if using TIFF images and the images do not contain the resolution tag. An error will raise if dpi is not passed and the TIFF resolution cannot be determined.
- sid
The Source-to-Image distance in mm. Only needed when using TIFF images.
- machine_scale: MachineScale¶
- image_type¶
alias of
WinstonLutz2D
- images: list[WinstonLutz2D]¶
- classmethod from_demo_images(**kwargs)[source]¶
Instantiate using the demo images.
Parameters¶
- kwargs
See parameters of the __init__ method for details.
- classmethod from_zip(zfile: str | BinaryIO, **kwargs)[source]¶
Instantiate from a zip file rather than a directory.
Parameters¶
- zfile
Path to the archive file.
- kwargs
See parameters of the __init__ method for details.
- classmethod from_url(url: str, **kwargs)[source]¶
Instantiate from a URL.
Parameters¶
- urlstr
URL that points to a zip archive of the DICOM images.
- kwargs
See parameters of the __init__ method for details.
- classmethod from_cbct_zip(file: Path | str, raw_pixels: bool = False, **kwargs)[source]¶
Instantiate from a zip file containing CBCT images.
Parameters¶
- file
Path to the archive file.
- raw_pixels
If True, uses the raw pixel values of the DICOM files. If False, uses the rescaled Hounsfield units. Generally, this should be true.
- kwargs
See parameters of the __init__ method for details.
- classmethod from_cbct(directory: Path | str, raw_pixels: bool = False, **kwargs)[source]¶
Create a 4-angle WL test from a CBCT dataset.
The dataset is loaded and the array is “viewed” from top, bottom, left, and right to create the 4 angles. The dataset has to be rescaled so that the z-axis spacing is equal to the x/y axis. This is because the typical slice thickness is much larger than the in-plane resolution.
Parameters¶
- directory
The directory containing the CBCT DICOM files.
- raw_pixels
If True, uses the raw pixel values of the DICOM files. If False, uses the rescaled Hounsfield units. Generally, this should be true.
- kwargs
See parameters of the __init__ method for details.
- static run_demo() None [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: MachineScale = MachineScale.IEC61217, low_density_bb: bool = False, open_field: bool = False) None [source]¶
Analyze the WL images.
Parameters¶
- bb_size_mm
The expected diameter 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.
- open_field
If True, sets the field center to the EPID center under the assumption the field is not the focus of interest or is too wide to be calculated. This is often helpful for kV WL analysis where the blades are wide open and even then the blade edge is of less interest than simply the imaging iso vs the BB.
- property gantry_iso_size: float¶
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.
- property gantry_coll_iso_size: float¶
The diameter of the 3D gantry isocenter size in mm including collimator and gantry/coll combo images. Images where the couch!=0 are excluded.
- property collimator_iso_size: float¶
The 2D collimator isocenter size (diameter) in mm. The iso size is in the plane normal to the gantry.
- property couch_iso_size: float¶
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.
- property bb_shift_vector: 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_vrtfloat
The current couch vertical position in cm.
- couch_lngfloat
The current couch longitudinal position in cm.
- couch_latfloat
The current couch lateral position in cm.
- axis_rms_deviation(axis: Axis | tuple[Axis, ...] = Axis.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, show: bool = True, ax: plt.Axes | None = 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.
- showbool
Whether to actually show the images.
- axNone, matplotlib.Axes
The axis to plot to. If None, creates a new plot.
- plot_location(show: bool = True, viewbox_mm: float | None = None, plot_bb: bool = True, plot_isocenter_sphere: bool = True, plot_couch_iso: bool = True, plot_coll_iso: bool = True, show_legend: bool = True)[source]¶
Plot the isocenter and size as a sphere in 3D space relative to the BB. The iso is at the origin.
Only images where the couch was at zero are considered.
Parameters¶
- showbool
Whether to plot the image.
- viewbox_mmfloat
The default size of the 3D space to plot in mm in each axis.
- plot_bbbool
Whether to plot the BB location; the size is also considered.
- plot_isocenter_spherebool
Whether to plot the gantry + collimator isocenter size.
- plot_couch_isobool
Whether to plot the couch-plane-only isocenter size. This will be zero if there are no images where the couch rotated.
- plot_coll_isobool
Whether to plot the collimator-plane-only isocenter size. This is shown along the Z/Y plane only to differentiate from the couch iso visualization. The collimator plane is always normal to the gantry angle. This will be zero if there are no images where the collimator rotated.
- show_legendbool
Whether to show the legend.
- plot_images(axis: Axis = Axis.GANTRY, show: bool = True, split: bool = False, **kwargs)[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’}
The axis to plot.
- showbool
Whether to show the image.
- splitbool
Whether to show/plot the images individually or as one large figure.
- save_images(filename: str | BinaryIO, axis: Axis = Axis.GANTRY, **kwargs) None [source]¶
Save the figure of plot_images() to file. Keyword arguments are passed to matplotlib.pyplot.savefig().
Parameters¶
- filenamestr
The name of the file to save to.
- axis
The axis to save.
- save_images_to_stream(**kwargs) dict[str, BytesIO] [source]¶
Save the individual image plots to stream
- plot_summary(show: bool = True, fig_size: tuple | None = None) None [source]¶
Plot a summary figure showing the gantry sag and wobble plots of the three axes.
- results(as_list: bool = False) str [source]¶
Return the analysis results summary.
Parameters¶
- as_listbool
Whether to return as a list of strings vs single string. Pretty much for internal usage.
- results_data(as_dict: bool = 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.
- notesstr, list of strings
Text; if str, prints single line. If list of strings, each list item is printed on its own line.
- open_filebool
Whether to open the file using the default program after creation.
- metadatadict
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.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[WinstonLutz2DResult], keyed_image_details: dict[str, WinstonLutz2DResult])[source]¶
Bases:
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: 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[WinstonLutz2DResult]¶
- keyed_image_details: dict[str, WinstonLutz2DResult]¶
- class pylinac.winston_lutz.WinstonLutz2D(file: str | BinaryIO | Path, use_filenames: bool = False, **kwargs)[source]¶
Bases:
LinacDicomImage
Holds individual Winston-Lutz EPID images, image properties, and automatically finds the field CAX and BB.
Parameters¶
- filestr
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, open_field: bool = False) None [source]¶
Analyze the image. See WinstonLutz.analyze for parameter details.
- property cax_line_projection: Line¶
The projection of the field CAX through space around the area of the BB. Used for determining gantry isocenter size.
Returns¶
- Line
The virtual line in space made by the beam CAX.
- property cax2bb_distance: float¶
The scalar distance in mm from the CAX to the BB.
- property cax2epid_distance: float¶
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) plt.Axes [source]¶
Plot the image, zoomed-in on the radiation field, along with the detected BB location and field CAX location.
Parameters¶
- axNone, matplotlib Axes instance
The axis to plot to. If None, will create a new figure.
- showbool
Whether to actually show the image.
- clear_figbool
Whether to clear the figure first before drawing.
- property variable_axis: 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: 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.WinstonLutz2DResult(variable_axis: 'str', cax2epid_vector: 'Vector', cax2epid_distance: 'float', cax2bb_distance: 'float', cax2bb_vector: 'Vector', bb_location: 'Point', field_cax: 'Point')[source]¶
Bases:
ResultBase
- variable_axis: str¶
- cax2epid_distance: float¶
- cax2bb_distance: float¶