Skip to content

Commit

Permalink
Merged in feature/RAM-3656_field_analysis_v2 (pull request jrkerns#390)
Browse files Browse the repository at this point in the history
Feature/RAM-3656 field analysis v2

Approved-by: Randy Taylor
  • Loading branch information
jrkerns committed May 31, 2024
2 parents 16688ee + 168b390 commit 467adaa
Show file tree
Hide file tree
Showing 15 changed files with 1,611 additions and 46 deletions.
24 changes: 24 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ Core
and out-of-place multiplication. E.g. ``Point(1, 2) * 2`` will return a new point at (2, 4) and
also change the original point to (2, 4).

Field Analysis
^^^^^^^^^^^^^^

* There is a new module for performing field analysis that leverages the 1D metrics framework. This
is an alternative and successor to the original field analysis module. You can read more here: :ref:`field-profile-analysis`.

Profiles & 1D Metrics
^^^^^^^^^^^^^^^^^^^^^

* 1D Profile Metrics have two new methods: ``geometric_center_idx`` and ``cax_index`` that return the index
(interpolated) for their respective values.
* The ``plot`` method for profiles now includes a ``mirror`` parameter. This will mirror the profile about the
geomtric center or beam center index. This is useful for visualizing the symmetry of the profile.
* Physical profile plots now also plot the x-axis in physical values on a secondary axis.
* 1D metrics now have a ``full_name`` property that concatenates the name of the metric and the unit if applicable.
* Calculated metrics of a profile that are stored in the ``metric_values`` attribute are now saved using the full name
as described above. This means if you access metrics this way, you may need to update the lookup to include the unit.
* Two new metrics have been added: ``CAXtoLeftBeamEdge`` and ``CAXToRightBeamEdge``. These metrics will calculate the distance
from the CAX to the left and right beam edges, respectively.
* The ``FlatnessDifferenceMetric`` had a bug that would cause plotting to fail.
* The ``SymmetryPointDifferenceQuotientMetric`` 's default max and min range has been adjusted to 100-105 to better reflect default values.
* The ``PenumbraLeftMetric`` and ``PenumbraRightMetric`` had their unit's changed from % to mm. % was incorrect.
* The ``SlopeMetric`` would sometimes fail to plot if an uneven number of points were calculated over.

v 3.23.0
--------

Expand Down
5 changes: 5 additions & 0 deletions docs/source/field_analysis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
Field Analysis
==============

.. warning::

This module will be deprecated in favor of the newer method of :ref:`field-profile-analysis` at some point in the future.
This module will either be moved to a ``deprecated`` module in v4 or simply removed.

Overview
--------

Expand Down
339 changes: 339 additions & 0 deletions docs/source/field_profile_analysis.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
.. _field-profile-analysis:

======================
Field Profile Analysis
======================

The modular field analysis is the newer implementation of the :ref:`field_analysis_module` concept.
Colloquially, it is referred to as "v2" of the field analysis module.
It leverages the new :ref:`profiles` framework to provide a more flexible way
to analyze fields.

.. note::

If you are familiar with the old field analysis module and want to convert, you can read about the differences in
:ref:`comparison-to-field-analysis-v1`.

Overview
========

The field profile analysis module (``pylinac.field_profile_analysis``) allows a physicist to analyze various metrics of an open radiation
field from extracted profiles. The user can specify the location and width of the profiles
from the image. They can also select various metrics to compute on the profiles as well
as write custom plugins to compute additional metrics as necessary for the specific application
of interest. The use cases and assumptions/constraints are the same as for the :ref:`profiles <profiles>` framework.

Typical Usage
=============

To analyze profiles from an open field, the user should import the class, pass the path to the open beam, and then analyze the profiles from the beam.

.. code-block:: python
from pylinac import FieldProfileAnalysis, Centering, Normalization, Edge
from pylinac.metrics.profile import (
PenumbraLeftMetric,
PenumbraRightMetric,
SymmetryAreaMetric,
FlatnessDifferenceMetric,
)
path = r"C:\path\to\open_field.dcm"
field_analyzer = FieldProfileAnalysis(path)
field_analyzer.analyze(
centering=Centering.BEAM_CENTER,
x_width=0.02,
y_width=0.02,
normalization=Normalization.BEAM_CENTER,
edge_type=Edge.INFLECTION_DERIVATIVE,
ground=True,
metrics=(
PenumbraLeftMetric(),
PenumbraRightMetric(),
SymmetryAreaMetric(),
FlatnessDifferenceMetric(),
),
)
field_analyzer.plot_analyzed_images(show_grid=True, mirror="beam")
which will output 3 images: the image itself marked with where the profiles were taken
and the X and Y profile plots along with the metrics plotted.

.. plot::
:include-source: false

from pylinac import FieldProfileAnalysis, Centering, Normalization
from pylinac.metrics.profile import PenumbraLeftMetric, PenumbraRightMetric, SymmetryAreaMetric, FlatnessDifferenceMetric

field_analyzer = FieldProfileAnalysis.from_demo_image()
field_analyzer.analyze(centering=Centering.BEAM_CENTER, x_width=0.02, y_width=0.02, normalization=Normalization.BEAM_CENTER, ground=True, metrics=(PenumbraLeftMetric(), PenumbraRightMetric(), SymmetryAreaMetric(), FlatnessDifferenceMetric()))
field_analyzer.plot_analyzed_images(mirror='beam')

Analysis Options
================

Centering
^^^^^^^^^

There are 3 centering options: manual, beam center, and geometric center.

Manual
######

Manual centering means that you as the user specify the position of the image that the profiles are taken from.

.. code-block:: python
from pylinac import FieldProfileAnalysis, Centering
fa = FieldProfileAnalysis(...)
fa.analyze(..., centering=Centering.MANUAL) # default is the middle of the image.
# or specify a custom location
fa.analyze(..., centering=Centering.MANUAL, position=(0.3, 0.8))
# take profile at 30% height and 80% width
Beam center
###########

This is the default centering option. It first looks for the field to find the approximate center along each axis.
Then it extracts the profiles at these coordinates. This is helpful if you always want to be near the center of the field, even for offset fields or wedges.

.. code-block:: python
from pylinac import FieldProfileAnalysis, Centering
fa = FieldProfileAnalysis(...)
fa.analyze(...) # nothing special needed as it's the default
# Specifying a position here will be ignored
fa.analyze(..., centering=Centering.BEAM_CENTER, position=(0.1, 0.4))
# this is allowed but will result in the same result as above
Geometric center
################

The geometric center will always find the middle pixel and extract the profiles from there.
This is helpful if you always want to be at the center of the image.

.. code-block:: python
from pylinac import FieldProfileAnalysis, Centering
fa = FieldProfileAnalysis(...)
fa.analyze(..., centering=Centering.GEOMETRIC_CENTER)
.. _edge-type:

Edge detection
^^^^^^^^^^^^^^

Edge detection is important for determining the field width and beam center (which is often used for symmetry).
There are 3 detection strategies: FWHM, inflection via derivative, and inflection via the Hill/sigmoid/4PNLR function.

FWHM
####

The full-width half-max strategy is traditional and works for flat beams. It can give poor values for FFF beams.

.. code-block:: python
from pylinac import FieldProfileAnalysis, Edge
fa = FieldProfileAnalysis(...)
fa.analyze(..., edge_type=Edge.FWHM)
Inflection (derivative)
#######################

The inflection point via the derivative is useful for both flat and FFF beams, and is thus the default for ``FieldProfileAnalysis``.
The method will find the positions of the max and min derivative of the values. Using a 0-crossing of the 2nd derivative
can be tripped up by noise so it is not used.

.. note::

This method is recommended for high spatial resolution images such as the EPID, where the derivative has several points to use at the beam edge.
It is not recommended for 2D device arrays.

.. code-block:: python
from pylinac import FieldProfileAnalysis, Edge
fa = FieldProfileAnalysis(...) # nothing special needed as it's the default
# you may also specify the edge smoothing value. This is a gaussian filter applied to the derivative just for the purposes of finding the min/max derivative.
# This is to ensure the derivative is not caught by some noise. It is usually not necessary to change this.
fa = FieldProfileAnalysis(..., edge_smoothing_ratio=0.005)
Inflection (Hill)
#################

The inflection point via the Hill function is useful for both flat and FFF beams
The fitting of the function is best for low-resolution data.
The Hill function, the sigmoid function, and 4-point non-linear regression belong to a family of logistic equations to fit a dual-curved value.
Since these fit a function to the data the resolution problems are eliminated. Some examples can be
seen `here <https://en.wikipedia.org/wiki/Sigmoid_function#Examples>`__. The generalized logistic function has helpful visuals as well
`here <https://en.wikipedia.org/wiki/Generalised_logistic_function>`__.

The function used here is:

:math:`f(x) = A + \frac{B - A}{1 + \frac{C}{x}^D}`

where :math:`A` is the low asymptote value (~0 on the left edge of a field),
:math:`B` is the high asymptote value (~1 for a normalized beam on the left edge),
:math:`C` is the inflection point of the sigmoid curve,
and :math:`D` is the slope of the sigmoid.

The function is fitted to the edge data of the field on each side to return the function. From there, the inflection point, penumbra, and slope can be found.

.. note::

This method is recommended for low spatial resolution images such as 2D device arrays, where there is very little data at the beam edges.
While it can be used for EPID images as well, the fit can have small errors as compared to the direct data.
The fit, however, is much better than a linear or even spline interpolation at low resolutions.

.. code-block:: python
from pylinac import FieldProfileAnalysis, Edge
fa = FieldProfileAnalysis(..., edge_type=Edge.INFLECTION_HILL)
# you may also specify the Hill window. This is the size of the window (as a ratio) to use to fit the field edge to the Hill function.
fa = FieldProfileAnalysis(..., edge_type=Edge.INFLECTION_HILL, hill_window_ratio=0.05)
# i.e. use a 5% field width about the edges to fit the Hill function.
.. note::

When using this method, the fitted Hill function will also be plotted on the image. Further, the exact field edge
marker (green x) may not align with the Hill function fit. This is just a rounding issue due to the plotting mechanism.
The field edge is really using the Hill fit under the hood.

Normalization
-------------

There are 4 options for interpolation: ``NONE``, ``GEOMETRIC_CENTER``, ``BEAM_CENTER``, and ``MAX``. These should be
self-explanatory, especially in light of the centering explanations.

.. code-block:: python
from pylinac import FieldProfileAnalysis, Normalization
fa = FieldProfileAnalysis(...)
fa.analyze(..., normalization_method=Normalization.BEAM_CENTER)
.. _choosing-field-metrics:

Choosing Metrics
================

The metrics the user can use in the field/profile analysis that come out of the box
can be viewed in this list: :ref:`profile_builtin_plugins`. These metrics can then
be passed to the ``metrics`` parameter. The user can mix and match as desired.
It's also possible to write custom plugins; see :ref:`using-custom-field-analysis-plugins`.

.. code-block:: python
from pylinac import FieldProfileAnalysis
from pylinac.metrics.profile import PenumbraRightMetric, SlopeMetric, SymmetryAreaMetric
p = Path(r"C:\path\to\open_field.dcm")
field_analyzer = FieldProfileAnalysis(p)
field_analyzer.analyze(
metrics=[PenumbraRightMetric(), SlopeMetric(), SymmetryAreaMetric()]
)
field_analyzer.plot_analyzed_images()
Customizing Metrics
===================

To change the name or parameters of a metric, pass the relevant parameters to the metric's constructor.
E.g. to use 90/10 for the penumbra distance instead of 80/20:

.. code-block:: python
from pylinac import FieldProfileAnalysis
from pylinac.metrics.profile import PenumbraRightMetric
fa = FieldProfileAnalysis(...)
fa.analyze(metrics=[PenumbraRightMetric(lower=10, upper=90, color="r", ls="--")])
# will use 90/10 instead of 80/20 and plot the line in red and dashed
fa.plot_analyzed_images()
Each metric has its own parameters that can be customized. See the metric's documentation for more information.

Accessing results
=================

Results from the analysis can be printed as a simple string using :meth:`~pylinac.field_profile_analysis.FieldProfileAnalysis.results`.
The ideal method is to access the results using the :meth:`~pylinac.field_profile_analysis.FieldProfileAnalysis.results_data` method, which
can return a JSON string, a dictionary, or a :class:`~pylinac.field_profile_analysis.FieldResult` that can be used to access the results.

See also :ref:`exporting-results`.

.. _using-custom-field-analysis-plugins:

Using Custom Plugins
====================

To analyze custom metrics on the profiles, the user can write custom plugins.
First, :ref:`create the plugin itself <writing-profile-plugins>`. Then,
pass it to the ``metrics`` parameter per :ref:`choosing-field-metrics`.

.. code-block:: python
from pylinac import FieldProfileAnalysis
from pylinac.metrics.profile import PenumbraLeftMetric
from my_custom_plugins import MyCustomMetric
p = Path(r"C:\path\to\open_field.dcm")
field_analyzer = FieldProfileAnalysis(p)
field_analyzer.analyze(metrics=[MyCustomMetric(), PenumbraLeftMetric()])
field_analyzer.plot_analyzed_images()
.. _comparison-to-field-analysis-v1:

Comparison to field analysis v1
===============================

In comparison to the original :ref:`field_analysis_module` module,
this updated module:

* Almost every metric will result in the same value. E.g. flatness, symmetry, etc. The only
times that metrics showed differences was if interpolation was "None". In such a case,
the v1 penumbra may be slightly larger. This is due to the field edge index "snapping" to the nearest pixel.
In v2, the edge is always interpolated and never snaps.
* Is much easier and more logical to write new plugins for. The plugins are modular and are not tightly coupled
to this module.
* Is loosely coupled to the metrics themselves. Since the metrics themselves are
self-contained, can be customized directly, and don't rely on any specific data massaging or formatting, they can be used in other modules.
* Does not do any device-specific analysis (e.g. IC Profiler). The original module and intent
was to have a generic device-to-profile framework. This ended up being a considerable amount
of work and ultimately we felt it fell out of the scope of pylinac, which is to focus on image
analysis. If you desire device-specific profile analysis, the conversion from the original file/format
a 1D array of values is your responsibility.
* There is no explicit interpolation method. Values are always interpolated linearly under the hood.
* v1 uses language like "top, left, bottom, right". In v2 everything is left and right, including the
y-profile (vertical). Rotating the y-profile 90 degrees counterclockwise will
give the same understanding of language. I.e. the "top" is the y-left value.

.. image:: images/field_analysis_v2.png
:width: 750
:align: center

* While not strictly a user benefit, the modularity of the metrics and the analysis itself
makes the codebase much cleaner and easier to maintain (v1 is ~1500 LOC vs v2 at ~400).


API Documentation
=================


.. autoclass:: pylinac.field_profile_analysis.FieldProfileAnalysis
:members:
:inherited-members:

.. autoclass:: pylinac.field_profile_analysis.FieldProfileResult
Binary file added docs/source/images/field_analysis_v2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
winston_lutz
winston_lutz_multi
planar_imaging
field_profile_analysis
field_analysis
nuclear

Expand Down
Loading

0 comments on commit 467adaa

Please sign in to comment.