| | |
| | """ |
| | 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) |
| |
|
| | |
| | v = QVBoxLayout(self) |
| | box = QGroupBox(title) |
| | vbox = QVBoxLayout() |
| | box.setLayout(vbox) |
| |
|
| | |
| | row = QHBoxLayout() |
| |
|
| | |
| | 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) |
| | |
| | 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) |
| |
|
| | |
| | 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") |
| | |
| | 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 |
| |
|
| | |
| | self.status_label.setVisible(False) |
| |
|
| | |
| | try: |
| | self.hist_ax.cla() |
| | self.hist_ax.set_title('Grayscale histogram') |
| | self.hist_ax.set_xlabel('Intensity') |
| | self.hist_ax.set_ylabel('Count') |
| | |
| | flat = gray.ravel() |
| | |
| | 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) |
| |
|
| | |
| | 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)') |
| | |
| | self.fft_ax.imshow(mag_log, origin='lower', aspect='auto') |
| | self.fft_ax.set_xticks([]) |
| | self.fft_ax.set_yticks([]) |
| | |
| | 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) |
| |
|
| | |
| | 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() |
| | |
| | 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 |
| |
|