deepfake-detection-bypass / analysis_panel.py
Fioceen's picture
Added LUT
476915c
#!/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