File size: 6,721 Bytes
128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c 128f42e 476915c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
#!/usr/bin/env python3
"""
Analysis panel for histogram, FFT, and radial profile plots.
Designed to plug straight into the provided run.py / MainWindow.
Exposes AnalysisPanel(title: str) with method update_from_path(path)
and clear_plots(). Uses helpers from utils:
- compute_gray_array(path) -> 2D numpy.ndarray (grayscale 0-255)
- compute_fft_magnitude(gray) -> (mag, mag_log)
- radial_profile(mag) -> (centers, radial)
- make_canvas(width, height) -> (FigureCanvas, Axes)
This module is intentionally defensive (catches errors) and keeps
its own layout compact so it will fit in the scrollable right-hand
panel in MainWindow.
"""
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QSizePolicy, QLabel
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import numpy as np
import os
from utils import compute_gray_array, compute_fft_magnitude, radial_profile, make_canvas
class AnalysisPanel(QWidget):
def __init__(self, title: str = "Analysis", parent=None):
super().__init__(parent)
self.setMinimumHeight(220)
# Top-level layout + framed group
v = QVBoxLayout(self)
box = QGroupBox(title)
vbox = QVBoxLayout()
box.setLayout(vbox)
# Row of three matplotlib canvases
row = QHBoxLayout()
# create canvases using project's make_canvas helper so styles match
self.hist_canvas, self.hist_ax = make_canvas(width=3, height=2)
self.fft_canvas, self.fft_ax = make_canvas(width=3, height=2)
self.radial_canvas, self.radial_ax = make_canvas(width=3, height=2)
for c in (self.hist_canvas, self.fft_canvas, self.radial_canvas):
c.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# give figures a consistent, compact margin so they sit well inside the GroupBox
try:
c.figure.subplots_adjust(top=0.88, bottom=0.12, left=0.12, right=0.96)
except Exception:
pass
row.addWidget(self.hist_canvas)
row.addWidget(self.fft_canvas)
row.addWidget(self.radial_canvas)
vbox.addLayout(row)
# small status label below canvases for quick diagnostics
self.status_label = QLabel("")
self.status_label.setWordWrap(True)
self.status_label.setVisible(False)
vbox.addWidget(self.status_label)
v.addWidget(box)
def update_from_path(self, path: str):
"""Update all three plots using the image at `path`.
If path is invalid or an error occurs while loading/processing,
plots are cleared and a status message is shown.
"""
if not path or not os.path.exists(path):
self.status_label.setText(f"No image: {path}")
self.status_label.setVisible(True)
self.clear_plots()
return
try:
gray = compute_gray_array(path)
if gray is None:
raise ValueError("compute_gray_array returned None")
# ensure grayscale array is 2D and finite
gray = np.asarray(gray)
if gray.ndim != 2:
raise ValueError("expected 2D grayscale array")
except Exception as e:
self.status_label.setText(f"Failed to load image: {e}")
self.status_label.setVisible(True)
self.clear_plots()
return
# Good: hide status
self.status_label.setVisible(False)
# -------------------- Histogram --------------------
try:
self.hist_ax.cla()
self.hist_ax.set_title('Grayscale histogram')
self.hist_ax.set_xlabel('Intensity')
self.hist_ax.set_ylabel('Count')
# ensure int range 0..255
flat = gray.ravel()
# handle float data by scaling if necessary
if flat.dtype.kind == 'f' and flat.max() <= 1.0:
flat = (flat * 255.0).astype(np.uint8)
self.hist_ax.hist(flat, bins=256, range=(0, 255))
self.hist_canvas.draw()
except Exception as e:
self.hist_ax.cla()
self.hist_canvas.draw()
self.status_label.setText(f"Histogram error: {e}")
self.status_label.setVisible(True)
# -------------------- FFT magnitude --------------------
try:
mag, mag_log = compute_fft_magnitude(gray)
if mag_log is None:
raise ValueError("compute_fft_magnitude returned None")
self.fft_ax.cla()
self.fft_ax.set_title('FFT magnitude (log)')
# use imshow with origin='lower' so low-frequencies sit near the centre visually
self.fft_ax.imshow(mag_log, origin='lower', aspect='auto')
self.fft_ax.set_xticks([])
self.fft_ax.set_yticks([])
# leave some room for colorbar in wider layouts (MainWindow adjusts overall sizes)
try:
self.fft_canvas.figure.subplots_adjust(right=0.92)
except Exception:
pass
self.fft_canvas.draw()
except Exception as e:
self.fft_ax.cla()
self.fft_canvas.draw()
self.status_label.setText(f"FFT error: {e}")
self.status_label.setVisible(True)
# -------------------- Radial profile --------------------
try:
centers, radial = radial_profile(mag)
if centers is None or radial is None:
raise ValueError("radial_profile returned invalid data")
self.radial_ax.cla()
self.radial_ax.set_title('Radial freq profile')
self.radial_ax.set_xlabel('Normalized radius')
self.radial_ax.set_ylabel('Mean magnitude')
self.radial_ax.plot(centers, radial)
self.radial_canvas.draw()
except Exception as e:
self.radial_ax.cla()
self.radial_canvas.draw()
self.status_label.setText(f"Radial profile error: {e}")
self.status_label.setVisible(True)
def clear_plots(self):
"""Clear all axes and redraw empty canvases."""
for ax, canvas in ((self.hist_ax, self.hist_canvas), (self.fft_ax, self.fft_canvas), (self.radial_ax, self.radial_canvas)):
try:
ax.cla()
# give an empty centered message on histogram axis only
if ax is self.hist_ax:
ax.text(0.5, 0.5, 'No image', horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
canvas.draw()
except Exception:
pass
|