diff --git a/docs/source/conf.py b/docs/source/conf.py index bcc42c0e1..55bb3e509 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,6 +16,8 @@ import sys import warnings +import matplotlib.pyplot as plt + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -25,6 +27,9 @@ # suppress pydicom VR length warnings; just creates noise + +plt.set_cmap("gray") + warnings.filterwarnings("ignore", category=UserWarning, module="pydicom") # -- General configuration ------------------------------------------------ diff --git a/docs/source/image_generator.rst b/docs/source/image_generator.rst index 049c4036e..88f6afd1d 100644 --- a/docs/source/image_generator.rst +++ b/docs/source/image_generator.rst @@ -28,15 +28,15 @@ The basics to get started are to import the image simulators and layers from pyl from matplotlib import pyplot as plt - from pylinac.core.image_generator import AS1000Image + from pylinac.core.image_generator import AS1200Image from pylinac.core.image_generator.layers import FilteredFieldLayer, GaussianFilterLayer - as1000 = AS1000Image() # this will set the pixel size and shape automatically - as1000.add_layer(FilteredFieldLayer(field_size_mm=(50, 50))) # create a 50x50mm square field - as1000.add_layer(GaussianFilterLayer(sigma_mm=2)) # add an image-wide gaussian to simulate penumbra/scatter - as1000.generate_dicom(file_out_name="my_AS1000.dcm", gantry_angle=45) # create a DICOM file with the simulated image + as1200 = AS1200Image() # this will set the pixel size and shape automatically + as1200.add_layer(FilteredFieldLayer(field_size_mm=(50, 50))) # create a 50x50mm square field + as1200.add_layer(GaussianFilterLayer(sigma_mm=2)) # add an image-wide gaussian to simulate penumbra/scatter + as1200.generate_dicom(file_out_name="my_AS1200.dcm", gantry_angle=45) # create a DICOM file with the simulated image # plot the generated image - plt.imshow(as1000.image) + plt.imshow(as1200.image) Layers & Simulators ------------------- @@ -81,10 +81,10 @@ To implement a custom layer, inherit from ``Layer`` and implement the ``apply`` # use - from pylinac.core.image_generator import AS1000Image + from pylinac.core.image_generator import AS1200Image - as1000 = AS1000Image() - as1000.add_layer(MyAwesomeLayer()) + as1200 = AS1200Image() + as1200.add_layer(MyAwesomeLayer()) ... Examples @@ -98,14 +98,14 @@ Simple Open Field .. plot:: from matplotlib import pyplot as plt - from pylinac.core.image_generator import AS1000Image + from pylinac.core.image_generator import AS1200Image from pylinac.core.image_generator.layers import FilteredFieldLayer, GaussianFilterLayer - as1000 = AS1000Image() # this will set the pixel size and shape automatically - as1000.add_layer(FilteredFieldLayer(field_size_mm=(150, 150))) # create a 50x50mm square field - as1000.add_layer(GaussianFilterLayer(sigma_mm=2)) # add an image-wide gaussian to simulate penumbra/scatter + as1200 = AS1200Image() # this will set the pixel size and shape automatically + as1200.add_layer(FilteredFieldLayer(field_size_mm=(150, 150))) # create a 50x50mm square field + as1200.add_layer(GaussianFilterLayer(sigma_mm=2)) # add an image-wide gaussian to simulate penumbra/scatter # plot the generated image - plt.imshow(as1000.image) + plt.imshow(as1200.image) Off-center Open Field ^^^^^^^^^^^^^^^^^^^^^ @@ -113,14 +113,14 @@ Off-center Open Field .. plot:: from matplotlib import pyplot as plt - from pylinac.core.image_generator import AS1000Image + from pylinac.core.image_generator import AS1200Image from pylinac.core.image_generator.layers import FilteredFieldLayer, GaussianFilterLayer - as1000 = AS1000Image() # this will set the pixel size and shape automatically - as1000.add_layer(FilteredFieldLayer(field_size_mm=(30, 30), cax_offset_mm=(20, 40))) - as1000.add_layer(GaussianFilterLayer(sigma_mm=3)) + as1200 = AS1200Image() # this will set the pixel size and shape automatically + as1200.add_layer(FilteredFieldLayer(field_size_mm=(30, 30), cax_offset_mm=(20, 40))) + as1200.add_layer(GaussianFilterLayer(sigma_mm=3)) # plot the generated image - plt.imshow(as1000.image) + plt.imshow(as1200.image) Winston-Lutz FFF Cone Field with Noise ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -194,17 +194,9 @@ rotates the image after every layer is applied. from pylinac.core.image_generator.layers import FilteredFieldLayer, GaussianFilterLayer as1200 = AS1200Image() - as1200.add_layer(FilteredFieldLayer((250, 7), alpha=0.5)) - as1200.image = ndimage.rotate(as1200.image, 30, reshape=False, mode='nearest') - as1200.add_layer(FilteredFieldLayer((250, 7), alpha=0.5)) - as1200.image = ndimage.rotate(as1200.image, 30, reshape=False, mode='nearest') - as1200.add_layer(FilteredFieldLayer((250, 7), alpha=0.5)) - as1200.image = ndimage.rotate(as1200.image, 30, reshape=False, mode='nearest') - as1200.add_layer(FilteredFieldLayer((250, 7), alpha=0.5)) - as1200.image = ndimage.rotate(as1200.image, 30, reshape=False, mode='nearest') - as1200.add_layer(FilteredFieldLayer((250, 7), alpha=0.5)) - as1200.image = ndimage.rotate(as1200.image, 30, reshape=False, mode='nearest') - as1200.add_layer(FilteredFieldLayer((250, 7), alpha=0.5)) + for _ in range(6): + as1200.add_layer(FilteredFieldLayer((250, 7), alpha=0.5)) + as1200.image = ndimage.rotate(as1200.image, 30, reshape=False, mode='nearest') as1200.add_layer(GaussianFilterLayer()) plt.imshow(as1200.image) plt.show() @@ -219,7 +211,7 @@ Using the new utility functions of v2.5+ we can construct full dicom files of pi from pylinac.core.image_generator import generate_picketfence, generate_winstonlutz from pylinac.core import image_generator - sim = image_generator.simulators.AS1000Image() + sim = image_generator.simulators.AS1200Image() field_layer = image_generator.layers.FilteredFieldLayer # could also do FilterFreeLayer generate_picketfence( simulator=Simulator, diff --git a/docs/source/vmat_docs.rst b/docs/source/vmat_docs.rst index 1cba98950..435fe29c0 100644 --- a/docs/source/vmat_docs.rst +++ b/docs/source/vmat_docs.rst @@ -251,15 +251,15 @@ In this example, we generate a perfectly flat set of images and analyze them. # open image open_path = 'perfect_open_drmlc.dcm' as1200 = AS1200Image() - as1200.add_layer(PerfectFieldLayer(field_size_mm=(150, 110), cax_offset_mm=(0, 5))) + as1200.add_layer(PerfectFieldLayer(field_size_mm=(150, 85), cax_offset_mm=(0, 0))) as1200.add_layer(GaussianFilterLayer(sigma_mm=2)) as1200.generate_dicom(file_out_name=open_path) # DMLC image dmlc_path = 'perfect_dmlc_drmlc.dcm' as1200 = AS1200Image() - for offset in (-40, -10, 20, 50): - as1200.add_layer(PerfectFieldLayer((150, 20), cax_offset_mm=(0, offset))) + for offset in (-45, -15, 15, 45): + as1200.add_layer(PerfectFieldLayer((150, 19.5), cax_offset_mm=(0, offset))) as1200.add_layer(GaussianFilterLayer(sigma_mm=2)) as1200.generate_dicom(file_out_name=dmlc_path) @@ -289,7 +289,7 @@ We now add a horn effect and random noise to the data: # open image open_path = 'noisy_open_drmlc.dcm' as1200 = AS1200Image() - as1200.add_layer(FilteredFieldLayer(field_size_mm=(150, 110), cax_offset_mm=(0, 5))) + as1200.add_layer(FilteredFieldLayer(field_size_mm=(150, 85), cax_offset_mm=(0, 0))) as1200.add_layer(GaussianFilterLayer(sigma_mm=2)) as1200.add_layer(RandomNoiseLayer(sigma=0.03)) as1200.generate_dicom(file_out_name=open_path) @@ -297,8 +297,8 @@ We now add a horn effect and random noise to the data: # DMLC image dmlc_path = 'noisy_dmlc_drmlc.dcm' as1200 = AS1200Image() - for offset in (-40, -10, 20, 50): - as1200.add_layer(FilteredFieldLayer((150, 20), cax_offset_mm=(0, offset))) + for offset in (-45, -15, 15, 45): + as1200.add_layer(FilteredFieldLayer((150, 19.5), cax_offset_mm=(0, offset))) as1200.add_layer(GaussianFilterLayer(sigma_mm=2)) as1200.add_layer(RandomNoiseLayer(sigma=0.03)) as1200.generate_dicom(file_out_name=dmlc_path) @@ -336,7 +336,7 @@ Let's now get devious and randomly adjust the height of each ROI (effectively ch # open image open_path = 'noisy_open_drmlc.dcm' as1200 = AS1200Image() - as1200.add_layer(FilteredFieldLayer(field_size_mm=(150, 110), cax_offset_mm=(0, 5))) + as1200.add_layer(FilteredFieldLayer(field_size_mm=(150, 85), cax_offset_mm=(0, 0))) as1200.add_layer(GaussianFilterLayer(sigma_mm=2)) as1200.add_layer(RandomNoiseLayer(sigma=0.03)) as1200.generate_dicom(file_out_name=open_path) @@ -344,10 +344,10 @@ Let's now get devious and randomly adjust the height of each ROI (effectively ch # DMLC image dmlc_path = 'noisy_dmlc_drmlc.dcm' as1200 = AS1200Image() - for offset in (-40, -10, 20, 50): - as1200.add_layer(FilteredFieldLayer((150, 20), cax_offset_mm=(0, offset), alpha=random.uniform(0.93, 1))) + for offset in (-45, -15, 15, 45): + as1200.add_layer(FilteredFieldLayer((150, 19.5), cax_offset_mm=(0, offset), alpha=random.uniform(0.93, 1))) as1200.add_layer(GaussianFilterLayer(sigma_mm=2)) - as1200.add_layer(RandomNoiseLayer(sigma=0.03)) + as1200.add_layer(RandomNoiseLayer(sigma=0.04)) as1200.generate_dicom(file_out_name=dmlc_path) # analyze it diff --git a/docs/source/winston_lutz.rst b/docs/source/winston_lutz.rst index 17a037693..dcf89d188 100644 --- a/docs/source/winston_lutz.rst +++ b/docs/source/winston_lutz.rst @@ -890,8 +890,8 @@ values. We offset the BB to the left by 2mm for visualization purposes: AS1200Image(1000), FilteredFieldLayer, dir_out=wl_dir, - final_layers=[GaussianFilterLayer(), ], - bb_size_mm=4, + final_layers=[GaussianFilterLayer(sigma_mm=1), ], + bb_size_mm=5, field_size_mm=(20, 20), offset_mm_left=2, image_axes=[(0, 0, 0), (0, 45, 0), (0, 270, 0), @@ -900,7 +900,7 @@ values. We offset the BB to the left by 2mm for visualization purposes: ) wl = pylinac.WinstonLutz(wl_dir) - wl.analyze(bb_size_mm=4) + wl.analyze(bb_size_mm=5) print(wl.results()) wl.plot_images(axis=Axis.GBP_COMBO) diff --git a/noxfile.py b/noxfile.py index c948d7a8d..7536350dd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,8 +31,6 @@ def build_docs(session): "docs/build", "-W", "--keep-going", - "-j", - "auto", "-a", "-E", "-b", diff --git a/pylinac/core/image_generator/layers.py b/pylinac/core/image_generator/layers.py index 1c6fad28f..838ca81aa 100644 --- a/pylinac/core/image_generator/layers.py +++ b/pylinac/core/image_generator/layers.py @@ -181,7 +181,7 @@ def __init__( ---------- field_size_mm - Field size in mm at the iso plane as (width, height) + Field size in mm at the iso plane as (height, width) cax_offset_mm The offset in mm. (down, right) alpha @@ -239,7 +239,7 @@ def __init__( ---------- field_size_mm - Field size in mm at the iso plane + Field size in mm at the iso plane (height, width) cax_offset_mm The offset in mm. (out, right) alpha @@ -299,7 +299,7 @@ def __init__( ---------- field_size_mm - Field size in mm at the iso plane + Field size in mm at the iso plane (height, width). cax_offset_mm The offset in mm. (out, right) alpha @@ -442,7 +442,7 @@ def draw_rotated_rectangle( center : list The center of the rectangle. extent : list - The width and height of the rectangle. + The height and width of the rectangle. angle : float The angle of rotation in degrees. @@ -457,10 +457,10 @@ def draw_rotated_rectangle( """ # Calculate rectangle coordinates before rotation # follows row,col convention of numpy; y = row, x = col - x0 = center[1] - extent[0] / 2 - x1 = center[1] + extent[0] / 2 - y0 = center[0] - extent[1] / 2 - y1 = center[0] + extent[1] / 2 + x0 = center[1] - extent[1] / 2 + x1 = center[1] + extent[1] / 2 + y0 = center[0] - extent[0] / 2 + y1 = center[0] + extent[0] / 2 rect_coords = np.array([[x0, y0], [x1, y0], [x1, y1], [x0, y1]]) diff --git a/pylinac/core/image_generator/utils.py b/pylinac/core/image_generator/utils.py index 876dbdcaa..b0eeb5457 100644 --- a/pylinac/core/image_generator/utils.py +++ b/pylinac/core/image_generator/utils.py @@ -125,10 +125,10 @@ def generate_picketfence( pos += picket_offset_error[idx] if orientation == orientation.UP_DOWN: position = (0, pos) - layout = (picket_width_mm, picket_height_mm) + layout = (picket_height_mm, picket_width_mm) else: position = (pos, 0) - layout = (picket_height_mm, picket_width_mm) + layout = (picket_width_mm, picket_height_mm) simulator.add_layer(field_layer(field_size_mm=layout, cax_offset_mm=position)) if final_layers is not None: for layer in final_layers: diff --git a/tests_basic/core/test_generator.py b/tests_basic/core/test_generator.py index e99a1579a..970350478 100644 --- a/tests_basic/core/test_generator.py +++ b/tests_basic/core/test_generator.py @@ -7,6 +7,7 @@ import numpy.testing import pydicom +import tests_basic # noqa; load env settings and filters from pylinac import Interpolation, Normalization from pylinac.core.image import DicomImage, load from pylinac.core.image_generator import ( @@ -14,6 +15,7 @@ AS1000Image, AS1200Image, ConstantLayer, + FilteredFieldLayer, FilterFreeFieldLayer, GaussianFilterLayer, PerfectBBLayer, @@ -86,7 +88,121 @@ def profiles_from_simulator( return inplane_profile, cross_profile +class TestFilteredFieldLayer(TestCase): + def test_50x50_1000sid_centered(self): + for sim in (AS500Image, AS1000Image, AS1200Image): + as1200 = sim(sid=1000) + as1200.add_layer(FilteredFieldLayer(field_size_mm=(50, 50))) + as1200_ds = as1200.as_dicom() + img = DicomImage.from_dataset(as1200_ds) + centers = img.compute(GlobalFieldLocator(max_number=1)) + # test we're at the center + self.assertAlmostEqual(centers[0].x, img.center.x, delta=1) + self.assertAlmostEqual(centers[0].y, img.center.y, delta=1) + + def test_50x50_1000sid_offset(self): + for sim in (AS500Image, AS1000Image, AS1200Image): + as1200 = sim(sid=1000) + as1200.add_layer( + FilteredFieldLayer(field_size_mm=(50, 50), cax_offset_mm=(30, 50)) + ) + as1200_ds = as1200.as_dicom() + img = DicomImage.from_dataset(as1200_ds) + centers = img.compute(GlobalFieldLocator(max_number=1)) + # test we're at the offset + self.assertAlmostEqual( + centers[0].x, img.center.x + 50 / sim.pixel_size, delta=1 + ) + self.assertAlmostEqual( + centers[0].y, img.center.y + 30 / sim.pixel_size, delta=1 + ) + + def test_50x50_1500sid_centered(self): + for sim in (AS500Image, AS1000Image, AS1200Image): + as1200 = sim(sid=1500) + as1200.add_layer(FilteredFieldLayer(field_size_mm=(50, 50))) + as1200_ds = as1200.as_dicom() + img = DicomImage.from_dataset(as1200_ds) + centers = img.compute(GlobalFieldLocator(max_number=1)) + # test we're at the center + self.assertAlmostEqual(centers[0].x, img.center.x, delta=1) + self.assertAlmostEqual(centers[0].y, img.center.y, delta=1) + + def test_50x50_1500sid_offset(self): + for sim in (AS500Image, AS1000Image, AS1200Image): + as1200 = sim(sid=1500) + as1200.add_layer( + FilteredFieldLayer(field_size_mm=(50, 50), cax_offset_mm=(30, 50)) + ) + as1200_ds = as1200.as_dicom() + img = DicomImage.from_dataset(as1200_ds) + centers = img.compute(GlobalFieldLocator(max_number=1)) + # test we're at the offset + self.assertAlmostEqual( + centers[0].x, img.center.x + 50 * 1.5 / sim.pixel_size, delta=1 + ) + self.assertAlmostEqual( + centers[0].y, img.center.y + 30 * 1.5 / sim.pixel_size, delta=1 + ) + + class TestPerfectFieldLayer(TestCase): + def test_50x50_1000sid_centered(self): + for sim in (AS500Image, AS1000Image, AS1200Image): + as1200 = sim(sid=1000) + as1200.add_layer(PerfectFieldLayer(field_size_mm=(50, 50))) + as1200_ds = as1200.as_dicom() + img = DicomImage.from_dataset(as1200_ds) + centers = img.compute(GlobalFieldLocator(max_number=1)) + # test we're at the center + self.assertAlmostEqual(centers[0].x, img.center.x, delta=1) + self.assertAlmostEqual(centers[0].y, img.center.y, delta=1) + + def test_50x50_1000sid_offset(self): + for sim in (AS500Image, AS1000Image, AS1200Image): + as1200 = sim(sid=1000) + as1200.add_layer( + PerfectFieldLayer(field_size_mm=(50, 50), cax_offset_mm=(30, 50)) + ) + as1200_ds = as1200.as_dicom() + img = DicomImage.from_dataset(as1200_ds) + centers = img.compute(GlobalFieldLocator(max_number=1)) + # test we're at the offset + self.assertAlmostEqual( + centers[0].x, img.center.x + 50 / sim.pixel_size, delta=1 + ) + self.assertAlmostEqual( + centers[0].y, img.center.y + 30 / sim.pixel_size, delta=1 + ) + + def test_50x50_1500sid_centered(self): + for sim in (AS500Image, AS1000Image, AS1200Image): + as1200 = sim(sid=1500) + as1200.add_layer(PerfectFieldLayer(field_size_mm=(50, 50))) + as1200_ds = as1200.as_dicom() + img = DicomImage.from_dataset(as1200_ds) + centers = img.compute(GlobalFieldLocator(max_number=1)) + # test we're at the center + self.assertAlmostEqual(centers[0].x, img.center.x, delta=1) + self.assertAlmostEqual(centers[0].y, img.center.y, delta=1) + + def test_50x50_1500sid_offset(self): + for sim in (AS500Image, AS1000Image, AS1200Image): + as1200 = sim(sid=1500) + as1200.add_layer( + PerfectFieldLayer(field_size_mm=(50, 50), cax_offset_mm=(30, 50)) + ) + as1200_ds = as1200.as_dicom() + img = DicomImage.from_dataset(as1200_ds) + centers = img.compute(GlobalFieldLocator(max_number=1)) + # test we're at the offset + self.assertAlmostEqual( + centers[0].x, img.center.x + 50 * 1.5 / sim.pixel_size, delta=1 + ) + self.assertAlmostEqual( + centers[0].y, img.center.y + 30 * 1.5 / sim.pixel_size, delta=1 + ) + def test_10x10_100sid(self): for sim in (AS500Image, AS1000Image, AS1200Image): as1200 = sim(sid=1000)