Fioceen commited on
Commit
fe2e16c
·
1 Parent(s): 4c76d26

GUI Improvement and Auto AWB Addition

Browse files
.vscode/settings.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "python-envs.defaultEnvManager": "ms-python.python:conda",
3
+ "python-envs.defaultPackageManager": "ms-python.python:conda",
4
+ "python-envs.pythonProjects": []
5
+ }
collapsible_box.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt5.QtWidgets import QWidget, QToolButton, QVBoxLayout
2
+ from PyQt5.QtCore import Qt
3
+
4
+ class CollapsibleBox(QWidget):
5
+ """A simple collapsible container widget with a chevron arrow."""
6
+ def __init__(self, title: str = "", parent=None):
7
+ super().__init__(parent)
8
+ self.toggle = QToolButton(text=title, checkable=True, checked=True)
9
+ self.toggle.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
10
+ self.toggle.setArrowType(Qt.DownArrow)
11
+ self.toggle.clicked.connect(self.on_toggled)
12
+ self.toggle.setStyleSheet("QToolButton { border: none; font-weight:600; padding:6px; }")
13
+
14
+ self.content = QWidget()
15
+ self.content_layout = QVBoxLayout()
16
+ self.content_layout.setContentsMargins(8, 4, 8, 8)
17
+ self.content.setLayout(self.content_layout)
18
+
19
+ lay = QVBoxLayout(self)
20
+ lay.setSpacing(0)
21
+ lay.setContentsMargins(0, 0, 0, 0)
22
+ lay.addWidget(self.toggle)
23
+ lay.addWidget(self.content)
24
+
25
+ def on_toggled(self):
26
+ checked = self.toggle.isChecked()
27
+ self.toggle.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
28
+ self.content.setVisible(checked)
image_postprocess/image_postprocess_with_camera_pipeline.py CHANGED
@@ -14,8 +14,13 @@ import numpy as np
14
  import piexif
15
  from datetime import datetime
16
 
17
-
18
- from .utils import add_gaussian_noise, clahe_color_correction, randomized_perturbation, fourier_match_spectrum
 
 
 
 
 
19
  from .camera_pipeline import simulate_camera_pipeline
20
 
21
  def add_fake_exif():
@@ -51,18 +56,32 @@ def add_fake_exif():
51
 
52
  def process_image(path_in, path_out, args):
53
  img = Image.open(path_in).convert('RGB')
54
- # img = remove_exif_pil(img) # <-- This line is removed
55
-
56
  arr = np.array(img)
57
 
 
 
 
 
 
 
 
 
 
 
58
  arr = clahe_color_correction(arr, clip_limit=args.clahe_clip, tile_grid_size=(args.tile, args.tile))
59
 
60
- ref_arr = None
 
61
  if args.fft_ref:
62
- ref_img = Image.open(args.fft_ref).convert('RGB')
63
- ref_arr = np.array(ref_img)
64
-
65
- arr = fourier_match_spectrum(arr, ref_img_arr=ref_arr, mode=args.fft_mode,
 
 
 
 
66
  alpha=args.fft_alpha, cutoff=args.cutoff,
67
  strength=args.fstrength, randomness=args.randomness,
68
  phase_perturb=args.phase_perturb, radial_smooth=args.radial_smooth,
@@ -96,7 +115,7 @@ def build_argparser():
96
  p = argparse.ArgumentParser(description="Image postprocessing pipeline with camera simulation")
97
  p.add_argument('input', help='Input image path')
98
  p.add_argument('output', help='Output image path')
99
- p.add_argument('--ref', help='Optional reference image for color matching (not implemented)', default=None)
100
  p.add_argument('--noise-std', type=float, default=0.02, help='Gaussian noise std fraction of 255 (0-0.1)')
101
  p.add_argument('--clahe-clip', type=float, default=2.0, help='CLAHE clip limit')
102
  p.add_argument('--tile', type=int, default=8, help='CLAHE tile grid size')
@@ -136,4 +155,4 @@ if __name__ == "__main__":
136
  print("Input not found:", args.input)
137
  raise SystemExit(2)
138
  process_image(args.input, args.output, args)
139
- print("Saved:", args.output)
 
14
  import piexif
15
  from datetime import datetime
16
 
17
+ from .utils import (
18
+ add_gaussian_noise,
19
+ clahe_color_correction,
20
+ randomized_perturbation,
21
+ fourier_match_spectrum,
22
+ auto_white_balance_ref, # <-- new import
23
+ )
24
  from .camera_pipeline import simulate_camera_pipeline
25
 
26
  def add_fake_exif():
 
56
 
57
  def process_image(path_in, path_out, args):
58
  img = Image.open(path_in).convert('RGB')
59
+ # input -> numpy array
 
60
  arr = np.array(img)
61
 
62
+ # --- Auto white-balance using reference (if provided) ---
63
+ if args.ref:
64
+ try:
65
+ ref_img_awb = Image.open(args.ref).convert('RGB')
66
+ ref_arr_awb = np.array(ref_img_awb)
67
+ arr = auto_white_balance_ref(arr, ref_arr_awb)
68
+ except Exception as e:
69
+ print(f"Warning: failed to load AWB reference '{args.ref}': {e}. Skipping AWB.")
70
+
71
+ # apply CLAHE color correction (contrast)
72
  arr = clahe_color_correction(arr, clip_limit=args.clahe_clip, tile_grid_size=(args.tile, args.tile))
73
 
74
+ # FFT spectral matching reference (separate flag: --fft-ref)
75
+ ref_arr_fft = None
76
  if args.fft_ref:
77
+ try:
78
+ ref_img_fft = Image.open(args.fft_ref).convert('RGB')
79
+ ref_arr_fft = np.array(ref_img_fft)
80
+ except Exception as e:
81
+ print(f"Warning: failed to load FFT reference '{args.fft_ref}': {e}. Skipping FFT reference matching.")
82
+ ref_arr_fft = None
83
+
84
+ arr = fourier_match_spectrum(arr, ref_img_arr=ref_arr_fft, mode=args.fft_mode,
85
  alpha=args.fft_alpha, cutoff=args.cutoff,
86
  strength=args.fstrength, randomness=args.randomness,
87
  phase_perturb=args.phase_perturb, radial_smooth=args.radial_smooth,
 
115
  p = argparse.ArgumentParser(description="Image postprocessing pipeline with camera simulation")
116
  p.add_argument('input', help='Input image path')
117
  p.add_argument('output', help='Output image path')
118
+ p.add_argument('--ref', help='Optional reference image for auto white-balance (applied before CLAHE)', default=None)
119
  p.add_argument('--noise-std', type=float, default=0.02, help='Gaussian noise std fraction of 255 (0-0.1)')
120
  p.add_argument('--clahe-clip', type=float, default=2.0, help='CLAHE clip limit')
121
  p.add_argument('--tile', type=int, default=8, help='CLAHE tile grid size')
 
155
  print("Input not found:", args.input)
156
  raise SystemExit(2)
157
  process_image(args.input, args.output, args)
158
+ print("Saved:", args.output)
image_postprocess/utils.py CHANGED
@@ -204,4 +204,29 @@ def fourier_match_spectrum(img_arr: np.ndarray,
204
  out[:, :, c] = blended
205
 
206
  out = np.clip(out, 0, 255).astype(np.uint8)
207
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  out[:, :, c] = blended
205
 
206
  out = np.clip(out, 0, 255).astype(np.uint8)
207
+ return out
208
+
209
+ def auto_white_balance_ref(img_arr: np.ndarray, ref_img_arr: np.ndarray = None) -> np.ndarray:
210
+ """
211
+ Auto white-balance correction using a reference image.
212
+ If ref_img_arr is None, uses a gray-world assumption instead.
213
+ """
214
+ img = img_arr.astype(np.float32)
215
+
216
+ if ref_img_arr is not None:
217
+ ref = ref_img_arr.astype(np.float32)
218
+ ref_mean = ref.reshape(-1, 3).mean(axis=0)
219
+ else:
220
+ # Gray-world assumption: target is neutral gray
221
+ ref_mean = np.array([128.0, 128.0, 128.0], dtype=np.float32)
222
+
223
+ img_mean = img.reshape(-1, 3).mean(axis=0)
224
+
225
+ # Avoid divide-by-zero
226
+ eps = 1e-6
227
+ scale = (ref_mean + eps) / (img_mean + eps)
228
+
229
+ corrected = img * scale
230
+ corrected = np.clip(corrected, 0, 255).astype(np.uint8)
231
+
232
+ return corrected
image_postprocess_gui_deprecated.py ADDED
@@ -0,0 +1,607 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Main GUI application for image_postprocess pipeline with camera-simulator controls.
4
+ Dark/gray modern theme and collapsible option sections.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ from pathlib import Path
10
+ from PyQt5.QtWidgets import (
11
+ QApplication, QMainWindow, QWidget, QLabel, QPushButton, QFileDialog,
12
+ QHBoxLayout, QVBoxLayout, QFormLayout, QSlider, QSpinBox, QDoubleSpinBox,
13
+ QProgressBar, QMessageBox, QLineEdit, QComboBox, QCheckBox, QToolButton
14
+ )
15
+ from PyQt5.QtCore import Qt
16
+ from PyQt5.QtGui import QPixmap, QPalette, QColor
17
+ from worker import Worker
18
+ from analysis_panel import AnalysisPanel
19
+ from utils import qpixmap_from_path
20
+
21
+ try:
22
+ from image_postprocess import process_image
23
+ except Exception as e:
24
+ process_image = None
25
+ IMPORT_ERROR = str(e)
26
+ else:
27
+ IMPORT_ERROR = None
28
+
29
+
30
+ class CollapsibleBox(QWidget):
31
+ """A simple collapsible container widget with a chevron arrow."""
32
+ def __init__(self, title: str = "", parent=None):
33
+ super().__init__(parent)
34
+ self.toggle = QToolButton(text=title, checkable=True, checked=True)
35
+ self.toggle.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
36
+ self.toggle.setArrowType(Qt.DownArrow)
37
+ self.toggle.clicked.connect(self.on_toggled)
38
+ self.toggle.setStyleSheet("QToolButton { border: none; font-weight:600; padding:6px; }")
39
+
40
+ self.content = QWidget()
41
+ self.content_layout = QVBoxLayout()
42
+ self.content_layout.setContentsMargins(8, 4, 8, 8)
43
+ self.content.setLayout(self.content_layout)
44
+
45
+ lay = QVBoxLayout(self)
46
+ lay.setSpacing(0)
47
+ lay.setContentsMargins(0, 0, 0, 0)
48
+ lay.addWidget(self.toggle)
49
+ lay.addWidget(self.content)
50
+
51
+ def on_toggled(self):
52
+ checked = self.toggle.isChecked()
53
+ self.toggle.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
54
+ self.content.setVisible(checked)
55
+
56
+
57
+ class MainWindow(QMainWindow):
58
+ def __init__(self):
59
+ super().__init__()
60
+ self.setWindowTitle("Image Postprocess — GUI (Camera Simulator)")
61
+ self.setMinimumSize(1200, 760)
62
+
63
+ central = QWidget()
64
+ self.setCentralWidget(central)
65
+ main_h = QHBoxLayout(central)
66
+
67
+ # Left: previews & file selection
68
+ left_v = QVBoxLayout()
69
+ main_h.addLayout(left_v, 2)
70
+
71
+ # Input/Output collapsible
72
+ io_box = CollapsibleBox("Input / Output")
73
+ left_v.addWidget(io_box)
74
+ in_layout = QFormLayout()
75
+ io_container = QWidget()
76
+ io_container.setLayout(in_layout)
77
+ io_box.content_layout.addWidget(io_container)
78
+
79
+ self.input_line = QLineEdit()
80
+ self.input_btn = QPushButton("Choose Input")
81
+ self.input_btn.clicked.connect(self.choose_input)
82
+
83
+ self.ref_line = QLineEdit()
84
+ self.ref_btn = QPushButton("Choose AWB Reference (optional)")
85
+ self.ref_btn.clicked.connect(self.choose_ref)
86
+
87
+ self.fft_ref_line = QLineEdit()
88
+ self.fft_ref_btn = QPushButton("Choose FFT Reference (optional)")
89
+ self.fft_ref_btn.clicked.connect(self.choose_fft_ref)
90
+
91
+ self.output_line = QLineEdit()
92
+ self.output_btn = QPushButton("Choose Output")
93
+ self.output_btn.clicked.connect(self.choose_output)
94
+
95
+ in_layout.addRow(self.input_btn, self.input_line)
96
+ in_layout.addRow(self.ref_btn, self.ref_line)
97
+ in_layout.addRow(self.fft_ref_btn, self.fft_ref_line)
98
+ in_layout.addRow(self.output_btn, self.output_line)
99
+
100
+ # Previews
101
+ self.preview_in = QLabel(alignment=Qt.AlignCenter)
102
+ self.preview_in.setFixedSize(480, 300)
103
+ self.preview_in.setStyleSheet("background:#121213; border:1px solid #2b2b2b; color:#ddd; border-radius:6px")
104
+ self.preview_in.setText("Input preview")
105
+
106
+ self.preview_out = QLabel(alignment=Qt.AlignCenter)
107
+ self.preview_out.setFixedSize(480, 300)
108
+ self.preview_out.setStyleSheet("background:#121213; border:1px solid #2b2b2b; color:#ddd; border-radius:6px")
109
+ self.preview_out.setText("Output preview")
110
+
111
+ left_v.addWidget(self.preview_in)
112
+ left_v.addWidget(self.preview_out)
113
+
114
+ # Actions
115
+ actions_h = QHBoxLayout()
116
+ self.run_btn = QPushButton("Run — Process Image")
117
+ self.run_btn.clicked.connect(self.on_run)
118
+ self.open_out_btn = QPushButton("Open Output Folder")
119
+ self.open_out_btn.clicked.connect(self.open_output_folder)
120
+ actions_h.addWidget(self.run_btn)
121
+ actions_h.addWidget(self.open_out_btn)
122
+ left_v.addLayout(actions_h)
123
+
124
+ self.progress = QProgressBar()
125
+ self.progress.setTextVisible(True)
126
+ self.progress.setRange(0, 100)
127
+ self.progress.setValue(0)
128
+ left_v.addWidget(self.progress)
129
+
130
+ # Right: controls + analysis panels
131
+ right_v = QVBoxLayout()
132
+ main_h.addLayout(right_v, 3)
133
+
134
+ # Auto Mode toggle (keeps top-level quick switch visible)
135
+ self.auto_mode_chk = QCheckBox("Enable Auto Mode")
136
+ self.auto_mode_chk.setChecked(False)
137
+ self.auto_mode_chk.stateChanged.connect(self._on_auto_mode_toggled)
138
+ right_v.addWidget(self.auto_mode_chk)
139
+
140
+ # Make Auto Mode section collapsible
141
+ self.auto_box = CollapsibleBox("Auto Mode")
142
+ right_v.addWidget(self.auto_box)
143
+ auto_layout = QFormLayout()
144
+ auto_container = QWidget()
145
+ auto_container.setLayout(auto_layout)
146
+ self.auto_box.content_layout.addWidget(auto_container)
147
+
148
+ strength_layout = QHBoxLayout()
149
+ self.strength_slider = QSlider(Qt.Horizontal)
150
+ self.strength_slider.setRange(0, 100)
151
+ self.strength_slider.setValue(25)
152
+ self.strength_slider.valueChanged.connect(self._update_strength_label)
153
+ self.strength_label = QLabel("25")
154
+ self.strength_label.setFixedWidth(30)
155
+ strength_layout.addWidget(self.strength_slider)
156
+ strength_layout.addWidget(self.strength_label)
157
+
158
+ auto_layout.addRow("Aberration Strength", strength_layout)
159
+
160
+ # Parameters (Manual Mode) collapsible
161
+ self.params_box = CollapsibleBox("Parameters (Manual Mode)")
162
+ right_v.addWidget(self.params_box)
163
+ params_layout = QFormLayout()
164
+ params_container = QWidget()
165
+ params_container.setLayout(params_layout)
166
+ self.params_box.content_layout.addWidget(params_container)
167
+
168
+ # Noise-std
169
+ self.noise_spin = QDoubleSpinBox()
170
+ self.noise_spin.setRange(0.0, 0.1)
171
+ self.noise_spin.setSingleStep(0.001)
172
+ self.noise_spin.setValue(0.02)
173
+ self.noise_spin.setToolTip("Gaussian noise std fraction of 255")
174
+ params_layout.addRow("Noise std (0-0.1)", self.noise_spin)
175
+
176
+ # CLAHE-clip
177
+ self.clahe_spin = QDoubleSpinBox()
178
+ self.clahe_spin.setRange(0.1, 10.0)
179
+ self.clahe_spin.setSingleStep(0.1)
180
+ self.clahe_spin.setValue(2.0)
181
+ params_layout.addRow("CLAHE clip", self.clahe_spin)
182
+
183
+ # Tile
184
+ self.tile_spin = QSpinBox()
185
+ self.tile_spin.setRange(1, 64)
186
+ self.tile_spin.setValue(8)
187
+ params_layout.addRow("CLAHE tile", self.tile_spin)
188
+
189
+ # Cutoff
190
+ self.cutoff_spin = QDoubleSpinBox()
191
+ self.cutoff_spin.setRange(0.01, 1.0)
192
+ self.cutoff_spin.setSingleStep(0.01)
193
+ self.cutoff_spin.setValue(0.25)
194
+ params_layout.addRow("Fourier cutoff (0-1)", self.cutoff_spin)
195
+
196
+ # Fstrength
197
+ self.fstrength_spin = QDoubleSpinBox()
198
+ self.fstrength_spin.setRange(0.0, 1.0)
199
+ self.fstrength_spin.setSingleStep(0.01)
200
+ self.fstrength_spin.setValue(0.9)
201
+ params_layout.addRow("Fourier strength (0-1)", self.fstrength_spin)
202
+
203
+ # Randomness
204
+ self.randomness_spin = QDoubleSpinBox()
205
+ self.randomness_spin.setRange(0.0, 1.0)
206
+ self.randomness_spin.setSingleStep(0.01)
207
+ self.randomness_spin.setValue(0.05)
208
+ params_layout.addRow("Fourier randomness", self.randomness_spin)
209
+
210
+ # Phase_perturb
211
+ self.phase_perturb_spin = QDoubleSpinBox()
212
+ self.phase_perturb_spin.setRange(0.0, 1.0)
213
+ self.phase_perturb_spin.setSingleStep(0.001)
214
+ self.phase_perturb_spin.setValue(0.08)
215
+ self.phase_perturb_spin.setToolTip("Phase perturbation std (radians)")
216
+ params_layout.addRow("Phase perturb (rad)", self.phase_perturb_spin)
217
+
218
+ # Radial_smooth
219
+ self.radial_smooth_spin = QSpinBox()
220
+ self.radial_smooth_spin.setRange(0, 50)
221
+ self.radial_smooth_spin.setValue(5)
222
+ params_layout.addRow("Radial smooth (bins)", self.radial_smooth_spin)
223
+
224
+ # FFT_mode
225
+ self.fft_mode_combo = QComboBox()
226
+ self.fft_mode_combo.addItems(["auto", "ref", "model"])
227
+ self.fft_mode_combo.setCurrentText("auto")
228
+ params_layout.addRow("FFT mode", self.fft_mode_combo)
229
+
230
+ # FFT_alpha
231
+ self.fft_alpha_spin = QDoubleSpinBox()
232
+ self.fft_alpha_spin.setRange(0.1, 4.0)
233
+ self.fft_alpha_spin.setSingleStep(0.1)
234
+ self.fft_alpha_spin.setValue(1.0)
235
+ self.fft_alpha_spin.setToolTip("Alpha exponent for 1/f model when using model mode")
236
+ params_layout.addRow("FFT alpha (model)", self.fft_alpha_spin)
237
+
238
+ # Perturb
239
+ self.perturb_spin = QDoubleSpinBox()
240
+ self.perturb_spin.setRange(0.0, 0.05)
241
+ self.perturb_spin.setSingleStep(0.001)
242
+ self.perturb_spin.setValue(0.008)
243
+ params_layout.addRow("Pixel perturb", self.perturb_spin)
244
+
245
+ # Seed
246
+ self.seed_spin = QSpinBox()
247
+ self.seed_spin.setRange(0, 2 ** 31 - 1)
248
+ self.seed_spin.setValue(0)
249
+ params_layout.addRow("Seed (0=none)", self.seed_spin)
250
+
251
+ # AWB checkbox (new)
252
+ self.awb_chk = QCheckBox("Enable auto white-balance (AWB)")
253
+ self.awb_chk.setChecked(False)
254
+ 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.")
255
+ params_layout.addRow(self.awb_chk)
256
+
257
+ # Camera simulator toggle
258
+ self.sim_camera_chk = QCheckBox("Enable camera pipeline simulation")
259
+ self.sim_camera_chk.setChecked(False)
260
+ self.sim_camera_chk.stateChanged.connect(self._on_sim_camera_toggled)
261
+ params_layout.addRow(self.sim_camera_chk)
262
+
263
+ # Camera simulator collapsible group
264
+ self.camera_box = CollapsibleBox("Camera simulator options")
265
+ right_v.addWidget(self.camera_box)
266
+ cam_layout = QFormLayout()
267
+ cam_container = QWidget()
268
+ cam_container.setLayout(cam_layout)
269
+ self.camera_box.content_layout.addWidget(cam_container)
270
+
271
+ # Enable bayer
272
+ self.bayer_chk = QCheckBox("Enable Bayer / demosaic (RGGB)")
273
+ self.bayer_chk.setChecked(True)
274
+ cam_layout.addRow(self.bayer_chk)
275
+
276
+ # JPEG cycles
277
+ self.jpeg_cycles_spin = QSpinBox()
278
+ self.jpeg_cycles_spin.setRange(0, 10)
279
+ self.jpeg_cycles_spin.setValue(1)
280
+ cam_layout.addRow("JPEG cycles", self.jpeg_cycles_spin)
281
+
282
+ # JPEG quality min/max
283
+ self.jpeg_qmin_spin = QSpinBox()
284
+ self.jpeg_qmin_spin.setRange(1, 100)
285
+ self.jpeg_qmin_spin.setValue(88)
286
+ self.jpeg_qmax_spin = QSpinBox()
287
+ self.jpeg_qmax_spin.setRange(1, 100)
288
+ self.jpeg_qmax_spin.setValue(96)
289
+ qbox = QHBoxLayout()
290
+ qbox.addWidget(self.jpeg_qmin_spin)
291
+ qbox.addWidget(QLabel("to"))
292
+ qbox.addWidget(self.jpeg_qmax_spin)
293
+ cam_layout.addRow("JPEG quality (min to max)", qbox)
294
+
295
+ # Vignette strength
296
+ self.vignette_spin = QDoubleSpinBox()
297
+ self.vignette_spin.setRange(0.0, 1.0)
298
+ self.vignette_spin.setSingleStep(0.01)
299
+ self.vignette_spin.setValue(0.35)
300
+ cam_layout.addRow("Vignette strength", self.vignette_spin)
301
+
302
+ # Chromatic aberration strength
303
+ self.chroma_spin = QDoubleSpinBox()
304
+ self.chroma_spin.setRange(0.0, 10.0)
305
+ self.chroma_spin.setSingleStep(0.1)
306
+ self.chroma_spin.setValue(1.2)
307
+ cam_layout.addRow("Chromatic aberration (px)", self.chroma_spin)
308
+
309
+ # ISO scale
310
+ self.iso_spin = QDoubleSpinBox()
311
+ self.iso_spin.setRange(0.1, 16.0)
312
+ self.iso_spin.setSingleStep(0.1)
313
+ self.iso_spin.setValue(1.0)
314
+ cam_layout.addRow("ISO/exposure scale", self.iso_spin)
315
+
316
+ # Read noise
317
+ self.read_noise_spin = QDoubleSpinBox()
318
+ self.read_noise_spin.setRange(0.0, 50.0)
319
+ self.read_noise_spin.setSingleStep(0.1)
320
+ self.read_noise_spin.setValue(2.0)
321
+ cam_layout.addRow("Read noise (DN)", self.read_noise_spin)
322
+
323
+ # Hot pixel prob
324
+ self.hot_pixel_spin = QDoubleSpinBox()
325
+ self.hot_pixel_spin.setDecimals(9)
326
+ self.hot_pixel_spin.setRange(0.0, 1.0)
327
+ self.hot_pixel_spin.setSingleStep(1e-6)
328
+ self.hot_pixel_spin.setValue(1e-6)
329
+ cam_layout.addRow("Hot pixel prob", self.hot_pixel_spin)
330
+
331
+ # Banding strength
332
+ self.banding_spin = QDoubleSpinBox()
333
+ self.banding_spin.setRange(0.0, 1.0)
334
+ self.banding_spin.setSingleStep(0.01)
335
+ self.banding_spin.setValue(0.0)
336
+ cam_layout.addRow("Banding strength", self.banding_spin)
337
+
338
+ # Motion blur kernel
339
+ self.motion_blur_spin = QSpinBox()
340
+ self.motion_blur_spin.setRange(1, 51)
341
+ self.motion_blur_spin.setValue(1)
342
+ cam_layout.addRow("Motion blur kernel", self.motion_blur_spin)
343
+
344
+ self.camera_box.setVisible(False)
345
+
346
+ self.ref_hint = QLabel("AWB uses the 'AWB reference' chooser. FFT spectral matching uses the 'FFT Reference' chooser.")
347
+ right_v.addWidget(self.ref_hint)
348
+
349
+ self.analysis_input = AnalysisPanel(title="Input analysis")
350
+ self.analysis_output = AnalysisPanel(title="Output analysis")
351
+ right_v.addWidget(self.analysis_input)
352
+ right_v.addWidget(self.analysis_output)
353
+
354
+ right_v.addStretch(1)
355
+
356
+ # Status bar
357
+ self.status = QLabel("Ready")
358
+ self.status.setStyleSheet("color:#bdbdbd;padding:6px")
359
+ self.status.setAlignment(Qt.AlignLeft)
360
+ self.status.setFixedHeight(28)
361
+ self.status.setContentsMargins(6, 6, 6, 6)
362
+ self.statusBar().addWidget(self.status)
363
+
364
+ self.worker = None
365
+ self._on_auto_mode_toggled(self.auto_mode_chk.checkState())
366
+
367
+ def _on_sim_camera_toggled(self, state):
368
+ enabled = state == Qt.Checked
369
+ self.camera_box.setVisible(enabled)
370
+
371
+ def _on_auto_mode_toggled(self, state):
372
+ is_auto = (state == Qt.Checked)
373
+ self.auto_box.setVisible(is_auto)
374
+ self.params_box.setVisible(not is_auto)
375
+
376
+ def _update_strength_label(self, value):
377
+ self.strength_label.setText(str(value))
378
+
379
+ def choose_input(self):
380
+ path, _ = QFileDialog.getOpenFileName(self, "Choose input image", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp *.tif)")
381
+ if path:
382
+ self.input_line.setText(path)
383
+ self.load_preview(self.preview_in, path)
384
+ self.analysis_input.update_from_path(path)
385
+ out_suggest = str(Path(path).with_name(Path(path).stem + "_out" + Path(path).suffix))
386
+ if not self.output_line.text():
387
+ self.output_line.setText(out_suggest)
388
+
389
+ def choose_ref(self):
390
+ path, _ = QFileDialog.getOpenFileName(self, "Choose AWB reference image", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp *.tif)")
391
+ if path:
392
+ self.ref_line.setText(path)
393
+
394
+ def choose_fft_ref(self):
395
+ path, _ = QFileDialog.getOpenFileName(self, "Choose FFT reference image", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp *.tif)")
396
+ if path:
397
+ self.fft_ref_line.setText(path)
398
+
399
+ def choose_output(self):
400
+ path, _ = QFileDialog.getSaveFileName(self, "Choose output path", str(Path.home()), "JPEG (*.jpg *.jpeg);;PNG (*.png);;TIFF (*.tif)")
401
+ if path:
402
+ self.output_line.setText(path)
403
+
404
+ def load_preview(self, widget: QLabel, path: str):
405
+ if not path or not os.path.exists(path):
406
+ widget.setText("No image")
407
+ widget.setPixmap(QPixmap())
408
+ return
409
+ pix = qpixmap_from_path(path, max_size=(widget.width(), widget.height()))
410
+ widget.setPixmap(pix)
411
+
412
+ def set_enabled_all(self, enabled: bool):
413
+ for w in self.findChildren((QPushButton, QDoubleSpinBox, QSpinBox, QLineEdit, QComboBox, QCheckBox, QSlider, QToolButton)):
414
+ w.setEnabled(enabled)
415
+
416
+ def on_run(self):
417
+ from types import SimpleNamespace
418
+ inpath = self.input_line.text().strip()
419
+ outpath = self.output_line.text().strip()
420
+ if not inpath or not os.path.exists(inpath):
421
+ QMessageBox.warning(self, "Missing input", "Please choose a valid input image.")
422
+ return
423
+ if not outpath:
424
+ QMessageBox.warning(self, "Missing output", "Please choose an output path.")
425
+ return
426
+
427
+ awb_ref_val = self.ref_line.text() or None
428
+ fft_ref_val = self.fft_ref_line.text() or None
429
+ args = SimpleNamespace()
430
+
431
+ if self.auto_mode_chk.isChecked():
432
+ strength = self.strength_slider.value() / 100.0
433
+ args.noise_std = strength * 0.04
434
+ args.clahe_clip = 1.0 + strength * 3.0
435
+ args.cutoff = max(0.01, 0.4 - strength * 0.3)
436
+ args.fstrength = strength * 0.95
437
+ args.phase_perturb = strength * 0.1
438
+ args.perturb = strength * 0.015
439
+ args.jpeg_cycles = int(strength * 2)
440
+ args.jpeg_qmin = max(1, int(95 - strength * 35))
441
+ args.jpeg_qmax = max(1, int(99 - strength * 25))
442
+ args.vignette_strength = strength * 0.6
443
+ args.chroma_strength = strength * 4.0
444
+ args.motion_blur_kernel = 1 + 2 * int(strength * 6)
445
+ args.banding_strength = strength * 0.1
446
+ args.tile = 8
447
+ args.randomness = 0.05
448
+ args.radial_smooth = 5
449
+ args.fft_mode = "auto"
450
+ args.fft_alpha = 1.0
451
+ args.alpha = 1.0
452
+ seed_val = int(self.seed_spin.value())
453
+ args.seed = None if seed_val == 0 else seed_val
454
+ args.sim_camera = bool(self.sim_camera_chk.isChecked())
455
+ args.no_no_bayer = True
456
+ args.iso_scale = 1.0
457
+ args.read_noise = 2.0
458
+ args.hot_pixel_prob = 1e-6
459
+ else:
460
+ seed_val = int(self.seed_spin.value())
461
+ args.seed = None if seed_val == 0 else seed_val
462
+ sim_camera = bool(self.sim_camera_chk.isChecked())
463
+ enable_bayer = bool(self.bayer_chk.isChecked())
464
+ args.noise_std = float(self.noise_spin.value())
465
+ args.clahe_clip = float(self.clahe_spin.value())
466
+ args.tile = int(self.tile_spin.value())
467
+ args.cutoff = float(self.cutoff_spin.value())
468
+ args.fstrength = float(self.fstrength_spin.value())
469
+ args.strength = float(self.fstrength_spin.value())
470
+ args.randomness = float(self.randomness_spin.value())
471
+ args.phase_perturb = float(self.phase_perturb_spin.value())
472
+ args.perturb = float(self.perturb_spin.value())
473
+ args.fft_mode = self.fft_mode_combo.currentText()
474
+ args.fft_alpha = float(self.fft_alpha_spin.value())
475
+ args.alpha = float(self.fft_alpha_spin.value())
476
+ args.radial_smooth = int(self.radial_smooth_spin.value())
477
+ args.sim_camera = sim_camera
478
+ args.no_no_bayer = bool(enable_bayer)
479
+ args.jpeg_cycles = int(self.jpeg_cycles_spin.value())
480
+ args.jpeg_qmin = int(self.jpeg_qmin_spin.value())
481
+ args.jpeg_qmax = int(self.jpeg_qmax_spin.value())
482
+ args.vignette_strength = float(self.vignette_spin.value())
483
+ args.chroma_strength = float(self.chroma_spin.value())
484
+ args.iso_scale = float(self.iso_spin.value())
485
+ args.read_noise = float(self.read_noise_spin.value())
486
+ args.hot_pixel_prob = float(self.hot_pixel_spin.value())
487
+ args.banding_strength = float(self.banding_spin.value())
488
+ args.motion_blur_kernel = int(self.motion_blur_spin.value())
489
+
490
+ # AWB handling: only apply if checkbox is checked
491
+ if self.awb_chk.isChecked():
492
+ args.ref = awb_ref_val # may be None -> the pipeline will fall back to gray-world
493
+ else:
494
+ args.ref = None
495
+
496
+ # FFT spectral matching reference
497
+ args.fft_ref = fft_ref_val
498
+
499
+ self.worker = Worker(inpath, outpath, args)
500
+ self.worker.finished.connect(self.on_finished)
501
+ self.worker.error.connect(self.on_error)
502
+ self.worker.started.connect(lambda: self.on_worker_started())
503
+ self.worker.start()
504
+
505
+ self.progress.setRange(0, 0)
506
+ self.status.setText("Processing...")
507
+ self.set_enabled_all(False)
508
+
509
+ def on_worker_started(self):
510
+ pass
511
+
512
+ def on_finished(self, outpath):
513
+ self.progress.setRange(0, 100)
514
+ self.progress.setValue(100)
515
+ self.status.setText("Done — saved to: " + outpath)
516
+ self.load_preview(self.preview_out, outpath)
517
+ self.analysis_output.update_from_path(outpath)
518
+ self.set_enabled_all(True)
519
+
520
+ def on_error(self, msg, traceback_text):
521
+ from PyQt5.QtWidgets import QDialog, QTextEdit
522
+ self.progress.setRange(0, 100)
523
+ self.progress.setValue(0)
524
+ self.status.setText("Error")
525
+
526
+ dialog = QDialog(self)
527
+ dialog.setWindowTitle("Processing Error")
528
+ dialog.setMinimumSize(700, 480)
529
+ layout = QVBoxLayout(dialog)
530
+
531
+ error_label = QLabel(f"Error: {msg}")
532
+ error_label.setWordWrap(True)
533
+ layout.addWidget(error_label)
534
+
535
+ traceback_edit = QTextEdit()
536
+ traceback_edit.setReadOnly(True)
537
+ traceback_edit.setText(traceback_text)
538
+ traceback_edit.setStyleSheet("font-family: monospace; font-size: 12px;")
539
+ layout.addWidget(traceback_edit)
540
+
541
+ ok_button = QPushButton("OK")
542
+ ok_button.clicked.connect(dialog.accept)
543
+ layout.addWidget(ok_button)
544
+
545
+ dialog.exec_()
546
+ self.set_enabled_all(True)
547
+
548
+ def open_output_folder(self):
549
+ out = self.output_line.text().strip()
550
+ if not out:
551
+ QMessageBox.information(self, "No output", "No output path set yet.")
552
+ return
553
+ folder = os.path.dirname(os.path.abspath(out))
554
+ if not os.path.exists(folder):
555
+ QMessageBox.warning(self, "Not found", "Output folder does not exist: " + folder)
556
+ return
557
+ if sys.platform.startswith('darwin'):
558
+ os.system(f'open "{folder}"')
559
+ elif os.name == 'nt':
560
+ os.startfile(folder)
561
+ else:
562
+ os.system(f'xdg-open "{folder}"')
563
+
564
+
565
+ def apply_dark_palette(app: QApplication):
566
+ pal = QPalette()
567
+ # base
568
+ pal.setColor(QPalette.Window, QColor(18, 18, 19))
569
+ pal.setColor(QPalette.WindowText, QColor(220, 220, 220))
570
+ pal.setColor(QPalette.Base, QColor(28, 28, 30))
571
+ pal.setColor(QPalette.AlternateBase, QColor(24, 24, 26))
572
+ pal.setColor(QPalette.ToolTipBase, QColor(220, 220, 220))
573
+ pal.setColor(QPalette.ToolTipText, QColor(220, 220, 220))
574
+ pal.setColor(QPalette.Text, QColor(230, 230, 230))
575
+ pal.setColor(QPalette.Button, QColor(40, 40, 42))
576
+ pal.setColor(QPalette.ButtonText, QColor(230, 230, 230))
577
+ pal.setColor(QPalette.Highlight, QColor(70, 130, 180))
578
+ pal.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
579
+ app.setPalette(pal)
580
+
581
+ # global stylesheet for a modern gray look
582
+ app.setStyleSheet(r"""
583
+ QWidget { font-family: 'Segoe UI', Roboto, Arial, sans-serif; font-size:11pt }
584
+ QToolButton { padding:6px; }
585
+ QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox { background: #1e1e1f; border: 1px solid #333; padding:4px; border-radius:6px }
586
+ QPushButton { background: #2a2a2c; border: 1px solid #3a3a3c; padding:6px 10px; border-radius:8px }
587
+ QPushButton:hover { background: #333336 }
588
+ QPushButton:pressed { background: #232325 }
589
+ QProgressBar { background: #222; border: 1px solid #333; border-radius:6px; text-align:center }
590
+ QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4b9bd6, stop:1 #3b83c0); }
591
+ QLabel { color: #d6d6d6 }
592
+ QCheckBox { padding:4px }
593
+ """)
594
+
595
+
596
+ def main():
597
+ app = QApplication([])
598
+ apply_dark_palette(app)
599
+ if IMPORT_ERROR:
600
+ QMessageBox.critical(None, "Import error", "Could not import image_postprocess module:\n" + IMPORT_ERROR)
601
+ w = MainWindow()
602
+ w.show()
603
+ sys.exit(app.exec_())
604
+
605
+
606
+ if __name__ == '__main__':
607
+ main()
image_postprocess_gui.py → run.py RENAMED
@@ -1,7 +1,7 @@
1
  #!/usr/bin/env python3
2
- """""
3
  Main GUI application for image_postprocess pipeline with camera-simulator controls.
4
- """""
5
 
6
  import sys
7
  import os
@@ -9,13 +9,15 @@ from pathlib import Path
9
  from PyQt5.QtWidgets import (
10
  QApplication, QMainWindow, QWidget, QLabel, QPushButton, QFileDialog,
11
  QHBoxLayout, QVBoxLayout, QFormLayout, QSlider, QSpinBox, QDoubleSpinBox,
12
- QProgressBar, QMessageBox, QGroupBox, QLineEdit, QComboBox, QCheckBox
13
  )
14
  from PyQt5.QtCore import Qt
15
  from PyQt5.QtGui import QPixmap
16
  from worker import Worker
17
  from analysis_panel import AnalysisPanel
18
  from utils import qpixmap_from_path
 
 
19
 
20
  try:
21
  from image_postprocess import process_image
@@ -28,7 +30,7 @@ else:
28
  class MainWindow(QMainWindow):
29
  def __init__(self):
30
  super().__init__()
31
- self.setWindowTitle("Image Postprocess — GUI (with Camera Simulator)")
32
  self.setMinimumSize(1200, 760)
33
 
34
  central = QWidget()
@@ -39,34 +41,44 @@ class MainWindow(QMainWindow):
39
  left_v = QVBoxLayout()
40
  main_h.addLayout(left_v, 2)
41
 
42
- in_group = QGroupBox("Input / Output")
43
- left_v.addWidget(in_group)
 
44
  in_layout = QFormLayout()
45
- in_group.setLayout(in_layout)
 
 
46
 
47
  self.input_line = QLineEdit()
48
  self.input_btn = QPushButton("Choose Input")
49
  self.input_btn.clicked.connect(self.choose_input)
 
50
  self.ref_line = QLineEdit()
51
- self.ref_btn = QPushButton("Choose Reference (optional)")
52
  self.ref_btn.clicked.connect(self.choose_ref)
 
 
 
 
 
53
  self.output_line = QLineEdit()
54
  self.output_btn = QPushButton("Choose Output")
55
  self.output_btn.clicked.connect(self.choose_output)
56
 
57
  in_layout.addRow(self.input_btn, self.input_line)
58
  in_layout.addRow(self.ref_btn, self.ref_line)
 
59
  in_layout.addRow(self.output_btn, self.output_line)
60
 
61
  # Previews
62
  self.preview_in = QLabel(alignment=Qt.AlignCenter)
63
  self.preview_in.setFixedSize(480, 300)
64
- self.preview_in.setStyleSheet("background:#111; border:1px solid #444; color:#ddd")
65
  self.preview_in.setText("Input preview")
66
 
67
  self.preview_out = QLabel(alignment=Qt.AlignCenter)
68
  self.preview_out.setFixedSize(480, 300)
69
- self.preview_out.setStyleSheet("background:#111; border:1px solid #444; color:#ddd")
70
  self.preview_out.setText("Output preview")
71
 
72
  left_v.addWidget(self.preview_in)
@@ -92,16 +104,20 @@ class MainWindow(QMainWindow):
92
  right_v = QVBoxLayout()
93
  main_h.addLayout(right_v, 3)
94
 
95
- # Auto Mode controls
96
  self.auto_mode_chk = QCheckBox("Enable Auto Mode")
97
  self.auto_mode_chk.setChecked(False)
98
  self.auto_mode_chk.stateChanged.connect(self._on_auto_mode_toggled)
99
  right_v.addWidget(self.auto_mode_chk)
100
 
101
- self.auto_group = QGroupBox("Auto Mode")
 
 
102
  auto_layout = QFormLayout()
103
- self.auto_group.setLayout(auto_layout)
104
-
 
 
105
  strength_layout = QHBoxLayout()
106
  self.strength_slider = QSlider(Qt.Horizontal)
107
  self.strength_slider.setRange(0, 100)
@@ -113,12 +129,14 @@ class MainWindow(QMainWindow):
113
  strength_layout.addWidget(self.strength_label)
114
 
115
  auto_layout.addRow("Aberration Strength", strength_layout)
116
- right_v.addWidget(self.auto_group)
117
 
118
- self.params_group = QGroupBox("Parameters (Manual Mode)")
119
- right_v.addWidget(self.params_group)
 
120
  params_layout = QFormLayout()
121
- self.params_group.setLayout(params_layout)
 
 
122
 
123
  # Noise-std
124
  self.noise_spin = QDoubleSpinBox()
@@ -203,16 +221,25 @@ class MainWindow(QMainWindow):
203
  self.seed_spin.setValue(0)
204
  params_layout.addRow("Seed (0=none)", self.seed_spin)
205
 
 
 
 
 
 
 
206
  # Camera simulator toggle
207
  self.sim_camera_chk = QCheckBox("Enable camera pipeline simulation")
208
  self.sim_camera_chk.setChecked(False)
209
  self.sim_camera_chk.stateChanged.connect(self._on_sim_camera_toggled)
210
  params_layout.addRow(self.sim_camera_chk)
211
 
212
- # Camera simulator group
213
- self.camera_group = QGroupBox("Camera simulator options")
 
214
  cam_layout = QFormLayout()
215
- self.camera_group.setLayout(cam_layout)
 
 
216
 
217
  # Enable bayer
218
  self.bayer_chk = QCheckBox("Enable Bayer / demosaic (RGGB)")
@@ -287,12 +314,9 @@ class MainWindow(QMainWindow):
287
  self.motion_blur_spin.setValue(1)
288
  cam_layout.addRow("Motion blur kernel", self.motion_blur_spin)
289
 
290
- self.camera_group.setVisible(False)
291
- right_v.addWidget(self.camera_group)
292
 
293
- params_layout.addRow(self.camera_group)
294
-
295
- self.ref_hint = QLabel("Reference color matching supported by OpenCV only.")
296
  right_v.addWidget(self.ref_hint)
297
 
298
  self.analysis_input = AnalysisPanel(title="Input analysis")
@@ -315,12 +339,12 @@ class MainWindow(QMainWindow):
315
 
316
  def _on_sim_camera_toggled(self, state):
317
  enabled = state == Qt.Checked
318
- self.camera_group.setVisible(enabled)
319
 
320
  def _on_auto_mode_toggled(self, state):
321
  is_auto = (state == Qt.Checked)
322
- self.auto_group.setVisible(is_auto)
323
- self.params_group.setVisible(not is_auto)
324
 
325
  def _update_strength_label(self, value):
326
  self.strength_label.setText(str(value))
@@ -336,10 +360,15 @@ class MainWindow(QMainWindow):
336
  self.output_line.setText(out_suggest)
337
 
338
  def choose_ref(self):
339
- path, _ = QFileDialog.getOpenFileName(self, "Choose reference image", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp *.tif)")
340
  if path:
341
  self.ref_line.setText(path)
342
 
 
 
 
 
 
343
  def choose_output(self):
344
  path, _ = QFileDialog.getSaveFileName(self, "Choose output path", str(Path.home()), "JPEG (*.jpg *.jpeg);;PNG (*.png);;TIFF (*.tif)")
345
  if path:
@@ -354,7 +383,7 @@ class MainWindow(QMainWindow):
354
  widget.setPixmap(pix)
355
 
356
  def set_enabled_all(self, enabled: bool):
357
- for w in self.findChildren((QPushButton, QDoubleSpinBox, QSpinBox, QLineEdit, QComboBox, QCheckBox, QSlider)):
358
  w.setEnabled(enabled)
359
 
360
  def on_run(self):
@@ -368,7 +397,8 @@ class MainWindow(QMainWindow):
368
  QMessageBox.warning(self, "Missing output", "Please choose an output path.")
369
  return
370
 
371
- ref_val = self.ref_line.text() or None
 
372
  args = SimpleNamespace()
373
 
374
  if self.auto_mode_chk.isChecked():
@@ -430,8 +460,14 @@ class MainWindow(QMainWindow):
430
  args.banding_strength = float(self.banding_spin.value())
431
  args.motion_blur_kernel = int(self.motion_blur_spin.value())
432
 
433
- args.ref = None
434
- args.fft_ref = ref_val
 
 
 
 
 
 
435
 
436
  self.worker = Worker(inpath, outpath, args)
437
  self.worker.finished.connect(self.on_finished)
@@ -455,7 +491,7 @@ class MainWindow(QMainWindow):
455
  self.set_enabled_all(True)
456
 
457
  def on_error(self, msg, traceback_text):
458
- from PyQt5.QtWidgets import QDialog, QTextEdit, QVBoxLayout
459
  self.progress.setRange(0, 100)
460
  self.progress.setValue(0)
461
  self.status.setText("Error")
@@ -500,6 +536,7 @@ class MainWindow(QMainWindow):
500
 
501
  def main():
502
  app = QApplication([])
 
503
  if IMPORT_ERROR:
504
  QMessageBox.critical(None, "Import error", "Could not import image_postprocess module:\n" + IMPORT_ERROR)
505
  w = MainWindow()
 
1
  #!/usr/bin/env python3
2
+ """
3
  Main GUI application for image_postprocess pipeline with camera-simulator controls.
4
+ """
5
 
6
  import sys
7
  import os
 
9
  from PyQt5.QtWidgets import (
10
  QApplication, QMainWindow, QWidget, QLabel, QPushButton, QFileDialog,
11
  QHBoxLayout, QVBoxLayout, QFormLayout, QSlider, QSpinBox, QDoubleSpinBox,
12
+ QProgressBar, QMessageBox, QLineEdit, QComboBox, QCheckBox, QToolButton
13
  )
14
  from PyQt5.QtCore import Qt
15
  from PyQt5.QtGui import QPixmap
16
  from worker import Worker
17
  from analysis_panel import AnalysisPanel
18
  from utils import qpixmap_from_path
19
+ from collapsible_box import CollapsibleBox
20
+ from theme import apply_dark_palette
21
 
22
  try:
23
  from image_postprocess import process_image
 
30
  class MainWindow(QMainWindow):
31
  def __init__(self):
32
  super().__init__()
33
+ self.setWindowTitle("Image Postprocess — GUI (Camera Simulator)")
34
  self.setMinimumSize(1200, 760)
35
 
36
  central = QWidget()
 
41
  left_v = QVBoxLayout()
42
  main_h.addLayout(left_v, 2)
43
 
44
+ # Input/Output collapsible
45
+ io_box = CollapsibleBox("Input / Output")
46
+ left_v.addWidget(io_box)
47
  in_layout = QFormLayout()
48
+ io_container = QWidget()
49
+ io_container.setLayout(in_layout)
50
+ io_box.content_layout.addWidget(io_container)
51
 
52
  self.input_line = QLineEdit()
53
  self.input_btn = QPushButton("Choose Input")
54
  self.input_btn.clicked.connect(self.choose_input)
55
+
56
  self.ref_line = QLineEdit()
57
+ self.ref_btn = QPushButton("Choose AWB Reference (optional)")
58
  self.ref_btn.clicked.connect(self.choose_ref)
59
+
60
+ self.fft_ref_line = QLineEdit()
61
+ self.fft_ref_btn = QPushButton("Choose FFT Reference (optional)")
62
+ self.fft_ref_btn.clicked.connect(self.choose_fft_ref)
63
+
64
  self.output_line = QLineEdit()
65
  self.output_btn = QPushButton("Choose Output")
66
  self.output_btn.clicked.connect(self.choose_output)
67
 
68
  in_layout.addRow(self.input_btn, self.input_line)
69
  in_layout.addRow(self.ref_btn, self.ref_line)
70
+ in_layout.addRow(self.fft_ref_btn, self.fft_ref_line)
71
  in_layout.addRow(self.output_btn, self.output_line)
72
 
73
  # Previews
74
  self.preview_in = QLabel(alignment=Qt.AlignCenter)
75
  self.preview_in.setFixedSize(480, 300)
76
+ self.preview_in.setStyleSheet("background:#121213; border:1px solid #2b2b2b; color:#ddd; border-radius:6px")
77
  self.preview_in.setText("Input preview")
78
 
79
  self.preview_out = QLabel(alignment=Qt.AlignCenter)
80
  self.preview_out.setFixedSize(480, 300)
81
+ self.preview_out.setStyleSheet("background:#121213; border:1px solid #2b2b2b; color:#ddd; border-radius:6px")
82
  self.preview_out.setText("Output preview")
83
 
84
  left_v.addWidget(self.preview_in)
 
104
  right_v = QVBoxLayout()
105
  main_h.addLayout(right_v, 3)
106
 
107
+ # Auto Mode toggle (keeps top-level quick switch visible)
108
  self.auto_mode_chk = QCheckBox("Enable Auto Mode")
109
  self.auto_mode_chk.setChecked(False)
110
  self.auto_mode_chk.stateChanged.connect(self._on_auto_mode_toggled)
111
  right_v.addWidget(self.auto_mode_chk)
112
 
113
+ # Make Auto Mode section collapsible
114
+ self.auto_box = CollapsibleBox("Auto Mode")
115
+ right_v.addWidget(self.auto_box)
116
  auto_layout = QFormLayout()
117
+ auto_container = QWidget()
118
+ auto_container.setLayout(auto_layout)
119
+ self.auto_box.content_layout.addWidget(auto_container)
120
+
121
  strength_layout = QHBoxLayout()
122
  self.strength_slider = QSlider(Qt.Horizontal)
123
  self.strength_slider.setRange(0, 100)
 
129
  strength_layout.addWidget(self.strength_label)
130
 
131
  auto_layout.addRow("Aberration Strength", strength_layout)
 
132
 
133
+ # Parameters (Manual Mode) collapsible
134
+ self.params_box = CollapsibleBox("Parameters (Manual Mode)")
135
+ right_v.addWidget(self.params_box)
136
  params_layout = QFormLayout()
137
+ params_container = QWidget()
138
+ params_container.setLayout(params_layout)
139
+ self.params_box.content_layout.addWidget(params_container)
140
 
141
  # Noise-std
142
  self.noise_spin = QDoubleSpinBox()
 
221
  self.seed_spin.setValue(0)
222
  params_layout.addRow("Seed (0=none)", self.seed_spin)
223
 
224
+ # AWB checkbox
225
+ self.awb_chk = QCheckBox("Enable auto white-balance (AWB)")
226
+ self.awb_chk.setChecked(False)
227
+ 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.")
228
+ params_layout.addRow(self.awb_chk)
229
+
230
  # Camera simulator toggle
231
  self.sim_camera_chk = QCheckBox("Enable camera pipeline simulation")
232
  self.sim_camera_chk.setChecked(False)
233
  self.sim_camera_chk.stateChanged.connect(self._on_sim_camera_toggled)
234
  params_layout.addRow(self.sim_camera_chk)
235
 
236
+ # Camera simulator collapsible group
237
+ self.camera_box = CollapsibleBox("Camera simulator options")
238
+ right_v.addWidget(self.camera_box)
239
  cam_layout = QFormLayout()
240
+ cam_container = QWidget()
241
+ cam_container.setLayout(cam_layout)
242
+ self.camera_box.content_layout.addWidget(cam_container)
243
 
244
  # Enable bayer
245
  self.bayer_chk = QCheckBox("Enable Bayer / demosaic (RGGB)")
 
314
  self.motion_blur_spin.setValue(1)
315
  cam_layout.addRow("Motion blur kernel", self.motion_blur_spin)
316
 
317
+ self.camera_box.setVisible(False)
 
318
 
319
+ self.ref_hint = QLabel("AWB uses the 'AWB reference' chooser. FFT spectral matching uses the 'FFT Reference' chooser.")
 
 
320
  right_v.addWidget(self.ref_hint)
321
 
322
  self.analysis_input = AnalysisPanel(title="Input analysis")
 
339
 
340
  def _on_sim_camera_toggled(self, state):
341
  enabled = state == Qt.Checked
342
+ self.camera_box.setVisible(enabled)
343
 
344
  def _on_auto_mode_toggled(self, state):
345
  is_auto = (state == Qt.Checked)
346
+ self.auto_box.setVisible(is_auto)
347
+ self.params_box.setVisible(not is_auto)
348
 
349
  def _update_strength_label(self, value):
350
  self.strength_label.setText(str(value))
 
360
  self.output_line.setText(out_suggest)
361
 
362
  def choose_ref(self):
363
+ path, _ = QFileDialog.getOpenFileName(self, "Choose AWB reference image", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp *.tif)")
364
  if path:
365
  self.ref_line.setText(path)
366
 
367
+ def choose_fft_ref(self):
368
+ path, _ = QFileDialog.getOpenFileName(self, "Choose FFT reference image", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp *.tif)")
369
+ if path:
370
+ self.fft_ref_line.setText(path)
371
+
372
  def choose_output(self):
373
  path, _ = QFileDialog.getSaveFileName(self, "Choose output path", str(Path.home()), "JPEG (*.jpg *.jpeg);;PNG (*.png);;TIFF (*.tif)")
374
  if path:
 
383
  widget.setPixmap(pix)
384
 
385
  def set_enabled_all(self, enabled: bool):
386
+ for w in self.findChildren((QPushButton, QDoubleSpinBox, QSpinBox, QLineEdit, QComboBox, QCheckBox, QSlider, QToolButton)):
387
  w.setEnabled(enabled)
388
 
389
  def on_run(self):
 
397
  QMessageBox.warning(self, "Missing output", "Please choose an output path.")
398
  return
399
 
400
+ awb_ref_val = self.ref_line.text() or None
401
+ fft_ref_val = self.fft_ref_line.text() or None
402
  args = SimpleNamespace()
403
 
404
  if self.auto_mode_chk.isChecked():
 
460
  args.banding_strength = float(self.banding_spin.value())
461
  args.motion_blur_kernel = int(self.motion_blur_spin.value())
462
 
463
+ # AWB handling: only apply if checkbox is checked
464
+ if self.awb_chk.isChecked():
465
+ args.ref = awb_ref_val # may be None -> the pipeline will fall back to gray-world
466
+ else:
467
+ args.ref = None
468
+
469
+ # FFT spectral matching reference
470
+ args.fft_ref = fft_ref_val
471
 
472
  self.worker = Worker(inpath, outpath, args)
473
  self.worker.finished.connect(self.on_finished)
 
491
  self.set_enabled_all(True)
492
 
493
  def on_error(self, msg, traceback_text):
494
+ from PyQt5.QtWidgets import QDialog, QTextEdit
495
  self.progress.setRange(0, 100)
496
  self.progress.setValue(0)
497
  self.status.setText("Error")
 
536
 
537
  def main():
538
  app = QApplication([])
539
+ apply_dark_palette(app)
540
  if IMPORT_ERROR:
541
  QMessageBox.critical(None, "Import error", "Could not import image_postprocess module:\n" + IMPORT_ERROR)
542
  w = MainWindow()
theme.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt5.QtWidgets import QApplication
2
+ from PyQt5.QtGui import QPalette, QColor
3
+
4
+ def apply_dark_palette(app: QApplication):
5
+ pal = QPalette()
6
+ # base
7
+ pal.setColor(QPalette.Window, QColor(18, 18, 19))
8
+ pal.setColor(QPalette.WindowText, QColor(220, 220, 220))
9
+ pal.setColor(QPalette.Base, QColor(28, 28, 30))
10
+ pal.setColor(QPalette.AlternateBase, QColor(24, 24, 26))
11
+ pal.setColor(QPalette.ToolTipBase, QColor(220, 220, 220))
12
+ pal.setColor(QPalette.ToolTipText, QColor(220, 220, 220))
13
+ pal.setColor(QPalette.Text, QColor(230, 230, 230))
14
+ pal.setColor(QPalette.Button, QColor(40, 40, 42))
15
+ pal.setColor(QPalette.ButtonText, QColor(230, 230, 230))
16
+ pal.setColor(QPalette.Highlight, QColor(70, 130, 180))
17
+ pal.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
18
+ app.setPalette(pal)
19
+
20
+ # global stylesheet for a modern gray look
21
+ app.setStyleSheet(r"""
22
+ QWidget { font-family: 'Segoe UI', Roboto, Arial, sans-serif; font-size:11pt }
23
+ QToolButton { padding:6px; }
24
+ QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox { background: #1e1e1f; border: 1px solid #333; padding:4px; border-radius:6px }
25
+ QPushButton { background: #2a2a2c; border: 1px solid #3a3a3c; padding:6px 10px; border-radius:8px }
26
+ QPushButton:hover { background: #333336 }
27
+ QPushButton:pressed { background: #232325 }
28
+ QProgressBar { background: #222; border: 1px solid #333; border-radius:6px; text-align:center }
29
+ QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4b9bd6, stop:1 #3b83c0); }
30
+ QLabel { color: #d6d6d6 }
31
+ QCheckBox { padding:4px }
32
+ """)