Source code for alexandria.plotters.uniformity_plotter
"""
Uniformity Plotter
Creates comprehensive visualization plots for uniformity analysis results.
"""
import matplotlib.patches as patches
import matplotlib.pyplot as plt
import numpy as np
from ..utils import compute_phantom_boundary
[docs]
class UniformityPlotter:
"""
Plotter for UniformityAnalyzer results.
"""
def __init__(self, analyzer):
self.analyzer = analyzer
self.results = analyzer.analyze()
def _add_roi_box(self, ax, center_xy, size, label, color="yellow", above=True):
cx, cy = center_xy
half = size / 2
rect = patches.Rectangle(
(cx - half, cy - half),
size,
size,
linewidth=1.5,
edgecolor=color,
facecolor="none",
)
ax.add_patch(rect)
stats = self.results[label.lower()]
text = f"{stats['mean']:.1f} ± {stats['std']:.1f}"
if above:
ax.text(
cx,
cy - 2 * half,
text,
color=color,
ha="center",
va="bottom",
fontsize=9,
bbox=dict(facecolor="black", alpha=0.4, pad=2),
)
else:
ax.text(
cx,
cy + 2 * half,
text,
color=color,
ha="center",
va="top",
fontsize=9,
bbox=dict(facecolor="black", alpha=0.4, pad=2),
)
[docs]
def plot(self):
img = self.analyzer.image
cx, cy = self.analyzer.center
fig, axes = plt.subplots(3, 2, figsize=(12, 15))
ax_img = axes[0, 0]
ax_hist = axes[0, 1]
ax_bar = axes[1, 0]
ax_box = axes[1, 1]
ax_prof = axes[2, 0]
ax_metric = axes[2, 1]
ax_img.imshow(img, cmap="gray")
ax_img.set_title("Uniformity Analysis")
ax_img.set_axis_off()
pixel_spacing = getattr(self.analyzer, "pixel_spacing", None)
boundary = getattr(self.analyzer, "boundary", None)
if boundary and "x" in boundary and "y" in boundary:
boundary_x = np.array(boundary["x"])
boundary_y = np.array(boundary["y"])
else:
_, (boundary_x, boundary_y) = compute_phantom_boundary(
img, self.analyzer.center, pixel_spacing
)
if len(boundary_x) > 0:
ax_img.plot(boundary_x, boundary_y, "r-", linewidth=1.5, alpha=0.5)
if hasattr(self.analyzer, "roi_offset_mm") and pixel_spacing:
analysis_radius_px = self.analyzer.roi_offset_mm / pixel_spacing
t = np.linspace(0, 2 * np.pi, 100)
analysis_x = analysis_radius_px * np.cos(t) + self.analyzer.center[0]
analysis_y = analysis_radius_px * np.sin(t) + self.analyzer.center[1]
ax_img.plot(analysis_x, analysis_y, "c--", linewidth=1.0, alpha=0.4)
ax_img.plot(cx, cy, "r+", markersize=15, markeredgewidth=2)
cx, cy = self.analyzer.center
size = self.analyzer.roi_size
offset = self.analyzer.roi_offset
centers = {
"centre": (cx, cy),
"north": (cx, cy - offset),
"south": (cx, cy + offset),
"east": (cx + offset, cy),
"west": (cx - offset, cy),
}
roi_colors = {
"centre": "purple",
"north": "blue",
"south": "orange",
"east": "green",
"west": "red",
}
legend_handles = []
labels = list(centers.keys())
means = []
sems = []
roi_datas = []
for label, coord in centers.items():
color = roi_colors.get(label, "white")
if label == "centre" or label == "south":
self._add_roi_box(ax_img, coord, size, label, color, above=False)
else:
self._add_roi_box(ax_img, coord, size, label, color)
cx_roi, cy_roi = coord
half = size / 2
roi_data = img[
int(cy_roi - half) : int(cy_roi + half),
int(cx_roi - half) : int(cx_roi + half),
].flatten()
roi_datas.append(roi_data)
ax_hist.hist(
roi_data, histtype="step", color=color, linewidth=3, label=label
)
mean_val = self.results[label.lower()]["mean"]
ax_hist.axvline(mean_val, color=color, linestyle="--", linewidth=2)
legend_handles.append(
plt.Line2D(
[0],
[0],
color=color,
linewidth=2,
label=f"{label} (mean: {mean_val:.1f})",
)
)
means.append(mean_val)
roi_attr = getattr(self.analyzer, f"m{label[0]}")
n = roi_attr.size
std_val = self.results[label.lower()]["std"]
sem = std_val / np.sqrt(n)
sems.append(sem)
ax_hist.set_title("ROI Histograms (Overlaid)")
ax_hist.set_xlabel("HU")
ax_hist.set_ylabel("Counts")
ax_hist.legend(handles=legend_handles, loc="upper left", bbox_to_anchor=(1, 1))
ax_hist.grid(True, alpha=0.3)
table_data = [["ROI", "Mean (HU)", "Std (HU)", "SEM (HU)"]]
for label, mean_val, sem in zip(labels, means, sems):
std_val = self.results[label.lower()]["std"]
table_data.append(
[label.title(), f"{mean_val:.1f}", f"{std_val:.1f}", f"{sem:.2f}"]
)
table = ax_bar.table(
cellText=table_data,
cellLoc="center",
loc="center",
colWidths=[0.25, 0.25, 0.25, 0.25],
)
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1, 2)
for i in range(len(table_data[0])):
cell = table[(0, i)]
cell.set_facecolor("#4CAF50")
cell.set_text_props(weight="bold", color="white")
for i in range(1, len(table_data)):
roi_label = labels[i - 1]
for j in range(len(table_data[0])):
cell = table[(i, j)]
if i % 2 == 0:
cell.set_facecolor("#f0f0f0")
if j == 0:
cell.set_text_props(color=roi_colors[roi_label], weight="bold")
ax_bar.set_title("ROI Statistics", fontsize=12, weight="bold", pad=10)
ax_box.boxplot(roi_datas, labels=labels, patch_artist=True)
ax_box.set_title("ROI Boxplots")
ax_box.set_ylabel("HU")
for patch, color in zip(
ax_box.patches, [roi_colors[roi_label] for roi_label in labels]
):
patch.set_facecolor(color)
patch.set_alpha(0.7)
central_range = 300
half_range = central_range // 2
center_x = int(round(cx))
center_y = int(round(cy))
start_y = max(0, center_y - half_range)
end_y = min(img.shape[0], center_y + half_range)
vertical_profile = img[start_y:end_y, center_x]
vertical_positions = np.arange(start_y, end_y, dtype=float) - float(cy)
start_x = max(0, center_x - half_range)
end_x = min(img.shape[1], center_x + half_range)
horizontal_profile = img[center_y, start_x:end_x]
horizontal_positions = np.arange(start_x, end_x, dtype=float) - float(cx)
ax_prof.plot(
vertical_positions,
vertical_profile,
label="Vertical (central {}px)".format(central_range),
color="blue",
)
ax_prof.plot(
horizontal_positions,
horizontal_profile,
label="Horizontal (central {}px)".format(central_range),
color="red",
)
ax_prof.axvline(0.0, color="black", linestyle="--", linewidth=2, label="Center")
ax_prof.set_title("Center Profiles (Central 300 Pixels)")
ax_prof.set_xlabel("Position Relative to Center (px)")
ax_prof.set_ylabel("HU")
ax_prof.legend()
ax_prof.grid(True, alpha=0.3)
uni = self.results["uniformity"]
ax_metric.text(
0.5,
0.5,
f"Uniformity: {uni:.2f} %",
ha="center",
va="center",
fontsize=16,
transform=ax_metric.transAxes,
)
ax_metric.axis("off")
fig.tight_layout()
return fig