|
|
|
""" |
|
Main GUI application for image_postprocess pipeline with camera-simulator controls. |
|
Dark/gray modern theme and collapsible option sections. |
|
""" |
|
|
|
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 |
|
) |
|
from PyQt5.QtCore import Qt |
|
from PyQt5.QtGui import QPixmap, QPalette, QColor |
|
from worker import Worker |
|
from analysis_panel import AnalysisPanel |
|
from utils import qpixmap_from_path |
|
|
|
try: |
|
from image_postprocess import process_image |
|
except Exception as e: |
|
process_image = None |
|
IMPORT_ERROR = str(e) |
|
else: |
|
IMPORT_ERROR = None |
|
|
|
|
|
class CollapsibleBox(QWidget): |
|
"""A simple collapsible container widget with a chevron arrow.""" |
|
def __init__(self, title: str = "", parent=None): |
|
super().__init__(parent) |
|
self.toggle = QToolButton(text=title, checkable=True, checked=True) |
|
self.toggle.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) |
|
self.toggle.setArrowType(Qt.DownArrow) |
|
self.toggle.clicked.connect(self.on_toggled) |
|
self.toggle.setStyleSheet("QToolButton { border: none; font-weight:600; padding:6px; }") |
|
|
|
self.content = QWidget() |
|
self.content_layout = QVBoxLayout() |
|
self.content_layout.setContentsMargins(8, 4, 8, 8) |
|
self.content.setLayout(self.content_layout) |
|
|
|
lay = QVBoxLayout(self) |
|
lay.setSpacing(0) |
|
lay.setContentsMargins(0, 0, 0, 0) |
|
lay.addWidget(self.toggle) |
|
lay.addWidget(self.content) |
|
|
|
def on_toggled(self): |
|
checked = self.toggle.isChecked() |
|
self.toggle.setArrowType(Qt.DownArrow if checked else Qt.RightArrow) |
|
self.content.setVisible(checked) |
|
|
|
|
|
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_v = QVBoxLayout() |
|
main_h.addLayout(left_v, 2) |
|
|
|
|
|
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) |
|
|
|
|
|
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_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_v = QVBoxLayout() |
|
main_h.addLayout(right_v, 3) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
self.tile_spin = QSpinBox() |
|
self.tile_spin.setRange(1, 64) |
|
self.tile_spin.setValue(8) |
|
params_layout.addRow("CLAHE tile", self.tile_spin) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
self.bayer_chk = QCheckBox("Enable Bayer / demosaic (RGGB)") |
|
self.bayer_chk.setChecked(True) |
|
cam_layout.addRow(self.bayer_chk) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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 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()) |
|
|
|
|
|
if self.awb_chk.isChecked(): |
|
args.ref = awb_ref_val |
|
else: |
|
args.ref = None |
|
|
|
|
|
args.fft_ref = fft_ref_val |
|
|
|
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}"') |
|
|
|
|
|
def apply_dark_palette(app: QApplication): |
|
pal = QPalette() |
|
|
|
pal.setColor(QPalette.Window, QColor(18, 18, 19)) |
|
pal.setColor(QPalette.WindowText, QColor(220, 220, 220)) |
|
pal.setColor(QPalette.Base, QColor(28, 28, 30)) |
|
pal.setColor(QPalette.AlternateBase, QColor(24, 24, 26)) |
|
pal.setColor(QPalette.ToolTipBase, QColor(220, 220, 220)) |
|
pal.setColor(QPalette.ToolTipText, QColor(220, 220, 220)) |
|
pal.setColor(QPalette.Text, QColor(230, 230, 230)) |
|
pal.setColor(QPalette.Button, QColor(40, 40, 42)) |
|
pal.setColor(QPalette.ButtonText, QColor(230, 230, 230)) |
|
pal.setColor(QPalette.Highlight, QColor(70, 130, 180)) |
|
pal.setColor(QPalette.HighlightedText, QColor(255, 255, 255)) |
|
app.setPalette(pal) |
|
|
|
|
|
app.setStyleSheet(r""" |
|
QWidget { font-family: 'Segoe UI', Roboto, Arial, sans-serif; font-size:11pt } |
|
QToolButton { padding:6px; } |
|
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox { background: #1e1e1f; border: 1px solid #333; padding:4px; border-radius:6px } |
|
QPushButton { background: #2a2a2c; border: 1px solid #3a3a3c; padding:6px 10px; border-radius:8px } |
|
QPushButton:hover { background: #333336 } |
|
QPushButton:pressed { background: #232325 } |
|
QProgressBar { background: #222; border: 1px solid #333; border-radius:6px; text-align:center } |
|
QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4b9bd6, stop:1 #3b83c0); } |
|
QLabel { color: #d6d6d6 } |
|
QCheckBox { padding:4px } |
|
""") |
|
|
|
|
|
def main(): |
|
app = QApplication([]) |
|
apply_dark_palette(app) |
|
if IMPORT_ERROR: |
|
QMessageBox.critical(None, "Import error", "Could not import image_postprocess module:\n" + IMPORT_ERROR) |
|
w = MainWindow() |
|
w.show() |
|
sys.exit(app.exec_()) |
|
|
|
|
|
if __name__ == '__main__': |
|
main() |
|
|