|
1
|
import os
|
|
2
|
|
|
3
|
# ---- DISABLE PYTORCH MPS (Apple GPU) ----
|
|
4
|
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
|
5
|
os.environ["PYTORCH_MPS_DISABLE"] = "1"
|
|
6
|
# ----------------------------------------
|
|
7
|
|
|
8
|
import tkinter as tk
|
|
9
|
from tkinter import ttk, messagebox
|
|
10
|
from PIL import Image, ImageTk
|
|
11
|
import cv2
|
|
12
|
import numpy as np
|
|
13
|
|
|
14
|
# Local imports (must come AFTER disabling MPS)
|
|
15
|
import altmain
|
|
16
|
import config_reader
|
|
17
|
|
|
18
|
|
|
19
|
class ImageDisplayApp:
|
|
20
|
"""!
|
|
21
|
@brief Main GUI application for controlling projection blending (Static Image Only).
|
|
22
|
|
|
23
|
This class creates a Tkinter window to display the original image and the split
|
|
24
|
left/right images. It allows the user to adjust overlap and blend mode, and
|
|
25
|
provides options for fullscreen display and auto-calibration.
|
|
26
|
"""
|
|
27
|
|
|
28
|
def __init__(self, root: tk.Tk):
|
|
29
|
"""!
|
|
30
|
@brief Initializes the ImageDisplayApp.
|
|
31
|
|
|
32
|
Sets up the window, initializes the projection processor, and builds the UI.
|
|
33
|
|
|
34
|
@param root The root Tkinter window.
|
|
35
|
"""
|
|
36
|
self.root = root
|
|
37
|
self.setup_window()
|
|
38
|
|
|
39
|
# Initialize Logic
|
|
40
|
try:
|
|
41
|
self.cfg = config_reader.ConfigReader("config.ini")
|
|
42
|
self.processor = altmain.ProjectionSplit()
|
|
43
|
except Exception as e:
|
|
44
|
messagebox.showerror("Initialization Error", str(e))
|
|
45
|
return
|
|
46
|
|
|
47
|
self._update_job = None
|
|
48
|
self.labels = {}
|
|
49
|
self.image_paths = {
|
|
50
|
"main": self.cfg.get_image_path(),
|
|
51
|
"left": "left.png",
|
|
52
|
"right": "right.png"
|
|
53
|
}
|
|
54
|
|
|
55
|
self.setup_ui()
|
|
56
|
|
|
57
|
def setup_window(self):
|
|
58
|
"""!
|
|
59
|
@brief Configures the main application window.
|
|
60
|
|
|
61
|
Sets the title, geometry, and background color.
|
|
62
|
"""
|
|
63
|
self.root.title("Projection Blending Controller")
|
|
64
|
self.root.geometry("1000x600")
|
|
65
|
self.root.configure(bg="#f5f5f5")
|
|
66
|
|
|
67
|
def setup_ui(self):
|
|
68
|
"""!
|
|
69
|
@brief Builds the user interface components.
|
|
70
|
|
|
71
|
Creates the image display area, control panel (overlap, blend mode),
|
|
72
|
and action buttons (fullscreen, auto-calibrate).
|
|
73
|
"""
|
|
74
|
# Image Display Area
|
|
75
|
self.image_frame = ttk.Frame(self.root, padding=10)
|
|
76
|
self.image_frame.pack(pady=20)
|
|
77
|
|
|
78
|
for key in ["main", "left", "right"]:
|
|
79
|
lbl = ttk.Label(self.image_frame)
|
|
80
|
lbl.pack(side=tk.LEFT, padx=10)
|
|
81
|
self.labels[key] = lbl
|
|
82
|
|
|
83
|
# Controls
|
|
84
|
ctrl_frame = ttk.Frame(self.root, padding=10)
|
|
85
|
ctrl_frame.pack(pady=10)
|
|
86
|
|
|
87
|
ttk.Label(ctrl_frame, text="Overlap (px):").grid(row=0, column=0, padx=5)
|
|
88
|
self.param_var = tk.IntVar(value=int(self.cfg.get_overlap()))
|
|
89
|
overlap_entry = ttk.Entry(ctrl_frame, textvariable=self.param_var, width=8)
|
|
90
|
overlap_entry.grid(row=0, column=1, padx=5)
|
|
91
|
|
|
92
|
ttk.Label(ctrl_frame, text="Blend Mode:").grid(row=0, column=2, padx=5)
|
|
93
|
self.blend_var = tk.StringVar(value=self.cfg.get_blend_mode())
|
|
94
|
blend_box = ttk.Combobox(
|
|
95
|
ctrl_frame,
|
|
96
|
textvariable=self.blend_var,
|
|
97
|
values=["linear", "quadratic", "gaussian"],
|
|
98
|
state="readonly",
|
|
99
|
width=12
|
|
100
|
)
|
|
101
|
blend_box.grid(row=0, column=3, padx=5)
|
|
102
|
|
|
103
|
overlap_entry.bind("<FocusOut>", lambda e: self.debounced_update())
|
|
104
|
blend_box.bind("<<ComboboxSelected>>", lambda e: self.debounced_update())
|
|
105
|
|
|
106
|
btn_frame = ttk.Frame(self.root, padding=10)
|
|
107
|
btn_frame.pack(pady=5)
|
|
108
|
|
|
109
|
ttk.Button(btn_frame, text="Left Fullscreen",
|
|
110
|
command=lambda: self.show_fullscreen("left")).pack(side=tk.LEFT, padx=10)
|
|
111
|
ttk.Button(btn_frame, text="Right Fullscreen",
|
|
112
|
command=lambda: self.show_fullscreen("right")).pack(side=tk.LEFT, padx=10)
|
|
113
|
ttk.Button(btn_frame, text="Auto-Calibrate",
|
|
114
|
command=self.run_calibration).pack(side=tk.LEFT, padx=10)
|
|
115
|
|
|
116
|
def debounced_update(self):
|
|
117
|
"""!
|
|
118
|
@brief Schedules a processing update with a delay (debounce).
|
|
119
|
|
|
120
|
Prevents excessive updates while the user is typing or selecting options.
|
|
121
|
"""
|
|
122
|
if self._update_job:
|
|
123
|
self.root.after_cancel(self._update_job)
|
|
124
|
self._update_job = self.root.after(500, self.run_processing_update)
|
|
125
|
|
|
126
|
def run_processing_update(self):
|
|
127
|
"""!
|
|
128
|
@brief Executes the image processing update.
|
|
129
|
|
|
130
|
Reads the current UI values, triggers the projection processor, and updates the display.
|
|
131
|
"""
|
|
132
|
try:
|
|
133
|
overlap = self.param_var.get()
|
|
134
|
blend = self.blend_var.get()
|
|
135
|
|
|
136
|
self.processor.process_images(overlap, blend)
|
|
137
|
|
|
138
|
self.update_display({
|
|
139
|
"left": self.processor.image_left,
|
|
140
|
"right": self.processor.image_right,
|
|
141
|
"main": self.processor.image_main
|
|
142
|
})
|
|
143
|
except Exception as e:
|
|
144
|
print(f"Processing Error: {e}")
|
|
145
|
|
|
146
|
def update_display(self, images_dict):
|
|
147
|
"""!
|
|
148
|
@brief Updates the image labels with new images.
|
|
149
|
|
|
150
|
Converts OpenCV images to PIL format and updates the Tkinter labels.
|
|
151
|
|
|
152
|
@param images_dict A dictionary mapping keys ('left', 'right', 'main') to numpy image arrays.
|
|
153
|
"""
|
|
154
|
for key, np_img in images_dict.items():
|
|
155
|
if np_img is None:
|
|
156
|
continue
|
|
157
|
|
|
158
|
if np_img.shape[2] == 4:
|
|
159
|
cvt_img = cv2.cvtColor(np_img, cv2.COLOR_BGRA2RGBA)
|
|
160
|
else:
|
|
161
|
cvt_img = cv2.cvtColor(np_img, cv2.COLOR_BGR2RGB)
|
|
162
|
|
|
163
|
pil_img = Image.fromarray(cvt_img).resize((300, 300))
|
|
164
|
photo = ImageTk.PhotoImage(pil_img)
|
|
165
|
|
|
166
|
self.labels[key].configure(image=photo, text="")
|
|
167
|
self.labels[key].image = photo
|
|
168
|
|
|
169
|
def show_fullscreen(self, key):
|
|
170
|
"""!
|
|
171
|
@brief Displays the specified image in a fullscreen window.
|
|
172
|
|
|
173
|
Opens a new toplevel window, sets it to fullscreen, and displays the image.
|
|
174
|
Pressing 'Escape' closes the window.
|
|
175
|
|
|
176
|
@param key The key of the image to display ('left' or 'right').
|
|
177
|
"""
|
|
178
|
path = self.image_paths.get(key)
|
|
179
|
if not os.path.exists(path):
|
|
180
|
messagebox.showwarning("File Missing", f"{path} not found.")
|
|
181
|
return
|
|
182
|
|
|
183
|
top = tk.Toplevel(self.root)
|
|
184
|
top.attributes("-fullscreen", True)
|
|
185
|
top.configure(bg="black")
|
|
186
|
top.bind("<Escape>", lambda e: top.destroy())
|
|
187
|
|
|
188
|
lbl = ttk.Label(top, background="black")
|
|
189
|
lbl.pack(expand=True)
|
|
190
|
|
|
191
|
def _refresh():
|
|
192
|
if not top.winfo_exists():
|
|
193
|
return
|
|
194
|
try:
|
|
195
|
pil_img = Image.open(path)
|
|
196
|
photo = ImageTk.PhotoImage(pil_img)
|
|
197
|
lbl.configure(image=photo)
|
|
198
|
lbl.image = photo
|
|
199
|
except:
|
|
200
|
pass
|
|
201
|
top.after(100, _refresh)
|
|
202
|
|
|
203
|
_refresh()
|
|
204
|
|
|
205
|
def run_calibration(self):
|
|
206
|
"""!
|
|
207
|
@brief Runs the auto-calibration process.
|
|
208
|
|
|
209
|
Uses the CalibrationManager to simulate calibration and adjust the overlap.
|
|
210
|
Shows a message box with the result.
|
|
211
|
"""
|
|
212
|
try:
|
|
213
|
import calibration
|
|
214
|
calib = calibration.CalibrationManager()
|
|
215
|
|
|
216
|
current_overlap = self.param_var.get()
|
|
217
|
adjustment = calib.simulate_calibration(1920, current_overlap)
|
|
218
|
|
|
219
|
new_overlap = current_overlap + adjustment
|
|
220
|
self.param_var.set(new_overlap)
|
|
221
|
self.debounced_update()
|
|
222
|
|
|
223
|
messagebox.showinfo(
|
|
224
|
"Calibration Complete",
|
|
225
|
f"Overlap adjusted by {adjustment}px.\nNew Overlap: {new_overlap}"
|
|
226
|
)
|
|
227
|
except Exception as e:
|
|
228
|
messagebox.showerror("Calibration Error", str(e))
|
|
229
|
|
|
230
|
|
|
231
|
if __name__ == "__main__":
|
|
232
|
root = tk.Tk()
|
|
233
|
app = ImageDisplayApp(root)
|
|
234
|
root.mainloop()
|