Skip to content

Commit

Permalink
Merged in feature/RAM-3808_plotly (pull request #429)
Browse files Browse the repository at this point in the history
Feature/RAM-3808 plotly

Approved-by: Randy Taylor
Approved-by: Hasan Ammar
  • Loading branch information
jrkerns committed Aug 6, 2024
2 parents 9b5ecdc + 3906be4 commit 166a903
Show file tree
Hide file tree
Showing 19 changed files with 1,745 additions and 116 deletions.
11 changes: 10 additions & 1 deletion docs/source/v4_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,20 @@ Core
* ``load_url``
* ``.from_url(...)`` class methods. I've not seen a single use case for loading from a URL directly.

Plotting
--------

* The plotting of analyzed images will migrate from ``matplotlib`` to ``plotly``.
This is mainly related to being able to zoom in and otherwise examine an analyzed image
more closely. Once plotly is supported, there is no reason to keep both plotting methods
around. To reduce maintenance burden, the matplotlib plotting will be removed.

* Methods involving saving figures (``.save_image(...)``) to file will be removed. Both matplotlib and plotly can
save to file programmatically, and plotly can do so interactively as well.
* Plotting will always produce individual images. Pylinac would sometimes combine plots
in a subplot. This made for annoying figure sizing. Now, each plot will be its own figure.
E.g. for a 2D planar analysis, the plotting method will return up to 3 figures: the image,
the low-contrast plot, and the high-contrast plot. Previously, these would be plotted into
a single 3x1 subplot figure.


Open-Field Analysis
Expand Down
116 changes: 116 additions & 0 deletions pylinac/acr.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import numpy as np
from matplotlib import pyplot as plt
from plotly import graph_objects as go
from pydantic import BaseModel, ConfigDict, Field
from scipy import ndimage

Expand Down Expand Up @@ -41,6 +42,7 @@


class CTModule(CatPhanModule):
common_name = "HU Linearity"
attr_name = "ct_calibration_module"
roi_dist_mm = 63
roi_radius_mm = 10
Expand Down Expand Up @@ -184,6 +186,10 @@ def mtf(self) -> MTF:
spacings=spacings, diskset=list(self.rois.values())
)

def plotly_rois(self, fig: go.Figure) -> None:
for name, roi in self.rois.items():
roi.plotly(fig, color="green", name=name)

def plot_rois(self, axis: plt.Axes) -> None:
"""Plot the ROIs to the axis. Override to set the color"""
for roi, mtf in zip(self.rois.values(), self.mtf.norm_mtfs.values()):
Expand Down Expand Up @@ -317,6 +323,44 @@ def analyze(self) -> None:
clear_borders=self.clear_borders,
)

def plotly_analyzed_image(
self,
show: bool = True,
show_colorbar: bool = True,
show_legend: bool = True,
**kwargs,
) -> dict[str, go.Figure]:
"""Plot the analyzed image using Plotly. Will create multiple figures.
Parameters
----------
show : bool
Whether to show the images. Set to False if doing further processing of the figure.
show_colorbar : bool
Whether to show the colorbar on the images.
show_legend : bool
Whether to show the legend on the images.
kwargs
Additional keyword arguments to pass to the figure.
"""
figs = {}
for module in (
self.ct_calibration_module,
self.uniformity_module,
self.spatial_resolution_module,
self.low_contrast_module,
):
figs[module.common_name] = module.plotly(
show_colorbar=show_colorbar, show_legend=show_legend, **kwargs
)
figs["MTF"] = self.spatial_resolution_module.mtf.plotly(**kwargs)
figs["Side View"] = self.plotly_side_view(offset=-0.5)

if show:
for fig in figs.values():
fig.show()
return figs

def plot_analyzed_image(self, show: bool = True, **plt_kwargs) -> plt.Figure:
"""Plot the analyzed image
Expand Down Expand Up @@ -646,6 +690,10 @@ def plot_rois(self, axis: plt.Axes) -> None:
for roi in self.rois.values():
roi.plot2axes(axis, edgecolor="blue")

def plotly_rois(self, fig: go.Figure) -> None:
for name, roi in self.rois.items():
roi.plotly(fig, color="blue", name=name)


class MRSlice11ModuleOutput(BaseModel):
"""This class should not be called directly. It is returned by the ``results_data()`` method.
Expand Down Expand Up @@ -739,6 +787,14 @@ def plot_rois(self, axis: plt.Axes) -> None:
for roi, mtf in zip(self.rois.values(), self.rois.values()):
roi.plot2axes(axis, edgecolor="g")

def plotly_rois(self, fig: go.Figure) -> None:
for name, roi in self.position_rois.items():
roi.plotly(fig, color="blue", name=name)
for name, roi in self.thickness_rois.items():
roi.plotly(fig, color="blue", name=name)
for name, roi in self.rois.items():
roi.plotly(fig, color="green", name=name)

@property
def bar_difference_mm(self) -> float:
"""The difference in height between the two angled bars"""
Expand Down Expand Up @@ -860,6 +916,11 @@ def plot_rois(self, axis: plt.Axes) -> None:
for roi in self.ghost_rois.values():
roi.plot2axes(axis, edgecolor="yellow")

def plotly_rois(self, fig: go.Figure) -> None:
super().plotly_rois(fig)
for name, roi in self.ghost_rois.items():
roi.plotly(fig, color="yellow", name=name)

@property
def percent_image_uniformity(self) -> float:
"""PIU value calculated via section 5.3 of the manual"""
Expand Down Expand Up @@ -1012,6 +1073,10 @@ def _setup_rois(self) -> None:
"line": line,
}

def plotly_rois(self, fig: go.Figure) -> None:
for name, profile_data in self.profiles.items():
profile_data["line"].plotly(fig, line_width=2, color="blue", name=name)

def plot_rois(self, axis: plt.Axes):
for name, profile_data in self.profiles.items():
profile_data["line"].plot2axes(axis, width=2, color="blue")
Expand Down Expand Up @@ -1188,6 +1253,57 @@ def _select_echo_images(self, echo_number: int | None) -> None:
del self.dicom_stack[idx]
del self.dicom_stack.metadatas[idx]

def plotly_analyzed_image(
self,
show: bool = True,
show_colorbar: bool = True,
show_legend: bool = True,
**kwargs,
) -> dict[str, go.Figure]:
"""Plot the analyzed set of images to Plotly figures.
Parameters
----------
show : bool
Whether to show the plot.
show_colorbar : bool
Whether to show the colorbar on the plot.
show_legend : bool
Whether to show the legend on the plot.
kwargs
Additional keyword arguments to pass to the plot.
Returns
-------
dict
A dictionary of the Plotly figures where the key is the name of the
image and the value is the figure.
"""
figs = {}
# plot the images
for module in (
self.slice1,
self.geometric_distortion,
self.uniformity_module,
self.slice11,
):
figs[module.common_name] = module.plotly(
show_colorbar=show_colorbar, show_legend=show_legend, **kwargs
)
# side view
figs["Side View"] = self.plotly_side_view(offset=-0.2)
# mtf
fig = go.Figure()
self.slice1.row_mtf.plotly(fig=fig, name="Row-wise rMTF")
figs["MTF"] = self.slice1.col_mtf.plotly(
fig=fig, name="Column-wise rMTF", marker_color="orange"
)

if show:
for fig in figs.values():
fig.show()
return figs

def plot_analyzed_image(self, show: bool = True, **plt_kwargs) -> plt.Figure:
"""Plot the analyzed image
Expand Down
76 changes: 76 additions & 0 deletions pylinac/cheese.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

import numpy as np
from matplotlib import pyplot as plt
from plotly import graph_objects as go
from pydantic import Field

from .core import pdf
from .core.plotly_utils import add_title
from .core.profile import CollapsedCircleProfile
from .core.roi import DiskROI
from .core.scale import abs360
Expand Down Expand Up @@ -109,6 +111,15 @@ def plot_rois(self, axis: plt.Axes) -> None:
for name, roi in self.rois.items():
roi.plot2axes(axis, edgecolor="blue", text=name)

def plotly_rois(self, fig: go.Figure) -> None:
"""Plot the ROIs to the figure. We add the ROI # to help the user differentiate"""
for name, roi in self.rois.items():
roi.plotly(
fig,
name=name,
color="blue",
)


class TomoCheeseModule(CheeseModule):
"""The pluggable module with user-accessible holes.
Expand Down Expand Up @@ -292,6 +303,71 @@ def find_phantom_roll(self, func: Callable | None = None) -> float:
)
return 0

def plotly_analyzed_image(
self,
show: bool = True,
show_colorbar: bool = True,
show_legend: bool = True,
**kwargs,
) -> dict[str, go.Figure]:
"""Plot the analyzed set of images to Plotly figures.
Parameters
----------
show : bool
Whether to show the plot.
show_colorbar : bool
Whether to show the colorbar on the plot.
show_legend : bool
Whether to show the legend on the plot.
kwargs
Additional keyword arguments to pass to the plot.
Returns
-------
dict
A dictionary of the Plotly figures where the key is the name of the
image and the value is the figure.
"""
figs = {
self.module.common_name: self.module.plotly(
show_colorbar=show_colorbar, show_legend=show_legend, **kwargs
)
}

# density curve (if densities passed)
if self.roi_config:
xs = []
ys = []
for roi_num, roi_data in self.roi_config.items():
xs.append(roi_data["density"])
ys.append(self.module.rois[roi_num].pixel_value)
# sort by HU so it looks like a normal curve; ROI densities can be out of order
sorted_args = np.argsort(xs)
xs = np.array(xs)[sorted_args]
ys = np.array(ys)[sorted_args]
# plot
density_fig = go.Figure()
density_fig.add_scatter(
x=xs,
y=ys,
mode="lines+markers",
line_dash="dash",
marker_symbol="diamond",
)
density_fig.update_layout(
yaxis_title="HU",
xaxis_title="Density",
)
add_title(density_fig, "Density vs HU curve")
figs["Density vs HU curve"] = density_fig

if show:
for fig in figs.values():
fig.show()
return figs

def plot_analyzed_image(self, show: bool = True, **plt_kwargs: dict) -> None:
"""Plot the images used in the calculation and summary data.
Expand Down
Loading

0 comments on commit 166a903

Please sign in to comment.