Skip to content

Commit

Permalink
Merged in feature/RAM-3730_roi_samplers (pull request #405)
Browse files Browse the repository at this point in the history
Feature/RAM-3730 ROI samplers

Approved-by: Randy Taylor
  • Loading branch information
jrkerns committed Jun 24, 2024
2 parents 9bda6a3 + ef2321f commit 23397fc
Show file tree
Hide file tree
Showing 15 changed files with 690 additions and 74 deletions.
24 changes: 24 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ Winston Lutz

* :bdg-success:`Feature` Reference axis values and tolerance-matching values for WL analysis can now be passed. See :ref:`setting-wl-reference-values`.

Metrics
^^^^^^^

* A new class of metrics has been added: Samplers. These are classes that sample an ROI of an image,
in contrast to the existing class of metrics that find image features. To start,
two finder-class metrics have been added: :class:`~pylinac.metrics.image.DiskROIMetric`
and :class:`~pylinac.metrics.image.RectangleROIMetric`.

Core
^^^^

Expand All @@ -26,7 +34,23 @@ Core
* :bdg-success:`Feature` Pylinac now supports QuAAC integration. QuAAC is an interoperability standard we created to attempt to
standardize how QA information is stored and to be vendor-neutral. You can read more about the QuAAC standard `here <https://quaac.readthedocs.io/en/latest/index.html>`__.
How to dump pylinac results to QuAAC format can be read in :ref:`exporting-to-quaac`.
* :class:`~pylinac.core.roi.DiskROI` has been refactored. The constructor signature has changed to be more generic for
other external usages.
Historically, these were only used internally and in the context of an ROI within a phantom. Unless you are using these
classes directly no change is needed. If using these classes, a new class method has been added that has the same
signature as the original constructor ``from_phantom_center``. I.e. to retain the old behavior:

.. code-block:: python
# old
d = DiskROI(array, angle, roi_radius, dist_from_center, phantom_center)
# new
d = DiskROI.from_phantom_center(
array, angle, roi_radius, dist_from_center, phantom_center
)
The ``DiskROI`` 's new constructor signature is a much simpler ``array, radius, center``.

v 3.24.1
--------
Expand Down
163 changes: 161 additions & 2 deletions docs/source/topics/image_metrics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ Use Cases
Tool Legend
-----------

Tools can be divided into two categories: Finders and Samplers. The main difference is that Finders are used to **locate** objects in the image (using image features/pixel data)
and Samplers are used to **sample** the image in some way where the location of the ROI is known or fixed.
Finders make use of image features while Samplers do not.

Finders use image features to locate objects in the image, such as a BB or field.
From here, a user may then want to calculate properties of the object, such as the area or weighted centroid.

Samplers are tools that sample the image in some way, such as calculating the mean pixel value of a region.
These are not mutually exclusive use cases. E.g. a Finder may be used to locate a BB and then a Sampler may be used to calculate the mean pixel value
of the region around that ROI or sample an offset from it, such as the penumbra or background.

Finders
^^^^^^^

+---------------------------------------------------------+------------------------------------------------------------------+------------------------------------------------------------------------------+
| Use Case | Constraint | Class |
+=========================================================+==================================================================+==============================================================================+
Expand All @@ -47,6 +61,17 @@ Tool Legend
| Find the ROI properties of a circular field in an image | The field size and location are known approximately | :class:`~pylinac.metrics.image.SizedDiskRegion` (``invert=False``) |
+---------------------------------------------------------+------------------------------------------------------------------+------------------------------------------------------------------------------+

Samplers
^^^^^^^^

* :class:`~pylinac.metrics.image.DiskROIMetric`: Sample a circular region of interest.
Will return a :class:`~pylinac.core.roi.DiskROI` object. This object contains
the mean pixel value, standard deviation, and other properties of the ROI.
* :class:`~pylinac.metrics.image.RectangleROIMetric`: Sample a rectangular region of interest.
Will return a :class:`~pylinac.core.roi.RectangleROI` object. This object contains
the mean pixel value, standard deviation, and other properties of the ROI.


Basic Usage
-----------

Expand Down Expand Up @@ -355,8 +380,10 @@ Global Field Locator
.. versionadded:: 3.17
The :class:`GlobalFieldLocator` metric will find fields within an image, but does not require the field to be a specific size.
It will find anything field-like in the image. The logic is similar to the :class:`GlobalSizedFieldLocator` metric otherwise.
The :class:`~pylinac.metrics.image.GlobalFieldLocator` metric will find fields within an image, but does not require the field to be a specific size.
It will find anything field-like in the image. The logic is similar to the :class:`~pylinac.metrics.image.GlobalSizedFieldLocator` metric otherwise.
The position can be specified in pixels or mm.
For example:
Expand All @@ -371,6 +398,124 @@ For example:
img.compute(metrics=GlobalFieldLocator(max_number=2))
img.plot() # this will plot the image with the fields overlaid
Disk ROI Metric
^^^^^^^^^^^^^^^
.. versionadded:: 3.25
The :class:`~pylinac.metrics.image.DiskROIMetric` metric will sample a circular region of interest (ROI) in the image.
This will return a :class:`~pylinac.core.roi.DiskROI` object. This object can then be used to
determine the mean pixel value, standard deviation, etc.
As with the other metrics, this can be specified in pixels or mm.
.. code-block:: python
import numpy as np
from pylinac.core.roi import DiskROI
from pylinac.core.image import DicomImage
from pylinac.metrics.image import DiskROIMetric
img = DicomImage("my_image.dcm")
# position in pixels
roi: DiskROI = img.compute(
metrics=DiskROIMetric(
radius=20,
center=Point(511.5, 383.5),
)
)
img.plot()
mean = np.mean(roi.pixel_values)
# position in mm
roi: DiskROI = img.compute(
metrics=DiskROIMetric.from_physical(
radius_mm=10,
center_mm=(70, 80),
)
)
.. plot::
:include-source: false
import numpy as np
from pylinac.core.geometry import Point
from pylinac.core.image import ArrayImage
from pylinac.metrics.image import DiskROIMetric
array = np.random.rand(100, 100)
img = ArrayImage(array)
img.compute(
metrics=DiskROIMetric(radius=20, center=Point(30, 40), text="My ROI", linewidth=5, fontsize='large'),
)
img.plot(vmin=0, vmax=5)
Rectangle ROI Metric
^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 3.25
The :class:`~pylinac.metrics.image.RectangleROIMetric` metric will sample a rectangular region of interest (ROI) in the image.
This will return a :class:`~pylinac.core.roi.RectangleROI` object. This object can then be used to
determine the mean pixel value, standard deviation, etc.

As with the other metrics, this can be specified in pixels or mm.

.. code-block:: python
import numpy as np
from pylinac.core.roi import RectangleROI
from pylinac.core.image import DicomImage
from pylinac.metrics.image import RectangleROIMetric
img = DicomImage("my_image.dcm")
# position in pixels
roi: RectangleROI = img.compute(
metrics=RectangleROIMetric(
width=20,
height=30,
center=Point(511.5, 383.5),
)
)
img.plot()
mean = np.mean(roi.pixel_array)
# position in mm
roi: RectangleROI = img.compute(
metrics=RectangleROIMetric.from_physical(
width_mm=10,
height_mm=20,
center_mm=(70, 80),
)
)
.. plot::
:include-source: false

import numpy as np

from pylinac.core.geometry import Point
from pylinac.core.image import ArrayImage
from pylinac.metrics.image import RectangleROIMetric

array = np.random.rand(100, 100)

img = ArrayImage(array)
img.compute(
metrics=RectangleROIMetric(width=20, height=30, center=Point(60, 50), text="My ROI", linewidth=5, fontsize='large'),
)
img.plot(vmin=0, vmax=5)


Writing Custom Plugins
----------------------

Expand Down Expand Up @@ -575,6 +720,9 @@ Here is the plot of the final image with the BB location and threshold boundary
API
---

Finders
^^^^^^^

.. autoclass:: pylinac.metrics.image.MetricBase
:inherited-members:
:members:
Expand All @@ -598,3 +746,14 @@ API
.. autoclass:: pylinac.metrics.image.GlobalFieldLocator
:inherited-members:
:members:

Samplers
^^^^^^^^

.. autoclass:: pylinac.metrics.image.DiskROIMetric
:inherited-members:
:members:

.. autoclass:: pylinac.metrics.image.RectangleROIMetric
:inherited-members:
:members:
12 changes: 6 additions & 6 deletions pylinac/acr.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class SpatialResolutionModule(CatPhanModule):

def _setup_rois(self) -> None:
for name, setting in self.roi_settings.items():
self.rois[name] = HighContrastDiskROI(
self.rois[name] = HighContrastDiskROI.from_phantom_center(
self.image,
setting["angle_corrected"],
setting["radius_pixels"],
Expand Down Expand Up @@ -587,7 +587,7 @@ class MRSlice11PositionModule(CatPhanModule):
def _setup_rois(self) -> None:
for name, setting in self.roi_settings.items():
# angle is +90 because pointing right is 0, and these rois move downward, not rightward
self.rois[name] = RectangleROI(
self.rois[name] = RectangleROI.from_phantom_center(
self.image,
setting["width_pixels"],
setting["height_pixels"],
Expand Down Expand Up @@ -664,7 +664,7 @@ def _setup_rois(self) -> None:
# thickness
for name, setting in self.thickness_roi_settings.items():
# angle is +90 because pointing right is 0, and these rois move downward, not rightward
self.thickness_rois[name] = ThicknessROI(
self.thickness_rois[name] = ThicknessROI.from_phantom_center(
self.image,
setting["width_pixels"],
setting["height_pixels"],
Expand All @@ -674,7 +674,7 @@ def _setup_rois(self) -> None:
)
# spatial res
for name, setting in self.roi_settings.items():
self.rois[name] = HighContrastDiskROI(
self.rois[name] = HighContrastDiskROI.from_phantom_center(
self.image,
setting["angle_corrected"],
setting["radius_pixels"],
Expand All @@ -685,7 +685,7 @@ def _setup_rois(self) -> None:
# slice position
for name, setting in self.position_roi_settings.items():
# angle is +90 because pointing right is 0, and these rois move downward, not rightward
self.position_rois[name] = ThicknessROI(
self.position_rois[name] = ThicknessROI.from_phantom_center(
self.image,
setting["width_pixels"],
setting["height_pixels"],
Expand Down Expand Up @@ -789,7 +789,7 @@ def __init__(self, catphan, offset):
def _setup_rois(self) -> None:
super()._setup_rois()
for name, roi in self.ghost_roi_settings.items():
self.ghost_rois[name] = RectangleROI(
self.ghost_rois[name] = RectangleROI.from_phantom_center(
self.image,
roi["width_pixels"],
roi["height_pixels"],
Expand Down
2 changes: 1 addition & 1 deletion pylinac/cheese.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class CheeseModule(CatPhanModule):
def _setup_rois(self) -> None:
# unlike its super, we use simple disk ROIs as we're not doing complicated things.
for name, setting in self.roi_settings.items():
self.rois[name] = DiskROI(
self.rois[name] = DiskROI.from_phantom_center(
self.image,
setting["angle_corrected"],
setting["radius_pixels"],
Expand Down
Loading

0 comments on commit 23397fc

Please sign in to comment.