Source code for alexandria.plotters.uniformity_plotter
"""
Uniformity Plotter
Creates comprehensive visualization plots for uniformity analysis results.
"""
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
from typing import Optional
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[l] for l in labels]):
patch.set_facecolor(color)
patch.set_alpha(0.7)
central_range = 300
half_range = central_range//2
center_y = img.shape[0] // 2
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, int(cx)]
center_x = img.shape[1] // 2
start_x = max(0, center_x - half_range)
end_x = min(img.shape[1], center_x + half_range)
horizontal_profile = img[int(cy), start_x:end_x]
ax_prof.plot(vertical_profile, label='Vertical (central {}px)'.format(central_range), color='blue')
ax_prof.plot(horizontal_profile, label='Horizontal (central {}px)'.format(central_range), color='red')
vert_center_idx = int(cy) - start_y
horiz_center_idx = int(cx) - start_x
ax_prof.axvline(vert_center_idx, color='blue', linestyle='--', linewidth=2, label='Vertical center (y={:.0f})'.format(cy))
ax_prof.axvline(horiz_center_idx, color='red', linestyle='--', linewidth=2, label='Horizontal center (x={:.0f})'.format(cx))
ax_prof.set_title('Center Profiles (Central 300 Pixels)')
ax_prof.set_xlabel('Pixel position (relative)')
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