Project

General

Profile

Files » ui.py

FUJITA Ryusei, 01/08/2026 01:48 PM

 
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()
(7-7/13)