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