deepfake-detection-bypass / main_window.py
Fioceen's picture
File modularization
2a8ed16
#!/usr/bin/env python3
"""
MainWindow definition extracted from the original single-file GUI.
All GUI wiring, widgets, and the MainWindow class live here.
"""
import sys
import os
from pathlib import Path
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QLabel, QPushButton, QFileDialog,
QHBoxLayout, QVBoxLayout, QFormLayout, QSlider, QSpinBox, QDoubleSpinBox,
QProgressBar, QMessageBox, QLineEdit, QComboBox, QCheckBox, QToolButton, QScrollArea
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from worker import Worker
from analysis_panel import AnalysisPanel
from utils import qpixmap_from_path
from collapsible_box import CollapsibleBox
from theme import apply_dark_palette
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Image Postprocess β€” GUI (Camera Simulator)")
self.setMinimumSize(1200, 760)
central = QWidget()
self.setCentralWidget(central)
main_h = QHBoxLayout(central)
# Left: previews & file selection
left_v = QVBoxLayout()
main_h.addLayout(left_v, 2)
# Input/Output collapsible
io_box = CollapsibleBox("Input / Output")
left_v.addWidget(io_box)
in_layout = QFormLayout()
io_container = QWidget()
io_container.setLayout(in_layout)
io_box.content_layout.addWidget(io_container)
self.input_line = QLineEdit()
self.input_btn = QPushButton("Choose Input")
self.input_btn.clicked.connect(self.choose_input)
self.ref_line = QLineEdit()
self.ref_btn = QPushButton("Choose AWB Reference (optional)")
self.ref_btn.clicked.connect(self.choose_ref)
self.fft_ref_line = QLineEdit()
self.fft_ref_btn = QPushButton("Choose FFT Reference (optional)")
self.fft_ref_btn.clicked.connect(self.choose_fft_ref)
self.output_line = QLineEdit()
self.output_btn = QPushButton("Choose Output")
self.output_btn.clicked.connect(self.choose_output)
in_layout.addRow(self.input_btn, self.input_line)
in_layout.addRow(self.ref_btn, self.ref_line)
in_layout.addRow(self.fft_ref_btn, self.fft_ref_line)
in_layout.addRow(self.output_btn, self.output_line)
# Previews
self.preview_in = QLabel(alignment=Qt.AlignCenter)
self.preview_in.setFixedSize(480, 300)
self.preview_in.setStyleSheet("background:#121213; border:1px solid #2b2b2b; color:#ddd; border-radius:6px")
self.preview_in.setText("Input preview")
self.preview_out = QLabel(alignment=Qt.AlignCenter)
self.preview_out.setFixedSize(480, 300)
self.preview_out.setStyleSheet("background:#121213; border:1px solid #2b2b2b; color:#ddd; border-radius:6px")
self.preview_out.setText("Output preview")
left_v.addWidget(self.preview_in)
left_v.addWidget(self.preview_out)
# Actions
actions_h = QHBoxLayout()
self.run_btn = QPushButton("Run β€” Process Image")
self.run_btn.clicked.connect(self.on_run)
self.open_out_btn = QPushButton("Open Output Folder")
self.open_out_btn.clicked.connect(self.open_output_folder)
actions_h.addWidget(self.run_btn)
actions_h.addWidget(self.open_out_btn)
left_v.addLayout(actions_h)
self.progress = QProgressBar()
self.progress.setTextVisible(True)
self.progress.setRange(0, 100)
self.progress.setValue(0)
left_v.addWidget(self.progress)
# Right: controls + analysis panels (with scroll area)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("QScrollArea { border: none; }")
main_h.addWidget(scroll_area, 3)
scroll_widget = QWidget()
right_v = QVBoxLayout(scroll_widget)
scroll_area.setWidget(scroll_widget)
# Auto Mode toggle (keeps top-level quick switch visible)
self.auto_mode_chk = QCheckBox("Enable Auto Mode")
self.auto_mode_chk.setChecked(False)
self.auto_mode_chk.stateChanged.connect(self._on_auto_mode_toggled)
right_v.addWidget(self.auto_mode_chk)
# Make Auto Mode section collapsible
self.auto_box = CollapsibleBox("Auto Mode")
right_v.addWidget(self.auto_box)
auto_layout = QFormLayout()
auto_container = QWidget()
auto_container.setLayout(auto_layout)
self.auto_box.content_layout.addWidget(auto_container)
strength_layout = QHBoxLayout()
self.strength_slider = QSlider(Qt.Horizontal)
self.strength_slider.setRange(0, 100)
self.strength_slider.setValue(25)
self.strength_slider.valueChanged.connect(self._update_strength_label)
self.strength_label = QLabel("25")
self.strength_label.setFixedWidth(30)
strength_layout.addWidget(self.strength_slider)
strength_layout.addWidget(self.strength_label)
auto_layout.addRow("Aberration Strength", strength_layout)
# Parameters (Manual Mode) collapsible
self.params_box = CollapsibleBox("Parameters (Manual Mode)")
right_v.addWidget(self.params_box)
params_layout = QFormLayout()
params_container = QWidget()
params_container.setLayout(params_layout)
self.params_box.content_layout.addWidget(params_container)
# Noise-std
self.noise_spin = QDoubleSpinBox()
self.noise_spin.setRange(0.0, 0.1)
self.noise_spin.setSingleStep(0.001)
self.noise_spin.setValue(0.02)
self.noise_spin.setToolTip("Gaussian noise std fraction of 255")
params_layout.addRow("Noise std (0-0.1)", self.noise_spin)
# CLAHE-clip
self.clahe_spin = QDoubleSpinBox()
self.clahe_spin.setRange(0.1, 10.0)
self.clahe_spin.setSingleStep(0.1)
self.clahe_spin.setValue(2.0)
params_layout.addRow("CLAHE clip", self.clahe_spin)
# Tile
self.tile_spin = QSpinBox()
self.tile_spin.setRange(1, 64)
self.tile_spin.setValue(8)
params_layout.addRow("CLAHE tile", self.tile_spin)
# Cutoff
self.cutoff_spin = QDoubleSpinBox()
self.cutoff_spin.setRange(0.01, 1.0)
self.cutoff_spin.setSingleStep(0.01)
self.cutoff_spin.setValue(0.25)
params_layout.addRow("Fourier cutoff (0-1)", self.cutoff_spin)
# Fstrength
self.fstrength_spin = QDoubleSpinBox()
self.fstrength_spin.setRange(0.0, 1.0)
self.fstrength_spin.setSingleStep(0.01)
self.fstrength_spin.setValue(0.9)
params_layout.addRow("Fourier strength (0-1)", self.fstrength_spin)
# Randomness
self.randomness_spin = QDoubleSpinBox()
self.randomness_spin.setRange(0.0, 1.0)
self.randomness_spin.setSingleStep(0.01)
self.randomness_spin.setValue(0.05)
params_layout.addRow("Fourier randomness", self.randomness_spin)
# Phase_perturb
self.phase_perturb_spin = QDoubleSpinBox()
self.phase_perturb_spin.setRange(0.0, 1.0)
self.phase_perturb_spin.setSingleStep(0.001)
self.phase_perturb_spin.setValue(0.08)
self.phase_perturb_spin.setToolTip("Phase perturbation std (radians)")
params_layout.addRow("Phase perturb (rad)", self.phase_perturb_spin)
# Radial_smooth
self.radial_smooth_spin = QSpinBox()
self.radial_smooth_spin.setRange(0, 50)
self.radial_smooth_spin.setValue(5)
params_layout.addRow("Radial smooth (bins)", self.radial_smooth_spin)
# FFT_mode
self.fft_mode_combo = QComboBox()
self.fft_mode_combo.addItems(["auto", "ref", "model"])
self.fft_mode_combo.setCurrentText("auto")
params_layout.addRow("FFT mode", self.fft_mode_combo)
# FFT_alpha
self.fft_alpha_spin = QDoubleSpinBox()
self.fft_alpha_spin.setRange(0.1, 4.0)
self.fft_alpha_spin.setSingleStep(0.1)
self.fft_alpha_spin.setValue(1.0)
self.fft_alpha_spin.setToolTip("Alpha exponent for 1/f model when using model mode")
params_layout.addRow("FFT alpha (model)", self.fft_alpha_spin)
# Perturb
self.perturb_spin = QDoubleSpinBox()
self.perturb_spin.setRange(0.0, 0.05)
self.perturb_spin.setSingleStep(0.001)
self.perturb_spin.setValue(0.008)
params_layout.addRow("Pixel perturb", self.perturb_spin)
# Seed
self.seed_spin = QSpinBox()
self.seed_spin.setRange(0, 2 ** 31 - 1)
self.seed_spin.setValue(0)
params_layout.addRow("Seed (0=none)", self.seed_spin)
# AWB checkbox
self.awb_chk = QCheckBox("Enable auto white-balance (AWB)")
self.awb_chk.setChecked(False)
self.awb_chk.setToolTip("If checked, AWB is applied. If a reference image is chosen, it will be used; otherwise gray-world AWB is applied.")
params_layout.addRow(self.awb_chk)
# Camera simulator toggle
self.sim_camera_chk = QCheckBox("Enable camera pipeline simulation")
self.sim_camera_chk.setChecked(False)
self.sim_camera_chk.stateChanged.connect(self._on_sim_camera_toggled)
params_layout.addRow(self.sim_camera_chk)
# --- LUT support UI ---
self.lut_chk = QCheckBox("Enable LUT")
self.lut_chk.setChecked(False)
self.lut_chk.setToolTip("Enable applying a 1D/.npy/.cube LUT to the output image")
self.lut_chk.stateChanged.connect(self._on_lut_toggled)
params_layout.addRow(self.lut_chk)
# LUT chooser (hidden until checkbox checked)
self.lut_line = QLineEdit()
self.lut_btn = QPushButton("Choose LUT")
self.lut_btn.clicked.connect(self.choose_lut)
lut_box = QWidget()
lut_box_layout = QHBoxLayout()
lut_box_layout.setContentsMargins(0, 0, 0, 0)
lut_box.setLayout(lut_box_layout)
lut_box_layout.addWidget(self.lut_line)
lut_box_layout.addWidget(self.lut_btn)
self.lut_file_label = QLabel("LUT file (png/.npy/.cube)")
params_layout.addRow(self.lut_file_label, lut_box)
self.lut_strength_spin = QDoubleSpinBox()
self.lut_strength_spin.setRange(0.0, 1.0)
self.lut_strength_spin.setSingleStep(0.01)
self.lut_strength_spin.setValue(1.0)
self.lut_strength_spin.setToolTip("Blend strength for LUT (0.0 = no effect, 1.0 = full LUT)")
self.lut_strength_label = QLabel("LUT strength")
params_layout.addRow(self.lut_strength_label, self.lut_strength_spin)
# Initially hide LUT controls and their labels
self.lut_file_label.setVisible(False)
lut_box.setVisible(False)
self.lut_strength_label.setVisible(False)
self.lut_strength_spin.setVisible(False)
# Store all widgets that need their visibility toggled
self._lut_controls = (self.lut_file_label, lut_box, self.lut_strength_label, self.lut_strength_spin)
# Camera simulator collapsible group
self.camera_box = CollapsibleBox("Camera simulator options")
right_v.addWidget(self.camera_box)
cam_layout = QFormLayout()
cam_container = QWidget()
cam_container.setLayout(cam_layout)
self.camera_box.content_layout.addWidget(cam_container)
# Enable bayer
self.bayer_chk = QCheckBox("Enable Bayer / demosaic (RGGB)")
self.bayer_chk.setChecked(True)
cam_layout.addRow(self.bayer_chk)
# JPEG cycles
self.jpeg_cycles_spin = QSpinBox()
self.jpeg_cycles_spin.setRange(0, 10)
self.jpeg_cycles_spin.setValue(1)
cam_layout.addRow("JPEG cycles", self.jpeg_cycles_spin)
# JPEG quality min/max
self.jpeg_qmin_spin = QSpinBox()
self.jpeg_qmin_spin.setRange(1, 100)
self.jpeg_qmin_spin.setValue(88)
self.jpeg_qmax_spin = QSpinBox()
self.jpeg_qmax_spin.setRange(1, 100)
self.jpeg_qmax_spin.setValue(96)
qbox = QHBoxLayout()
qbox.addWidget(self.jpeg_qmin_spin)
qbox.addWidget(QLabel("to"))
qbox.addWidget(self.jpeg_qmax_spin)
cam_layout.addRow("JPEG quality (min to max)", qbox)
# Vignette strength
self.vignette_spin = QDoubleSpinBox()
self.vignette_spin.setRange(0.0, 1.0)
self.vignette_spin.setSingleStep(0.01)
self.vignette_spin.setValue(0.35)
cam_layout.addRow("Vignette strength", self.vignette_spin)
# Chromatic aberration strength
self.chroma_spin = QDoubleSpinBox()
self.chroma_spin.setRange(0.0, 10.0)
self.chroma_spin.setSingleStep(0.1)
self.chroma_spin.setValue(1.2)
cam_layout.addRow("Chromatic aberration (px)", self.chroma_spin)
# ISO scale
self.iso_spin = QDoubleSpinBox()
self.iso_spin.setRange(0.1, 16.0)
self.iso_spin.setSingleStep(0.1)
self.iso_spin.setValue(1.0)
cam_layout.addRow("ISO/exposure scale", self.iso_spin)
# Read noise
self.read_noise_spin = QDoubleSpinBox()
self.read_noise_spin.setRange(0.0, 50.0)
self.read_noise_spin.setSingleStep(0.1)
self.read_noise_spin.setValue(2.0)
cam_layout.addRow("Read noise (DN)", self.read_noise_spin)
# Hot pixel prob
self.hot_pixel_spin = QDoubleSpinBox()
self.hot_pixel_spin.setDecimals(9)
self.hot_pixel_spin.setRange(0.0, 1.0)
self.hot_pixel_spin.setSingleStep(1e-6)
self.hot_pixel_spin.setValue(1e-6)
cam_layout.addRow("Hot pixel prob", self.hot_pixel_spin)
# Banding strength
self.banding_spin = QDoubleSpinBox()
self.banding_spin.setRange(0.0, 1.0)
self.banding_spin.setSingleStep(0.01)
self.banding_spin.setValue(0.0)
cam_layout.addRow("Banding strength", self.banding_spin)
# Motion blur kernel
self.motion_blur_spin = QSpinBox()
self.motion_blur_spin.setRange(1, 51)
self.motion_blur_spin.setValue(1)
cam_layout.addRow("Motion blur kernel", self.motion_blur_spin)
self.camera_box.setVisible(False)
self.ref_hint = QLabel("AWB uses the 'AWB reference' chooser. FFT spectral matching uses the 'FFT Reference' chooser.")
right_v.addWidget(self.ref_hint)
self.analysis_input = AnalysisPanel(title="Input analysis")
self.analysis_output = AnalysisPanel(title="Output analysis")
right_v.addWidget(self.analysis_input)
right_v.addWidget(self.analysis_output)
right_v.addStretch(1)
# Status bar
self.status = QLabel("Ready")
self.status.setStyleSheet("color:#bdbdbd;padding:6px")
self.status.setAlignment(Qt.AlignLeft)
self.status.setFixedHeight(28)
self.status.setContentsMargins(6, 6, 6, 6)
self.statusBar().addWidget(self.status)
self.worker = None
self._on_auto_mode_toggled(self.auto_mode_chk.checkState())
def _on_sim_camera_toggled(self, state):
enabled = state == Qt.Checked
self.camera_box.setVisible(enabled)
def _on_auto_mode_toggled(self, state):
is_auto = (state == Qt.Checked)
self.auto_box.setVisible(is_auto)
self.params_box.setVisible(not is_auto)
def _update_strength_label(self, value):
self.strength_label.setText(str(value))
def choose_input(self):
path, _ = QFileDialog.getOpenFileName(self, "Choose input image", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp *.tif)")
if path:
self.input_line.setText(path)
self.load_preview(self.preview_in, path)
self.analysis_input.update_from_path(path)
out_suggest = str(Path(path).with_name(Path(path).stem + "_out" + Path(path).suffix))
if not self.output_line.text():
self.output_line.setText(out_suggest)
def choose_ref(self):
path, _ = QFileDialog.getOpenFileName(self, "Choose AWB reference image", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp *.tif)")
if path:
self.ref_line.setText(path)
def choose_fft_ref(self):
path, _ = QFileDialog.getOpenFileName(self, "Choose FFT reference image", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp *.tif)")
if path:
self.fft_ref_line.setText(path)
def choose_output(self):
path, _ = QFileDialog.getSaveFileName(self, "Choose output path", str(Path.home()), "JPEG (*.jpg *.jpeg);;PNG (*.png);;TIFF (*.tif)")
if path:
self.output_line.setText(path)
def choose_lut(self):
path, _ = QFileDialog.getOpenFileName(self, "Choose LUT file", str(Path.home()), "LUTs (*.png *.npy *.cube);;All files (*)")
if path:
self.lut_line.setText(path)
def _on_lut_toggled(self, state):
visible = (state == Qt.Checked)
for w in self._lut_controls:
w.setVisible(visible)
def load_preview(self, widget: QLabel, path: str):
if not path or not os.path.exists(path):
widget.setText("No image")
widget.setPixmap(QPixmap())
return
pix = qpixmap_from_path(path, max_size=(widget.width(), widget.height()))
widget.setPixmap(pix)
def set_enabled_all(self, enabled: bool):
for w in self.findChildren((QPushButton, QDoubleSpinBox, QSpinBox, QLineEdit, QComboBox, QCheckBox, QSlider, QToolButton)):
w.setEnabled(enabled)
def on_run(self):
from types import SimpleNamespace
inpath = self.input_line.text().strip()
outpath = self.output_line.text().strip()
if not inpath or not os.path.exists(inpath):
QMessageBox.warning(self, "Missing input", "Please choose a valid input image.")
return
if not outpath:
QMessageBox.warning(self, "Missing output", "Please choose an output path.")
return
awb_ref_val = self.ref_line.text() or None
fft_ref_val = self.fft_ref_line.text() or None
args = SimpleNamespace()
if self.auto_mode_chk.isChecked():
strength = self.strength_slider.value() / 100.0
args.noise_std = strength * 0.04
args.clahe_clip = 1.0 + strength * 3.0
args.cutoff = max(0.01, 0.4 - strength * 0.3)
args.fstrength = strength * 0.95
args.phase_perturb = strength * 0.1
args.perturb = strength * 0.015
args.jpeg_cycles = int(strength * 2)
args.jpeg_qmin = max(1, int(95 - strength * 35))
args.jpeg_qmax = max(1, int(99 - strength * 25))
args.vignette_strength = strength * 0.6
args.chroma_strength = strength * 4.0
args.motion_blur_kernel = 1 + 2 * int(strength * 6)
args.banding_strength = strength * 0.1
args.tile = 8
args.randomness = 0.05
args.radial_smooth = 5
args.fft_mode = "auto"
args.fft_alpha = 1.0
args.alpha = 1.0
seed_val = int(self.seed_spin.value())
args.seed = None if seed_val == 0 else seed_val
args.sim_camera = bool(self.sim_camera_chk.isChecked())
args.no_no_bayer = True
args.iso_scale = 1.0
args.read_noise = 2.0
args.hot_pixel_prob = 1e-6
else:
seed_val = int(self.seed_spin.value())
args.seed = None if seed_val == 0 else seed_val
sim_camera = bool(self.sim_camera_chk.isChecked())
enable_bayer = bool(self.bayer_chk.isChecked())
args.noise_std = float(self.noise_spin.value())
args.clahe_clip = float(self.clahe_spin.value())
args.tile = int(self.tile_spin.value())
args.cutoff = float(self.cutoff_spin.value())
args.fstrength = float(self.fstrength_spin.value())
args.strength = float(self.fstrength_spin.value())
args.randomness = float(self.randomness_spin.value())
args.phase_perturb = float(self.phase_perturb_spin.value())
args.perturb = float(self.perturb_spin.value())
args.fft_mode = self.fft_mode_combo.currentText()
args.fft_alpha = float(self.fft_alpha_spin.value())
args.alpha = float(self.fft_alpha_spin.value())
args.radial_smooth = int(self.radial_smooth_spin.value())
args.sim_camera = sim_camera
args.no_no_bayer = bool(enable_bayer)
args.jpeg_cycles = int(self.jpeg_cycles_spin.value())
args.jpeg_qmin = int(self.jpeg_qmin_spin.value())
args.jpeg_qmax = int(self.jpeg_qmax_spin.value())
args.vignette_strength = float(self.vignette_spin.value())
args.chroma_strength = float(self.chroma_spin.value())
args.iso_scale = float(self.iso_spin.value())
args.read_noise = float(self.read_noise_spin.value())
args.hot_pixel_prob = float(self.hot_pixel_spin.value())
args.banding_strength = float(self.banding_spin.value())
args.motion_blur_kernel = int(self.motion_blur_spin.value())
# AWB handling to match the new --awb flag in the backend
if self.awb_chk.isChecked():
args.awb = True
args.ref = awb_ref_val # This can be the path or None (for grey-world)
else:
args.awb = False
args.ref = None
# FFT spectral matching reference
args.fft_ref = fft_ref_val
# LUT handling: only include if LUT checkbox is checked and a path is provided
if self.lut_chk.isChecked():
lut_path = self.lut_line.text().strip()
args.lut = lut_path if lut_path else None
args.lut_strength = float(self.lut_strength_spin.value())
else:
args.lut = None
args.lut_strength = 1.0
self.worker = Worker(inpath, outpath, args)
self.worker.finished.connect(self.on_finished)
self.worker.error.connect(self.on_error)
self.worker.started.connect(lambda: self.on_worker_started())
self.worker.start()
self.progress.setRange(0, 0)
self.status.setText("Processing...")
self.set_enabled_all(False)
def on_worker_started(self):
pass
def on_finished(self, outpath):
self.progress.setRange(0, 100)
self.progress.setValue(100)
self.status.setText("Done β€” saved to: " + outpath)
self.load_preview(self.preview_out, outpath)
self.analysis_output.update_from_path(outpath)
self.set_enabled_all(True)
def on_error(self, msg, traceback_text):
from PyQt5.QtWidgets import QDialog, QTextEdit
self.progress.setRange(0, 100)
self.progress.setValue(0)
self.status.setText("Error")
dialog = QDialog(self)
dialog.setWindowTitle("Processing Error")
dialog.setMinimumSize(700, 480)
layout = QVBoxLayout(dialog)
error_label = QLabel(f"Error: {msg}")
error_label.setWordWrap(True)
layout.addWidget(error_label)
traceback_edit = QTextEdit()
traceback_edit.setReadOnly(True)
traceback_edit.setText(traceback_text)
traceback_edit.setStyleSheet("font-family: monospace; font-size: 12px;")
layout.addWidget(traceback_edit)
ok_button = QPushButton("OK")
ok_button.clicked.connect(dialog.accept)
layout.addWidget(ok_button)
dialog.exec_()
self.set_enabled_all(True)
def open_output_folder(self):
out = self.output_line.text().strip()
if not out:
QMessageBox.information(self, "No output", "No output path set yet.")
return
folder = os.path.dirname(os.path.abspath(out))
if not os.path.exists(folder):
QMessageBox.warning(self, "Not found", "Output folder does not exist: " + folder)
return
if sys.platform.startswith('darwin'):
os.system(f'open "{folder}"')
elif os.name == 'nt':
os.startfile(folder)
else:
os.system(f'xdg-open "{folder}"')