|
1
|
# gui.py
|
|
2
|
|
|
3
|
import tkinter as tk
|
|
4
|
from tkinter import ttk, filedialog, messagebox
|
|
5
|
import threading
|
|
6
|
import json
|
|
7
|
import os
|
|
8
|
|
|
9
|
# Import the logic classes
|
|
10
|
from main_alpha_blender import MainAlphaBlender
|
|
11
|
from video_processor import VideoProcessor
|
|
12
|
|
|
13
|
class BlenderGUI:
|
|
14
|
"""A Tkinter GUI with tabs for image and video edge blending."""
|
|
15
|
def __init__(self, master):
|
|
16
|
self.master = master
|
|
17
|
master.title("Image and Video Edge Blender")
|
|
18
|
master.geometry("600x450") # Increased height for new buttons
|
|
19
|
|
|
20
|
# --- Create a Tabbed Interface ---
|
|
21
|
self.notebook = ttk.Notebook(master)
|
|
22
|
self.notebook.pack(pady=10, padx=10, fill="both", expand=True)
|
|
23
|
|
|
24
|
self.image_tab = ttk.Frame(self.notebook, padding="10")
|
|
25
|
self.video_tab = ttk.Frame(self.notebook, padding="10")
|
|
26
|
|
|
27
|
self.notebook.add(self.image_tab, text="Image Blender")
|
|
28
|
self.notebook.add(self.video_tab, text="Video Processor")
|
|
29
|
|
|
30
|
# --- Populate each tab ---
|
|
31
|
self.create_image_widgets()
|
|
32
|
self.create_video_widgets()
|
|
33
|
|
|
34
|
# --- NEW: Add a frame at the bottom for config management ---
|
|
35
|
self.config_frame = ttk.Frame(master, padding=(10, 0, 10, 10))
|
|
36
|
self.config_frame.pack(fill=tk.X, side=tk.BOTTOM)
|
|
37
|
self.create_config_widgets()
|
|
38
|
|
|
39
|
# --- NEW: Load default config on startup ---
|
|
40
|
# It will silently fail if config.json doesn't exist, using hardcoded defaults.
|
|
41
|
self.load_config(filepath="config.json", silent=True)
|
|
42
|
|
|
43
|
def create_image_widgets(self):
|
|
44
|
"""Creates all widgets for the Image Blender tab."""
|
|
45
|
self.image_blender = MainAlphaBlender()
|
|
46
|
|
|
47
|
ttk.Label(self.image_tab, text="Input Image Directory:").grid(row=0, column=0, sticky=tk.W, pady=2)
|
|
48
|
self.img_input_path_var = tk.StringVar(value=self.image_blender.image_path)
|
|
49
|
ttk.Entry(self.image_tab, textvariable=self.img_input_path_var, width=50).grid(row=0, column=1, sticky=tk.EW, padx=5)
|
|
50
|
ttk.Button(self.image_tab, text="Browse...", command=self.select_img_input_dir).grid(row=0, column=2)
|
|
51
|
|
|
52
|
ttk.Label(self.image_tab, text="Output Directory:").grid(row=1, column=0, sticky=tk.W, pady=2)
|
|
53
|
self.img_output_path_var = tk.StringVar(value=self.image_blender.output_dir)
|
|
54
|
ttk.Entry(self.image_tab, textvariable=self.img_output_path_var, width=50).grid(row=1, column=1, sticky=tk.EW, padx=5)
|
|
55
|
ttk.Button(self.image_tab, text="Browse...", command=self.select_img_output_dir).grid(row=1, column=2)
|
|
56
|
|
|
57
|
ttk.Label(self.image_tab, text="Blend Width (pixels):").grid(row=2, column=0, sticky=tk.W, pady=5)
|
|
58
|
self.img_blend_width_var = tk.IntVar(value=self.image_blender.blend_width)
|
|
59
|
ttk.Entry(self.image_tab, textvariable=self.img_blend_width_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=5)
|
|
60
|
|
|
61
|
ttk.Label(self.image_tab, text="Gamma Value:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
|
62
|
self.img_gamma_var = tk.DoubleVar(value=self.image_blender.gamma_value)
|
|
63
|
ttk.Entry(self.image_tab, textvariable=self.img_gamma_var, width=10).grid(row=3, column=1, sticky=tk.W, padx=5)
|
|
64
|
|
|
65
|
ttk.Label(self.image_tab, text="Blend Method:").grid(row=4, column=0, sticky=tk.W, pady=2)
|
|
66
|
self.img_method_var = tk.StringVar(value=self.image_blender.method)
|
|
67
|
methods = ['linear', 'cosine', 'quadratic', 'sqrt', 'log', 'sigmoid']
|
|
68
|
ttk.Combobox(self.image_tab, textvariable=self.img_method_var, values=methods, state="readonly").grid(row=4, column=1, sticky=tk.W, padx=5)
|
|
69
|
|
|
70
|
self.img_preview_var = tk.BooleanVar(value=self.image_blender.preview)
|
|
71
|
ttk.Checkbutton(self.image_tab, text="Show Preview After Processing", variable=self.img_preview_var).grid(row=5, column=1, sticky=tk.W, pady=10, padx=5)
|
|
72
|
|
|
73
|
ttk.Button(self.image_tab, text="Run Blending Process", command=self.run_image_blending).grid(row=6, column=1, pady=20, sticky=tk.W)
|
|
74
|
|
|
75
|
self.img_status_var = tk.StringVar(value="Ready.")
|
|
76
|
ttk.Label(self.image_tab, textvariable=self.img_status_var, font=("Helvetica", 10, "italic")).grid(row=7, column=0, columnspan=3, sticky=tk.W, pady=5)
|
|
77
|
|
|
78
|
self.image_tab.columnconfigure(1, weight=1)
|
|
79
|
|
|
80
|
def create_video_widgets(self):
|
|
81
|
"""Creates all widgets for the Video Processor tab."""
|
|
82
|
self.video_processor = VideoProcessor()
|
|
83
|
|
|
84
|
ttk.Label(self.video_tab, text="Input Video File:").grid(row=0, column=0, sticky=tk.W, pady=2)
|
|
85
|
self.vid_input_path_var = tk.StringVar()
|
|
86
|
ttk.Entry(self.video_tab, textvariable=self.vid_input_path_var, width=50).grid(row=0, column=1, sticky=tk.EW, padx=5)
|
|
87
|
ttk.Button(self.video_tab, text="Browse...", command=self.select_vid_input_file).grid(row=0, column=2)
|
|
88
|
|
|
89
|
ttk.Label(self.video_tab, text="Output Directory:").grid(row=1, column=0, sticky=tk.W, pady=2)
|
|
90
|
self.vid_output_path_var = tk.StringVar(value=self.video_processor.output_dir)
|
|
91
|
ttk.Entry(self.video_tab, textvariable=self.vid_output_path_var, width=50).grid(row=1, column=1, sticky=tk.EW, padx=5)
|
|
92
|
ttk.Button(self.video_tab, text="Browse...", command=self.select_vid_output_dir).grid(row=1, column=2)
|
|
93
|
|
|
94
|
ttk.Label(self.video_tab, text="Blend Width (pixels):").grid(row=2, column=0, sticky=tk.W, pady=5)
|
|
95
|
self.vid_blend_width_var = tk.IntVar(value=self.video_processor.blend_width)
|
|
96
|
ttk.Entry(self.video_tab, textvariable=self.vid_blend_width_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=5)
|
|
97
|
|
|
98
|
ttk.Label(self.video_tab, text="Blend Method:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
|
99
|
self.vid_method_var = tk.StringVar(value=self.video_processor.blend_method)
|
|
100
|
methods = ['linear', 'cosine']
|
|
101
|
ttk.Combobox(self.video_tab, textvariable=self.vid_method_var, values=methods, state="readonly").grid(row=3, column=1, sticky=tk.W, padx=5)
|
|
102
|
|
|
103
|
self.run_video_button = ttk.Button(self.video_tab, text="Process Video", command=self.run_video_processing_thread)
|
|
104
|
self.run_video_button.grid(row=4, column=1, pady=20, sticky=tk.W)
|
|
105
|
|
|
106
|
self.vid_status_var = tk.StringVar(value="Ready.")
|
|
107
|
ttk.Label(self.video_tab, textvariable=self.vid_status_var).grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=5)
|
|
108
|
|
|
109
|
self.video_tab.columnconfigure(1, weight=1)
|
|
110
|
|
|
111
|
def create_config_widgets(self):
|
|
112
|
"""Creates the Load and Save configuration buttons."""
|
|
113
|
ttk.Button(self.config_frame, text="Load Config", command=self.load_config).pack(side=tk.LEFT, padx=5)
|
|
114
|
ttk.Button(self.config_frame, text="Save Config", command=self.save_config).pack(side=tk.LEFT, padx=5)
|
|
115
|
|
|
116
|
def load_config(self, filepath=None, silent=False):
|
|
117
|
"""Loads settings from a JSON file and updates the GUI."""
|
|
118
|
if filepath is None:
|
|
119
|
filepath = filedialog.askopenfilename(
|
|
120
|
title="Open Configuration File",
|
|
121
|
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
|
|
122
|
)
|
|
123
|
|
|
124
|
if not filepath or not os.path.exists(filepath):
|
|
125
|
if not silent:
|
|
126
|
messagebox.showwarning("Load Config", "No configuration file selected or file not found.")
|
|
127
|
return
|
|
128
|
|
|
129
|
try:
|
|
130
|
with open(filepath, 'r') as f:
|
|
131
|
data = json.load(f)
|
|
132
|
|
|
133
|
# Update Image Tab variables
|
|
134
|
self.img_input_path_var.set(data.get("image_path", "OriginalImages"))
|
|
135
|
self.img_output_path_var.set(data.get("output_dir", "Results"))
|
|
136
|
self.img_blend_width_var.set(data.get("blend_width", 200))
|
|
137
|
self.img_gamma_var.set(data.get("gamma_value", 1.4))
|
|
138
|
self.img_method_var.set(data.get("blend_method", "cosine"))
|
|
139
|
self.img_preview_var.set(data.get("preview", True))
|
|
140
|
|
|
141
|
# Update Video Tab variables
|
|
142
|
self.vid_input_path_var.set(data.get("video_input_path", ""))
|
|
143
|
self.vid_output_path_var.set(data.get("video_output_dir", "VideoResults"))
|
|
144
|
self.vid_blend_width_var.set(data.get("video_blend_width", 100))
|
|
145
|
self.vid_method_var.set(data.get("video_blend_method", "linear"))
|
|
146
|
|
|
147
|
if not silent:
|
|
148
|
messagebox.showinfo("Load Config", f"Configuration loaded successfully from {os.path.basename(filepath)}.")
|
|
149
|
|
|
150
|
except Exception as e:
|
|
151
|
if not silent:
|
|
152
|
messagebox.showerror("Load Config Error", f"Failed to load or parse the configuration file.\n\nError: {e}")
|
|
153
|
|
|
154
|
def save_config(self):
|
|
155
|
"""Saves the current GUI settings to a JSON file."""
|
|
156
|
filepath = filedialog.asksaveasfilename(
|
|
157
|
title="Save Configuration File",
|
|
158
|
defaultextension=".json",
|
|
159
|
initialfile="config.json",
|
|
160
|
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
|
|
161
|
)
|
|
162
|
|
|
163
|
if not filepath:
|
|
164
|
return
|
|
165
|
|
|
166
|
try:
|
|
167
|
config_data = {
|
|
168
|
# Image Tab settings
|
|
169
|
"image_path": self.img_input_path_var.get(),
|
|
170
|
"output_dir": self.img_output_path_var.get(),
|
|
171
|
"blend_width": self.img_blend_width_var.get(),
|
|
172
|
"gamma_value": self.img_gamma_var.get(),
|
|
173
|
"blend_method": self.img_method_var.get(),
|
|
174
|
"preview": self.img_preview_var.get(),
|
|
175
|
|
|
176
|
# Video Tab settings
|
|
177
|
"video_input_path": self.vid_input_path_var.get(),
|
|
178
|
"video_output_dir": self.vid_output_path_var.get(),
|
|
179
|
"video_blend_width": self.vid_blend_width_var.get(),
|
|
180
|
"video_blend_method": self.vid_method_var.get()
|
|
181
|
}
|
|
182
|
|
|
183
|
with open(filepath, 'w') as f:
|
|
184
|
json.dump(config_data, f, indent=4)
|
|
185
|
|
|
186
|
messagebox.showinfo("Save Config", f"Configuration saved successfully to {os.path.basename(filepath)}.")
|
|
187
|
|
|
188
|
except Exception as e:
|
|
189
|
messagebox.showerror("Save Config Error", f"Failed to save the configuration file.\n\nError: {e}")
|
|
190
|
|
|
191
|
# --- Callbacks for Image Tab ---
|
|
192
|
def select_img_input_dir(self):
|
|
193
|
path = filedialog.askdirectory(title="Select Input Image Directory")
|
|
194
|
if path: self.img_input_path_var.set(path)
|
|
195
|
|
|
196
|
def select_img_output_dir(self):
|
|
197
|
path = filedialog.askdirectory(title="Select Output Directory")
|
|
198
|
if path: self.img_output_path_var.set(path)
|
|
199
|
|
|
200
|
def run_image_blending(self):
|
|
201
|
self.image_blender.image_path = self.img_input_path_var.get()
|
|
202
|
self.image_blender.output_dir = self.img_output_path_var.get()
|
|
203
|
self.image_blender.blend_width = self.img_blend_width_var.get()
|
|
204
|
self.image_blender.gamma_value = self.img_gamma_var.get()
|
|
205
|
self.image_blender.method = self.img_method_var.get()
|
|
206
|
self.image_blender.preview = self.img_preview_var.get()
|
|
207
|
self.image_blender.update_paths()
|
|
208
|
|
|
209
|
success, message = self.image_blender.run()
|
|
210
|
if success:
|
|
211
|
self.img_status_var.set(f"Success! {message}")
|
|
212
|
messagebox.showinfo("Success", message)
|
|
213
|
else:
|
|
214
|
self.img_status_var.set(f"Error: {message}")
|
|
215
|
messagebox.showerror("Error", message)
|
|
216
|
|
|
217
|
# --- Callbacks for Video Tab ---
|
|
218
|
def select_vid_input_file(self):
|
|
219
|
path = filedialog.askopenfilename(title="Select Input Video File", filetypes=[("MP4 files", "*.mp4"), ("All files", "*.*")])
|
|
220
|
if path: self.vid_input_path_var.set(path)
|
|
221
|
|
|
222
|
def select_vid_output_dir(self):
|
|
223
|
path = filedialog.askdirectory(title="Select Output Directory")
|
|
224
|
if path: self.vid_output_path_var.set(path)
|
|
225
|
|
|
226
|
def update_video_status(self, message):
|
|
227
|
"""Thread-safe method to update the GUI status label."""
|
|
228
|
self.vid_status_var.set(message)
|
|
229
|
|
|
230
|
def run_video_processing_thread(self):
|
|
231
|
"""Starts the video processing in a new thread to avoid freezing the GUI."""
|
|
232
|
self.run_video_button.config(state="disabled")
|
|
233
|
thread = threading.Thread(target=self.run_video_processing)
|
|
234
|
thread.daemon = True
|
|
235
|
thread.start()
|
|
236
|
|
|
237
|
def run_video_processing(self):
|
|
238
|
"""The actual processing logic, run in the background thread."""
|
|
239
|
try:
|
|
240
|
self.video_processor.input_video_path = self.vid_input_path_var.get()
|
|
241
|
self.video_processor.output_dir = self.vid_output_path_var.get()
|
|
242
|
self.video_processor.blend_width = self.vid_blend_width_var.get()
|
|
243
|
self.video_processor.blend_method = self.vid_method_var.get()
|
|
244
|
|
|
245
|
success, message = self.video_processor.run(status_callback=self.update_video_status)
|
|
246
|
|
|
247
|
if success:
|
|
248
|
messagebox.showinfo("Success", message)
|
|
249
|
else:
|
|
250
|
messagebox.showerror("Error", message)
|
|
251
|
|
|
252
|
except Exception as e:
|
|
253
|
messagebox.showerror("Critical Error", f"An unexpected error occurred: {e}")
|
|
254
|
finally:
|
|
255
|
self.run_video_button.config(state="normal")
|