Skip to content

Commit

Permalink
Merged in feature/RAM-3231_pf_from_multiple (pull request #340)
Browse files Browse the repository at this point in the history
RAM-3231 Improve PF from_multiple

Approved-by: Randy Taylor
  • Loading branch information
jrkerns committed Feb 7, 2024
2 parents edb24af + 593e7d1 commit b6aa0dd
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 6 deletions.
14 changes: 14 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ Changelog
v 3.20.0
--------

Core
^^^^

* The function ``image.load_multiples`` now accepts a ``loader`` parameter. This lets the user
pass a custom image class if desired. This is useful for subclasses of the base image classes.
E.g. ``image.load_multiples("my_image.dcm", loader=MyDicomImage)``. Default behavior still uses
``load``.

PicketFence
^^^^^^^^^^^

* The ``from_multiple_images`` method signature added the ``mlc`` keyword argument. Previously,
only the default MLC could be used.

Winston-Lutz
^^^^^^^^^^^^

Expand Down
5 changes: 4 additions & 1 deletion pylinac/core/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ def load_multiples(
image_file_list: Sequence,
method: str = "mean",
stretch_each: bool = True,
loader: callable = load,
**kwargs,
) -> ImageLike:
"""Combine multiple image files into one superimposed image.
Expand All @@ -225,6 +226,8 @@ def load_multiples(
A string specifying how the image values should be combined.
stretch_each : bool
Whether to normalize the images being combined by stretching their high/low values to the same values across images.
loader: callable
The function to use to load the images. If a special image subclass is used, this is how it can be passed.
kwargs :
Further keyword arguments are passed to the load function and stretch function.
Expand All @@ -237,7 +240,7 @@ def load_multiples(
>>> superimposed_img = load_multiples(paths)
"""
# load images
img_list = [load(path, **kwargs) for path in image_file_list]
img_list = [loader(path, **kwargs) for path in image_file_list]
first_img = img_list[0]

# check that all images are the same size and stretch if need be
Expand Down
17 changes: 14 additions & 3 deletions pylinac/picketfence.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ def from_multiple_images(
path_list: Iterable[str | Path],
stretch_each: bool = True,
method: str = "mean",
mlc: MLC | MLCArrangement | str = MLC.MILLENNIUM,
**kwargs,
):
"""Load and superimpose multiple images and instantiate a PF object.
Expand All @@ -310,16 +311,26 @@ def from_multiple_images(
Whether to stretch each image individually before combining. See ``load_multiples``.
method : {'sum', 'mean'}
The method to combine the images. See ``load_multiples``.
mlc : MLC, MLCArrangement, or str
The MLC model of the image. Must be an option from the enum :class:`~pylinac.picketfence.MLCs` or
an :class:`~pylinac.picketfence.MLCArrangement`.
kwargs
Passed to :func:`~pylinac.core.image.load_multiples`.
Passed to :func:`~pylinac.core.image.load_multiples` and to the PicketFence constructor.
"""
with io.BytesIO() as stream:
img = image.load_multiples(
path_list, stretch_each=stretch_each, method=method, **kwargs
path_list,
stretch_each=stretch_each,
method=method,
loader=PFDicomImage,
**kwargs,
)
img.save(stream)
stream.seek(0)
return cls(stream, **kwargs)
# there is a parameter name mismatch between the PFDicomImage and PicketFence constructors
# Dicom uses "use_filenames" and PicketFence uses "use_filename" 😖
use_filename = kwargs.pop("use_filenames", False)
return cls(stream, mlc=mlc, use_filename=use_filename, **kwargs)

@classmethod
def from_bb_setup(
Expand Down
11 changes: 9 additions & 2 deletions tests_basic/core/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,9 @@ class TestEquateImages(TestCase):
def test_same_sized_images_work(self):
"""As found here: https://github.com/jrkerns/pylinac/issues/446"""

image1 = load(np.random.rand(20, 20), dpi=10)
image2 = load(np.random.rand(10, 10), dpi=5)
gen = np.random.default_rng()
image1 = load(gen.random((20, 20)), dpi=10)
image2 = load(gen.random((10, 10)), dpi=5)

img1, img2 = equate_images(image1, image2)
self.assertEqual(img1.shape, img2.shape)
Expand Down Expand Up @@ -309,6 +310,12 @@ def test_load_multiples(self):
with self.assertRaises(ValueError):
image.load_multiples(paths)

def test_load_multiples_custom_loader(self):
"""Use a custom loader to load multiple images"""
paths = [dcm_path, dcm_path, dcm_path]
img = image.load_multiples(paths, loader=image.LinacDicomImage)
self.assertIsInstance(img, image.LinacDicomImage)

def test_nonsense(self):
with self.assertRaises(FileNotFoundError):
image.load("blahblah")
Expand Down
25 changes: 25 additions & 0 deletions tests_basic/test_picketfence.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,31 @@ def test_bb_pf_combo(self):
self.assertAlmostEqual(results.max_error_mm, 0.0, delta=0.005)


class LoadingFromMultiple(TestCase):
def test_loading_with_keywords(self):
# we pass **kwargs to the PFDicomImage constructor and also the PicketFence constructor
# make sure the kwargs are passed through and don't raise
path1 = get_file_from_cloud_test_repo([TEST_DIR, "combo-jaw.dcm"])
path2 = get_file_from_cloud_test_repo([TEST_DIR, "combo-mlc.dcm"])
pf = PicketFence.from_multiple_images(
[path1, path2],
stretch_each=True,
mlc=MLC.BMOD,
use_filenames=False,
crop_mm=1,
)
pf.analyze()
self.assertAlmostEqual(pf.percent_passing, 100, delta=1)

def test_loading_no_keywords(self):
# make sure no keywords doesn't raise
path1 = get_file_from_cloud_test_repo([TEST_DIR, "combo-jaw.dcm"])
path2 = get_file_from_cloud_test_repo([TEST_DIR, "combo-mlc.dcm"])
pf = PicketFence.from_multiple_images([path1, path2])
pf.analyze()
self.assertAlmostEqual(pf.percent_passing, 100, delta=1)


class TestPlottingSaving(TestCase):
@classmethod
def setUpClass(cls):
Expand Down

0 comments on commit b6aa0dd

Please sign in to comment.