Actions
Codes » History » Revision 6
« Previous |
Revision 6/7
(diff)
| Next »
Zhi Jie YEW, 11/06/2025 02:51 PM
Codes¶
gui.py
# gui.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
import json
import os
# Import the logic classes
from main_alpha_blender import MainAlphaBlender
from video_processor import VideoProcessor
class BlenderGUI:
"""A Tkinter GUI with tabs for image and video edge blending."""
def __init__(self, master):
self.master = master
master.title("Image and Video Edge Blender")
master.geometry("600x450") # Increased height for new buttons
# --- Create a Tabbed Interface ---
self.notebook = ttk.Notebook(master)
self.notebook.pack(pady=10, padx=10, fill="both", expand=True)
self.image_tab = ttk.Frame(self.notebook, padding="10")
self.video_tab = ttk.Frame(self.notebook, padding="10")
self.notebook.add(self.image_tab, text="Image Blender")
self.notebook.add(self.video_tab, text="Video Processor")
# --- Populate each tab ---
self.create_image_widgets()
self.create_video_widgets()
# --- NEW: Add a frame at the bottom for config management ---
self.config_frame = ttk.Frame(master, padding=(10, 0, 10, 10))
self.config_frame.pack(fill=tk.X, side=tk.BOTTOM)
self.create_config_widgets()
# --- NEW: Load default config on startup ---
# It will silently fail if config.json doesn't exist, using hardcoded defaults.
self.load_config(filepath="config.json", silent=True)
def create_image_widgets(self):
"""Creates all widgets for the Image Blender tab."""
self.image_blender = MainAlphaBlender()
ttk.Label(self.image_tab, text="Input Image Directory:").grid(row=0, column=0, sticky=tk.W, pady=2)
self.img_input_path_var = tk.StringVar(value=self.image_blender.image_path)
ttk.Entry(self.image_tab, textvariable=self.img_input_path_var, width=50).grid(row=0, column=1, sticky=tk.EW, padx=5)
ttk.Button(self.image_tab, text="Browse...", command=self.select_img_input_dir).grid(row=0, column=2)
ttk.Label(self.image_tab, text="Output Directory:").grid(row=1, column=0, sticky=tk.W, pady=2)
self.img_output_path_var = tk.StringVar(value=self.image_blender.output_dir)
ttk.Entry(self.image_tab, textvariable=self.img_output_path_var, width=50).grid(row=1, column=1, sticky=tk.EW, padx=5)
ttk.Button(self.image_tab, text="Browse...", command=self.select_img_output_dir).grid(row=1, column=2)
ttk.Label(self.image_tab, text="Blend Width (pixels):").grid(row=2, column=0, sticky=tk.W, pady=5)
self.img_blend_width_var = tk.IntVar(value=self.image_blender.blend_width)
ttk.Entry(self.image_tab, textvariable=self.img_blend_width_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=5)
ttk.Label(self.image_tab, text="Gamma Value:").grid(row=3, column=0, sticky=tk.W, pady=2)
self.img_gamma_var = tk.DoubleVar(value=self.image_blender.gamma_value)
ttk.Entry(self.image_tab, textvariable=self.img_gamma_var, width=10).grid(row=3, column=1, sticky=tk.W, padx=5)
ttk.Label(self.image_tab, text="Blend Method:").grid(row=4, column=0, sticky=tk.W, pady=2)
self.img_method_var = tk.StringVar(value=self.image_blender.method)
methods = ['linear', 'cosine', 'quadratic', 'sqrt', 'log', 'sigmoid']
ttk.Combobox(self.image_tab, textvariable=self.img_method_var, values=methods, state="readonly").grid(row=4, column=1, sticky=tk.W, padx=5)
self.img_preview_var = tk.BooleanVar(value=self.image_blender.preview)
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)
ttk.Button(self.image_tab, text="Run Blending Process", command=self.run_image_blending).grid(row=6, column=1, pady=20, sticky=tk.W)
self.img_status_var = tk.StringVar(value="Ready.")
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)
self.image_tab.columnconfigure(1, weight=1)
def create_video_widgets(self):
"""Creates all widgets for the Video Processor tab."""
self.video_processor = VideoProcessor()
ttk.Label(self.video_tab, text="Input Video File:").grid(row=0, column=0, sticky=tk.W, pady=2)
self.vid_input_path_var = tk.StringVar()
ttk.Entry(self.video_tab, textvariable=self.vid_input_path_var, width=50).grid(row=0, column=1, sticky=tk.EW, padx=5)
ttk.Button(self.video_tab, text="Browse...", command=self.select_vid_input_file).grid(row=0, column=2)
ttk.Label(self.video_tab, text="Output Directory:").grid(row=1, column=0, sticky=tk.W, pady=2)
self.vid_output_path_var = tk.StringVar(value=self.video_processor.output_dir)
ttk.Entry(self.video_tab, textvariable=self.vid_output_path_var, width=50).grid(row=1, column=1, sticky=tk.EW, padx=5)
ttk.Button(self.video_tab, text="Browse...", command=self.select_vid_output_dir).grid(row=1, column=2)
ttk.Label(self.video_tab, text="Blend Width (pixels):").grid(row=2, column=0, sticky=tk.W, pady=5)
self.vid_blend_width_var = tk.IntVar(value=self.video_processor.blend_width)
ttk.Entry(self.video_tab, textvariable=self.vid_blend_width_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=5)
ttk.Label(self.video_tab, text="Blend Method:").grid(row=3, column=0, sticky=tk.W, pady=2)
self.vid_method_var = tk.StringVar(value=self.video_processor.blend_method)
methods = ['linear', 'cosine']
ttk.Combobox(self.video_tab, textvariable=self.vid_method_var, values=methods, state="readonly").grid(row=3, column=1, sticky=tk.W, padx=5)
self.run_video_button = ttk.Button(self.video_tab, text="Process Video", command=self.run_video_processing_thread)
self.run_video_button.grid(row=4, column=1, pady=20, sticky=tk.W)
self.vid_status_var = tk.StringVar(value="Ready.")
ttk.Label(self.video_tab, textvariable=self.vid_status_var).grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=5)
self.video_tab.columnconfigure(1, weight=1)
def create_config_widgets(self):
"""Creates the Load and Save configuration buttons."""
ttk.Button(self.config_frame, text="Load Config", command=self.load_config).pack(side=tk.LEFT, padx=5)
ttk.Button(self.config_frame, text="Save Config", command=self.save_config).pack(side=tk.LEFT, padx=5)
def load_config(self, filepath=None, silent=False):
"""Loads settings from a JSON file and updates the GUI."""
if filepath is None:
filepath = filedialog.askopenfilename(
title="Open Configuration File",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if not filepath or not os.path.exists(filepath):
if not silent:
messagebox.showwarning("Load Config", "No configuration file selected or file not found.")
return
try:
with open(filepath, 'r') as f:
data = json.load(f)
# Update Image Tab variables
self.img_input_path_var.set(data.get("image_path", "OriginalImages"))
self.img_output_path_var.set(data.get("output_dir", "Results"))
self.img_blend_width_var.set(data.get("blend_width", 200))
self.img_gamma_var.set(data.get("gamma_value", 1.4))
self.img_method_var.set(data.get("blend_method", "cosine"))
self.img_preview_var.set(data.get("preview", True))
# Update Video Tab variables
self.vid_input_path_var.set(data.get("video_input_path", ""))
self.vid_output_path_var.set(data.get("video_output_dir", "VideoResults"))
self.vid_blend_width_var.set(data.get("video_blend_width", 100))
self.vid_method_var.set(data.get("video_blend_method", "linear"))
if not silent:
messagebox.showinfo("Load Config", f"Configuration loaded successfully from {os.path.basename(filepath)}.")
except Exception as e:
if not silent:
messagebox.showerror("Load Config Error", f"Failed to load or parse the configuration file.\n\nError: {e}")
def save_config(self):
"""Saves the current GUI settings to a JSON file."""
filepath = filedialog.asksaveasfilename(
title="Save Configuration File",
defaultextension=".json",
initialfile="config.json",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if not filepath:
return
try:
config_data = {
# Image Tab settings
"image_path": self.img_input_path_var.get(),
"output_dir": self.img_output_path_var.get(),
"blend_width": self.img_blend_width_var.get(),
"gamma_value": self.img_gamma_var.get(),
"blend_method": self.img_method_var.get(),
"preview": self.img_preview_var.get(),
# Video Tab settings
"video_input_path": self.vid_input_path_var.get(),
"video_output_dir": self.vid_output_path_var.get(),
"video_blend_width": self.vid_blend_width_var.get(),
"video_blend_method": self.vid_method_var.get()
}
with open(filepath, 'w') as f:
json.dump(config_data, f, indent=4)
messagebox.showinfo("Save Config", f"Configuration saved successfully to {os.path.basename(filepath)}.")
except Exception as e:
messagebox.showerror("Save Config Error", f"Failed to save the configuration file.\n\nError: {e}")
# --- Callbacks for Image Tab ---
def select_img_input_dir(self):
path = filedialog.askdirectory(title="Select Input Image Directory")
if path: self.img_input_path_var.set(path)
def select_img_output_dir(self):
path = filedialog.askdirectory(title="Select Output Directory")
if path: self.img_output_path_var.set(path)
def run_image_blending(self):
self.image_blender.image_path = self.img_input_path_var.get()
self.image_blender.output_dir = self.img_output_path_var.get()
self.image_blender.blend_width = self.img_blend_width_var.get()
self.image_blender.gamma_value = self.img_gamma_var.get()
self.image_blender.method = self.img_method_var.get()
self.image_blender.preview = self.img_preview_var.get()
self.image_blender.update_paths()
success, message = self.image_blender.run()
if success:
self.img_status_var.set(f"Success! {message}")
messagebox.showinfo("Success", message)
else:
self.img_status_var.set(f"Error: {message}")
messagebox.showerror("Error", message)
# --- Callbacks for Video Tab ---
def select_vid_input_file(self):
path = filedialog.askopenfilename(title="Select Input Video File", filetypes=[("MP4 files", "*.mp4"), ("All files", "*.*")])
if path: self.vid_input_path_var.set(path)
def select_vid_output_dir(self):
path = filedialog.askdirectory(title="Select Output Directory")
if path: self.vid_output_path_var.set(path)
def update_video_status(self, message):
"""Thread-safe method to update the GUI status label."""
self.vid_status_var.set(message)
def run_video_processing_thread(self):
"""Starts the video processing in a new thread to avoid freezing the GUI."""
self.run_video_button.config(state="disabled")
thread = threading.Thread(target=self.run_video_processing)
thread.daemon = True
thread.start()
def run_video_processing(self):
"""The actual processing logic, run in the background thread."""
try:
self.video_processor.input_video_path = self.vid_input_path_var.get()
self.video_processor.output_dir = self.vid_output_path_var.get()
self.video_processor.blend_width = self.vid_blend_width_var.get()
self.video_processor.blend_method = self.vid_method_var.get()
success, message = self.video_processor.run(status_callback=self.update_video_status)
if success:
messagebox.showinfo("Success", message)
else:
messagebox.showerror("Error", message)
except Exception as e:
messagebox.showerror("Critical Error", f"An unexpected error occurred: {e}")
finally:
self.run_video_button.config(state="normal")
# gui.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
import json
import os
# Import the logic classes
from main_alpha_blender import MainAlphaBlender
from video_processor import VideoProcessor
class BlenderGUI:
"""A Tkinter GUI with tabs for image and video edge blending."""
def __init__(self, master):
self.master = master
master.title("Image and Video Edge Blender")
master.geometry("600x450") # Increased height for new buttons
# --- Create a Tabbed Interface ---
self.notebook = ttk.Notebook(master)
self.notebook.pack(pady=10, padx=10, fill="both", expand=True)
self.image_tab = ttk.Frame(self.notebook, padding="10")
self.video_tab = ttk.Frame(self.notebook, padding="10")
self.notebook.add(self.image_tab, text="Image Blender")
self.notebook.add(self.video_tab, text="Video Processor")
# --- Populate each tab ---
self.create_image_widgets()
self.create_video_widgets()
# --- NEW: Add a frame at the bottom for config management ---
self.config_frame = ttk.Frame(master, padding=(10, 0, 10, 10))
self.config_frame.pack(fill=tk.X, side=tk.BOTTOM)
self.create_config_widgets()
# --- NEW: Load default config on startup ---
# It will silently fail if config.json doesn't exist, using hardcoded defaults.
self.load_config(filepath="config.json", silent=True)
def create_image_widgets(self):
"""Creates all widgets for the Image Blender tab."""
self.image_blender = MainAlphaBlender()
ttk.Label(self.image_tab, text="Input Image Directory:").grid(row=0, column=0, sticky=tk.W, pady=2)
self.img_input_path_var = tk.StringVar(value=self.image_blender.image_path)
ttk.Entry(self.image_tab, textvariable=self.img_input_path_var, width=50).grid(row=0, column=1, sticky=tk.EW, padx=5)
ttk.Button(self.image_tab, text="Browse...", command=self.select_img_input_dir).grid(row=0, column=2)
ttk.Label(self.image_tab, text="Output Directory:").grid(row=1, column=0, sticky=tk.W, pady=2)
self.img_output_path_var = tk.StringVar(value=self.image_blender.output_dir)
ttk.Entry(self.image_tab, textvariable=self.img_output_path_var, width=50).grid(row=1, column=1, sticky=tk.EW, padx=5)
ttk.Button(self.image_tab, text="Browse...", command=self.select_img_output_dir).grid(row=1, column=2)
ttk.Label(self.image_tab, text="Blend Width (pixels):").grid(row=2, column=0, sticky=tk.W, pady=5)
self.img_blend_width_var = tk.IntVar(value=self.image_blender.blend_width)
ttk.Entry(self.image_tab, textvariable=self.img_blend_width_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=5)
ttk.Label(self.image_tab, text="Gamma Value:").grid(row=3, column=0, sticky=tk.W, pady=2)
self.img_gamma_var = tk.DoubleVar(value=self.image_blender.gamma_value)
ttk.Entry(self.image_tab, textvariable=self.img_gamma_var, width=10).grid(row=3, column=1, sticky=tk.W, padx=5)
ttk.Label(self.image_tab, text="Blend Method:").grid(row=4, column=0, sticky=tk.W, pady=2)
self.img_method_var = tk.StringVar(value=self.image_blender.method)
methods = ['linear', 'cosine', 'quadratic', 'sqrt', 'log', 'sigmoid']
ttk.Combobox(self.image_tab, textvariable=self.img_method_var, values=methods, state="readonly").grid(row=4, column=1, sticky=tk.W, padx=5)
self.img_preview_var = tk.BooleanVar(value=self.image_blender.preview)
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)
ttk.Button(self.image_tab, text="Run Blending Process", command=self.run_image_blending).grid(row=6, column=1, pady=20, sticky=tk.W)
self.img_status_var = tk.StringVar(value="Ready.")
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)
self.image_tab.columnconfigure(1, weight=1)
def create_video_widgets(self):
"""Creates all widgets for the Video Processor tab."""
self.video_processor = VideoProcessor()
ttk.Label(self.video_tab, text="Input Video File:").grid(row=0, column=0, sticky=tk.W, pady=2)
self.vid_input_path_var = tk.StringVar()
ttk.Entry(self.video_tab, textvariable=self.vid_input_path_var, width=50).grid(row=0, column=1, sticky=tk.EW, padx=5)
ttk.Button(self.video_tab, text="Browse...", command=self.select_vid_input_file).grid(row=0, column=2)
ttk.Label(self.video_tab, text="Output Directory:").grid(row=1, column=0, sticky=tk.W, pady=2)
self.vid_output_path_var = tk.StringVar(value=self.video_processor.output_dir)
ttk.Entry(self.video_tab, textvariable=self.vid_output_path_var, width=50).grid(row=1, column=1, sticky=tk.EW, padx=5)
ttk.Button(self.video_tab, text="Browse...", command=self.select_vid_output_dir).grid(row=1, column=2)
ttk.Label(self.video_tab, text="Blend Width (pixels):").grid(row=2, column=0, sticky=tk.W, pady=5)
self.vid_blend_width_var = tk.IntVar(value=self.video_processor.blend_width)
ttk.Entry(self.video_tab, textvariable=self.vid_blend_width_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=5)
ttk.Label(self.video_tab, text="Blend Method:").grid(row=3, column=0, sticky=tk.W, pady=2)
self.vid_method_var = tk.StringVar(value=self.video_processor.blend_method)
methods = ['linear', 'cosine']
ttk.Combobox(self.video_tab, textvariable=self.vid_method_var, values=methods, state="readonly").grid(row=3, column=1, sticky=tk.W, padx=5)
self.run_video_button = ttk.Button(self.video_tab, text="Process Video", command=self.run_video_processing_thread)
self.run_video_button.grid(row=4, column=1, pady=20, sticky=tk.W)
self.vid_status_var = tk.StringVar(value="Ready.")
ttk.Label(self.video_tab, textvariable=self.vid_status_var).grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=5)
self.video_tab.columnconfigure(1, weight=1)
def create_config_widgets(self):
"""Creates the Load and Save configuration buttons."""
ttk.Button(self.config_frame, text="Load Config", command=self.load_config).pack(side=tk.LEFT, padx=5)
ttk.Button(self.config_frame, text="Save Config", command=self.save_config).pack(side=tk.LEFT, padx=5)
def load_config(self, filepath=None, silent=False):
"""Loads settings from a JSON file and updates the GUI."""
if filepath is None:
filepath = filedialog.askopenfilename(
title="Open Configuration File",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if not filepath or not os.path.exists(filepath):
if not silent:
messagebox.showwarning("Load Config", "No configuration file selected or file not found.")
return
try:
with open(filepath, 'r') as f:
data = json.load(f)
# Update Image Tab variables
self.img_input_path_var.set(data.get("image_path", "OriginalImages"))
self.img_output_path_var.set(data.get("output_dir", "Results"))
self.img_blend_width_var.set(data.get("blend_width", 200))
self.img_gamma_var.set(data.get("gamma_value", 1.4))
self.img_method_var.set(data.get("blend_method", "cosine"))
self.img_preview_var.set(data.get("preview", True))
# Update Video Tab variables
self.vid_input_path_var.set(data.get("video_input_path", ""))
self.vid_output_path_var.set(data.get("video_output_dir", "VideoResults"))
self.vid_blend_width_var.set(data.get("video_blend_width", 100))
self.vid_method_var.set(data.get("video_blend_method", "linear"))
if not silent:
messagebox.showinfo("Load Config", f"Configuration loaded successfully from {os.path.basename(filepath)}.")
except Exception as e:
if not silent:
messagebox.showerror("Load Config Error", f"Failed to load or parse the configuration file.\n\nError: {e}")
def save_config(self):
"""Saves the current GUI settings to a JSON file."""
filepath = filedialog.asksaveasfilename(
title="Save Configuration File",
defaultextension=".json",
initialfile="config.json",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if not filepath:
return
try:
config_data = {
# Image Tab settings
"image_path": self.img_input_path_var.get(),
"output_dir": self.img_output_path_var.get(),
"blend_width": self.img_blend_width_var.get(),
"gamma_value": self.img_gamma_var.get(),
"blend_method": self.img_method_var.get(),
"preview": self.img_preview_var.get(),
# Video Tab settings
"video_input_path": self.vid_input_path_var.get(),
"video_output_dir": self.vid_output_path_var.get(),
"video_blend_width": self.vid_blend_width_var.get(),
"video_blend_method": self.vid_method_var.get()
}
with open(filepath, 'w') as f:
json.dump(config_data, f, indent=4)
messagebox.showinfo("Save Config", f"Configuration saved successfully to {os.path.basename(filepath)}.")
except Exception as e:
messagebox.showerror("Save Config Error", f"Failed to save the configuration file.\n\nError: {e}")
# --- Callbacks for Image Tab ---
def select_img_input_dir(self):
path = filedialog.askdirectory(title="Select Input Image Directory")
if path: self.img_input_path_var.set(path)
def select_img_output_dir(self):
path = filedialog.askdirectory(title="Select Output Directory")
if path: self.img_output_path_var.set(path)
def run_image_blending(self):
self.image_blender.image_path = self.img_input_path_var.get()
self.image_blender.output_dir = self.img_output_path_var.get()
self.image_blender.blend_width = self.img_blend_width_var.get()
self.image_blender.gamma_value = self.img_gamma_var.get()
self.image_blender.method = self.img_method_var.get()
self.image_blender.preview = self.img_preview_var.get()
self.image_blender.update_paths()
success, message = self.image_blender.run()
if success:
self.img_status_var.set(f"Success! {message}")
messagebox.showinfo("Success", message)
else:
self.img_status_var.set(f"Error: {message}")
messagebox.showerror("Error", message)
# --- Callbacks for Video Tab ---
def select_vid_input_file(self):
path = filedialog.askopenfilename(title="Select Input Video File", filetypes=[("MP4 files", "*.mp4"), ("All files", "*.*")])
if path: self.vid_input_path_var.set(path)
def select_vid_output_dir(self):
path = filedialog.askdirectory(title="Select Output Directory")
if path: self.vid_output_path_var.set(path)
def update_video_status(self, message):
"""Thread-safe method to update the GUI status label."""
self.vid_status_var.set(message)
def run_video_processing_thread(self):
"""Starts the video processing in a new thread to avoid freezing the GUI."""
self.run_video_button.config(state="disabled")
thread = threading.Thread(target=self.run_video_processing)
thread.daemon = True
thread.start()
def run_video_processing(self):
"""The actual processing logic, run in the background thread."""
try:
self.video_processor.input_video_path = self.vid_input_path_var.get()
self.video_processor.output_dir = self.vid_output_path_var.get()
self.video_processor.blend_width = self.vid_blend_width_var.get()
self.video_processor.blend_method = self.vid_method_var.get()
success, message = self.video_processor.run(status_callback=self.update_video_status)
if success:
messagebox.showinfo("Success", message)
else:
messagebox.showerror("Error", message)
except Exception as e:
messagebox.showerror("Critical Error", f"An unexpected error occurred: {e}")
finally:
self.run_video_button.config(state="normal")
main_alpha_blender.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cv2
import numpy as np
import os
from config_reader import ConfigReader
class MainAlphaBlender(object):
def __init__(self, config_path="config.json"):
try:
self.__config_reader = ConfigReader(config_path)
self.blend_width = self.__config_reader.get_blend_width()
self.gamma_value = self.__config_reader.get_gamma_value()
self.method = self.__config_reader.get_blend_method()
self.output_dir = self.__config_reader.get_output_dir()
self.preview = self.__config_reader.get_preview()
self.image_path = self.__config_reader.get_image_path()
except FileNotFoundError:
self.blend_width = 200
self.gamma_value = 1.4
self.method = "cosine"
self.output_dir = "Results"
self.preview = True
self.image_path = "OriginalImages"
self.update_paths()
def update_paths(self):
self.left_image_path = os.path.join(self.image_path, "Left.jpg")
self.right_image_path = os.path.join(self.image_path, "Right.jpg")
def create_alpha_gradient(self, blend_width, side, method="cosine"):
if method == 'linear':
alpha_gradient = np.linspace(0, 1, blend_width)
elif method == 'cosine':
t = np.linspace(0, np.pi, blend_width)
alpha_gradient = (1 - np.cos(t**0.85)) / 2
elif method == 'quadratic':
t = np.linspace(0, 1, blend_width)
alpha_gradient = t**2
elif method == 'sqrt':
t = np.linspace(0, 1, blend_width)
alpha_gradient = np.sqrt(t)
elif method == 'log':
t = np.linspace(0, 1, blend_width)
alpha_gradient = np.log1p(9 * t) / np.log1p(9)
elif method == 'sigmoid':
t = np.linspace(0, 1, blend_width)
alpha_gradient = 1 / (1 + np.exp(-12 * (t - 0.5)))
alpha_gradient = (alpha_gradient - alpha_gradient.min()) / (alpha_gradient.max() - alpha_gradient.min())
else:
raise ValueError("Invalid method: choose from 'linear', 'cosine', 'quadratic', 'sqrt', 'log', or 'sigmoid'")
if side == 'right':
alpha_gradient = 1 - alpha_gradient
return alpha_gradient
def gamma_correction(self, image, gamma):
img_float = image.astype(np.float32) / 255.0
mean_intensity = np.mean(img_float)
adaptive_gamma = gamma * (0.5 / (mean_intensity + 1e-5))
adaptive_gamma = np.clip(adaptive_gamma, 0.8, 2.0)
corrected = np.power(img_float, 1.0 / adaptive_gamma)
return np.uint8(np.clip(corrected * 255, 0, 255))
def alpha_blend_edge(self, image, blend_width, side, method="cosine"):
height, width, _ = image.shape
blended_image = image.copy()
alpha_gradient = self.create_alpha_gradient(blend_width, side, method)
if side == 'right':
roi = blended_image[:, width - blend_width:]
elif side == 'left':
roi = blended_image[:, :blend_width]
else:
raise ValueError("Side must be 'left' or 'right'")
gradient_3d = alpha_gradient[np.newaxis, :, np.newaxis]
gradient_3d = np.tile(gradient_3d, (height, 1, 3))
if side == 'right':
blended_image[:, width - blend_width:] = (roi * gradient_3d).astype(np.uint8)
else:
blended_image[:, :blend_width] = (roi * gradient_3d).astype(np.uint8)
return blended_image
def show_preview(self, left_image, right_image, scale=0.5):
h = min(left_image.shape[0], right_image.shape[0])
left_resized = cv2.resize(left_image, (int(left_image.shape[1]*scale), int(h*scale)))
right_resized = cv2.resize(right_image, (int(right_image.shape[1]*scale), int(h*scale)))
combined = np.hstack((left_resized, right_resized))
cv2.imshow("Preview (Left + Right)", combined)
cv2.waitKey(0)
cv2.destroyAllWindows()
def run(self):
try:
os.makedirs(self.output_dir, exist_ok=True)
left_img = cv2.imread(self.left_image_path, cv2.IMREAD_COLOR)
right_img = cv2.imread(self.right_image_path, cv2.IMREAD_COLOR)
if left_img is None or right_img is None:
raise FileNotFoundError(f"Could not read images from '{self.image_path}'. Check path.")
left_blended = self.alpha_blend_edge(left_img, self.blend_width, side='right', method=self.method)
right_blended = self.alpha_blend_edge(right_img, self.blend_width, side='left', method=self.method)
left_gamma = self.gamma_correction(left_blended, self.gamma_value)
right_gamma = self.gamma_correction(right_blended, self.gamma_value)
left_output_path = os.path.join(self.output_dir, f"{self.method}_left_gamma.jpg")
right_output_path = os.path.join(self.output_dir, f"{self.method}_right_gamma.jpg")
cv2.imwrite(left_output_path, left_gamma)
cv2.imwrite(right_output_path, right_gamma)
if self.preview:
self.show_preview(left_gamma, right_gamma)
return (True, f"Images saved successfully in '{self.output_dir}'.")
except (FileNotFoundError, ValueError) as e:
return (False, str(e))
except Exception as e:
return (False, f"An unexpected error occurred: {e}")
finally:
cv2.destroyAllWindows()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cv2
import numpy as np
import os
from config_reader import ConfigReader
class MainAlphaBlender(object):
def __init__(self, config_path="config.json"):
try:
self.__config_reader = ConfigReader(config_path)
self.blend_width = self.__config_reader.get_blend_width()
self.gamma_value = self.__config_reader.get_gamma_value()
self.method = self.__config_reader.get_blend_method()
self.output_dir = self.__config_reader.get_output_dir()
self.preview = self.__config_reader.get_preview()
self.image_path = self.__config_reader.get_image_path()
except FileNotFoundError:
self.blend_width = 200
self.gamma_value = 1.4
self.method = "cosine"
self.output_dir = "Results"
self.preview = True
self.image_path = "OriginalImages"
self.update_paths()
def update_paths(self):
self.left_image_path = os.path.join(self.image_path, "Left.jpg")
self.right_image_path = os.path.join(self.image_path, "Right.jpg")
def create_alpha_gradient(self, blend_width, side, method="cosine"):
if method == 'linear':
alpha_gradient = np.linspace(0, 1, blend_width)
elif method == 'cosine':
t = np.linspace(0, np.pi, blend_width)
alpha_gradient = (1 - np.cos(t**0.85)) / 2
elif method == 'quadratic':
t = np.linspace(0, 1, blend_width)
alpha_gradient = t**2
elif method == 'sqrt':
t = np.linspace(0, 1, blend_width)
alpha_gradient = np.sqrt(t)
elif method == 'log':
t = np.linspace(0, 1, blend_width)
alpha_gradient = np.log1p(9 * t) / np.log1p(9)
elif method == 'sigmoid':
t = np.linspace(0, 1, blend_width)
alpha_gradient = 1 / (1 + np.exp(-12 * (t - 0.5)))
alpha_gradient = (alpha_gradient - alpha_gradient.min()) / (alpha_gradient.max() - alpha_gradient.min())
else:
raise ValueError("Invalid method: choose from 'linear', 'cosine', 'quadratic', 'sqrt', 'log', or 'sigmoid'")
if side == 'right':
alpha_gradient = 1 - alpha_gradient
return alpha_gradient
def gamma_correction(self, image, gamma):
img_float = image.astype(np.float32) / 255.0
mean_intensity = np.mean(img_float)
adaptive_gamma = gamma * (0.5 / (mean_intensity + 1e-5))
adaptive_gamma = np.clip(adaptive_gamma, 0.8, 2.0)
corrected = np.power(img_float, 1.0 / adaptive_gamma)
return np.uint8(np.clip(corrected * 255, 0, 255))
def alpha_blend_edge(self, image, blend_width, side, method="cosine"):
height, width, _ = image.shape
blended_image = image.copy()
alpha_gradient = self.create_alpha_gradient(blend_width, side, method)
if side == 'right':
roi = blended_image[:, width - blend_width:]
elif side == 'left':
roi = blended_image[:, :blend_width]
else:
raise ValueError("Side must be 'left' or 'right'")
gradient_3d = alpha_gradient[np.newaxis, :, np.newaxis]
gradient_3d = np.tile(gradient_3d, (height, 1, 3))
if side == 'right':
blended_image[:, width - blend_width:] = (roi * gradient_3d).astype(np.uint8)
else:
blended_image[:, :blend_width] = (roi * gradient_3d).astype(np.uint8)
return blended_image
def show_preview(self, left_image, right_image, scale=0.5):
h = min(left_image.shape[0], right_image.shape[0])
left_resized = cv2.resize(left_image, (int(left_image.shape[1]*scale), int(h*scale)))
right_resized = cv2.resize(right_image, (int(right_image.shape[1]*scale), int(h*scale)))
combined = np.hstack((left_resized, right_resized))
cv2.imshow("Preview (Left + Right)", combined)
cv2.waitKey(0)
cv2.destroyAllWindows()
def run(self):
try:
os.makedirs(self.output_dir, exist_ok=True)
left_img = cv2.imread(self.left_image_path, cv2.IMREAD_COLOR)
right_img = cv2.imread(self.right_image_path, cv2.IMREAD_COLOR)
if left_img is None or right_img is None:
raise FileNotFoundError(f"Could not read images from '{self.image_path}'. Check path.")
left_blended = self.alpha_blend_edge(left_img, self.blend_width, side='right', method=self.method)
right_blended = self.alpha_blend_edge(right_img, self.blend_width, side='left', method=self.method)
left_gamma = self.gamma_correction(left_blended, self.gamma_value)
right_gamma = self.gamma_correction(right_blended, self.gamma_value)
left_output_path = os.path.join(self.output_dir, f"{self.method}_left_gamma.jpg")
right_output_path = os.path.join(self.output_dir, f"{self.method}_right_gamma.jpg")
cv2.imwrite(left_output_path, left_gamma)
cv2.imwrite(right_output_path, right_gamma)
if self.preview:
self.show_preview(left_gamma, right_gamma)
return (True, f"Images saved successfully in '{self.output_dir}'.")
except (FileNotFoundError, ValueError) as e:
return (False, str(e))
except Exception as e:
return (False, f"An unexpected error occurred: {e}")
finally:
cv2.destroyAllWindows()
main.py
import tkinter as tk
from gui import BlenderGUI
if __name__ == "__main__":
"""
Main entry point for the application.
Initializes and runs the Tkinter GUI.
"""
root = tk.Tk()
app = BlenderGUI(master=root)
root.mainloop()
import tkinter as tk
from gui import BlenderGUI
if __name__ == "__main__":
"""
Main entry point for the application.
Initializes and runs the Tkinter GUI.
"""
root = tk.Tk()
app = BlenderGUI(master=root)
root.mainloop()
video_processor.py
import cv2
import numpy as np
import os
import time
class VideoProcessor:
"""
A class to handle dividing a video and applying alpha blending to the edges.
Consolidates logic from divide_video.py, apply_alpha_blending_on_video.py, and Video_utility.py.
"""
def __init__(self, config=None):
"""Initializes the processor with default or provided settings."""
# Set default parameters
self.input_video_path = ""
self.output_dir = "VideoResults"
self.blend_width = 100
self.blend_method = "linear"
self.divide_ratio = 2/3
# Overwrite defaults with a configuration dictionary if provided
if config:
self.input_video_path = config.get("input_video_path", self.input_video_path)
self.output_dir = config.get("output_dir", self.output_dir)
self.blend_width = config.get("blend_width", self.blend_width)
self.blend_method = config.get("blend_method", self.blend_method)
self.divide_ratio = config.get("divide_ratio", self.divide_ratio)
def _create_alpha_gradient(self, blend_width, side, method):
"""Creates a 1D alpha gradient for blending."""
if method == 'linear':
alpha_gradient = np.linspace(0, 1, blend_width)
elif method == 'cosine':
t = np.linspace(0, np.pi, blend_width)
alpha_gradient = (1 - np.cos(t)) / 2
else:
raise ValueError(f"Invalid blend method: {method}")
if side == 'right':
alpha_gradient = 1 - alpha_gradient # Create a fade-out gradient
return alpha_gradient
def _blend_image_edge(self, image, blend_width, side, method):
"""Applies the alpha gradient to a single frame."""
height, width, _ = image.shape
blended_image = image.copy()
alpha_gradient = self._create_alpha_gradient(blend_width, side, method)
if side == 'right':
roi = blended_image[:, width - blend_width:]
elif side == 'left':
roi = blended_image[:, :blend_width]
else:
raise ValueError("Side must be 'left' or 'right'")
# Tile the 1D gradient to match the 3 color channels of the ROI
gradient_3d = alpha_gradient[np.newaxis, :, np.newaxis]
gradient_3d = np.tile(gradient_3d, (height, 1, 3))
if side == 'right':
blended_image[:, width - blend_width:] = (roi * gradient_3d).astype(np.uint8)
else:
blended_image[:, :blend_width] = (roi * gradient_3d).astype(np.uint8)
return blended_image
def _divide_video(self, input_path, output_left_path, output_right_path, status_callback):
"""Splits a video into two halves based on the divide_ratio."""
cap = cv2.VideoCapture(input_path)
if not cap.isOpened():
raise FileNotFoundError(f"Could not open video file: {input_path}")
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
midpoint = int(width * self.divide_ratio)
out_left = cv2.VideoWriter(output_left_path, fourcc, fps, (midpoint, height))
out_right = cv2.VideoWriter(output_right_path, fourcc, fps, (width - midpoint, height))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret: break
frame_count += 1
if status_callback and frame_count % 30 == 0: # Update status every 30 frames
progress = int((frame_count / total_frames) * 100)
status_callback(f"Dividing video... {progress}%")
out_left.write(frame[:, :midpoint])
out_right.write(frame[:, midpoint:])
cap.release()
out_left.release()
out_right.release()
def _apply_alpha_blending_to_video(self, input_path, output_path, side, status_callback):
"""Applies alpha blending to each frame of a video."""
cap = cv2.VideoCapture(input_path)
if not cap.isOpened(): raise FileNotFoundError(f"Could not open video for blending: {input_path}")
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret: break
frame_count += 1
if status_callback and frame_count % 30 == 0:
progress = int((frame_count / total_frames) * 100)
status_callback(f"Blending {side} video... {progress}%")
blended_frame = self._blend_image_edge(frame, self.blend_width, side, self.blend_method)
out.write(blended_frame)
cap.release()
out.release()
def run(self, status_callback=None):
"""Executes the full video processing pipeline."""
try:
start_time = time.time()
os.makedirs(self.output_dir, exist_ok=True)
# Define intermediate and final file paths
temp_left_path = os.path.join(self.output_dir, "temp_left.mp4")
temp_right_path = os.path.join(self.output_dir, "temp_right.mp4")
final_left_path = os.path.join(self.output_dir, "final_left.mp4")
final_right_path = os.path.join(self.output_dir, "final_right.mp4")
if status_callback: status_callback("Starting to divide video...")
self._divide_video(self.input_video_path, temp_left_path, temp_right_path, status_callback)
if status_callback: status_callback("Starting to blend left video...")
self._apply_alpha_blending_to_video(temp_left_path, final_left_path, "right", status_callback)
if status_callback: status_callback("Starting to blend right video...")
self._apply_alpha_blending_to_video(temp_right_path, final_right_path, "left", status_callback)
if status_callback: status_callback("Cleaning up temporary files...")
os.remove(temp_left_path)
os.remove(temp_right_path)
duration = time.time() - start_time
message = f"Video processing complete in {duration:.2f}s. Files saved in '{self.output_dir}'."
if status_callback: status_callback(message)
return (True, message)
except Exception as e:
if status_callback: status_callback(f"Error: {e}")
return (False, str(e))
import cv2
import numpy as np
import os
import time
class VideoProcessor:
"""
A class to handle dividing a video and applying alpha blending to the edges.
Consolidates logic from divide_video.py, apply_alpha_blending_on_video.py, and Video_utility.py.
"""
def __init__(self, config=None):
"""Initializes the processor with default or provided settings."""
# Set default parameters
self.input_video_path = ""
self.output_dir = "VideoResults"
self.blend_width = 100
self.blend_method = "linear"
self.divide_ratio = 2/3
# Overwrite defaults with a configuration dictionary if provided
if config:
self.input_video_path = config.get("input_video_path", self.input_video_path)
self.output_dir = config.get("output_dir", self.output_dir)
self.blend_width = config.get("blend_width", self.blend_width)
self.blend_method = config.get("blend_method", self.blend_method)
self.divide_ratio = config.get("divide_ratio", self.divide_ratio)
def _create_alpha_gradient(self, blend_width, side, method):
"""Creates a 1D alpha gradient for blending."""
if method == 'linear':
alpha_gradient = np.linspace(0, 1, blend_width)
elif method == 'cosine':
t = np.linspace(0, np.pi, blend_width)
alpha_gradient = (1 - np.cos(t)) / 2
else:
raise ValueError(f"Invalid blend method: {method}")
if side == 'right':
alpha_gradient = 1 - alpha_gradient # Create a fade-out gradient
return alpha_gradient
def _blend_image_edge(self, image, blend_width, side, method):
"""Applies the alpha gradient to a single frame."""
height, width, _ = image.shape
blended_image = image.copy()
alpha_gradient = self._create_alpha_gradient(blend_width, side, method)
if side == 'right':
roi = blended_image[:, width - blend_width:]
elif side == 'left':
roi = blended_image[:, :blend_width]
else:
raise ValueError("Side must be 'left' or 'right'")
# Tile the 1D gradient to match the 3 color channels of the ROI
gradient_3d = alpha_gradient[np.newaxis, :, np.newaxis]
gradient_3d = np.tile(gradient_3d, (height, 1, 3))
if side == 'right':
blended_image[:, width - blend_width:] = (roi * gradient_3d).astype(np.uint8)
else:
blended_image[:, :blend_width] = (roi * gradient_3d).astype(np.uint8)
return blended_image
def _divide_video(self, input_path, output_left_path, output_right_path, status_callback):
"""Splits a video into two halves based on the divide_ratio."""
cap = cv2.VideoCapture(input_path)
if not cap.isOpened():
raise FileNotFoundError(f"Could not open video file: {input_path}")
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
midpoint = int(width * self.divide_ratio)
out_left = cv2.VideoWriter(output_left_path, fourcc, fps, (midpoint, height))
out_right = cv2.VideoWriter(output_right_path, fourcc, fps, (width - midpoint, height))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret: break
frame_count += 1
if status_callback and frame_count % 30 == 0: # Update status every 30 frames
progress = int((frame_count / total_frames) * 100)
status_callback(f"Dividing video... {progress}%")
out_left.write(frame[:, :midpoint])
out_right.write(frame[:, midpoint:])
cap.release()
out_left.release()
out_right.release()
def _apply_alpha_blending_to_video(self, input_path, output_path, side, status_callback):
"""Applies alpha blending to each frame of a video."""
cap = cv2.VideoCapture(input_path)
if not cap.isOpened(): raise FileNotFoundError(f"Could not open video for blending: {input_path}")
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret: break
frame_count += 1
if status_callback and frame_count % 30 == 0:
progress = int((frame_count / total_frames) * 100)
status_callback(f"Blending {side} video... {progress}%")
blended_frame = self._blend_image_edge(frame, self.blend_width, side, self.blend_method)
out.write(blended_frame)
cap.release()
out.release()
def run(self, status_callback=None):
"""Executes the full video processing pipeline."""
try:
start_time = time.time()
os.makedirs(self.output_dir, exist_ok=True)
# Define intermediate and final file paths
temp_left_path = os.path.join(self.output_dir, "temp_left.mp4")
temp_right_path = os.path.join(self.output_dir, "temp_right.mp4")
final_left_path = os.path.join(self.output_dir, "final_left.mp4")
final_right_path = os.path.join(self.output_dir, "final_right.mp4")
if status_callback: status_callback("Starting to divide video...")
self._divide_video(self.input_video_path, temp_left_path, temp_right_path, status_callback)
if status_callback: status_callback("Starting to blend left video...")
self._apply_alpha_blending_to_video(temp_left_path, final_left_path, "right", status_callback)
if status_callback: status_callback("Starting to blend right video...")
self._apply_alpha_blending_to_video(temp_right_path, final_right_path, "left", status_callback)
if status_callback: status_callback("Cleaning up temporary files...")
os.remove(temp_left_path)
os.remove(temp_right_path)
duration = time.time() - start_time
message = f"Video processing complete in {duration:.2f}s. Files saved in '{self.output_dir}'."
if status_callback: status_callback(message)
return (True, message)
except Exception as e:
if status_callback: status_callback(f"Error: {e}")
return (False, str(e))
config_reader.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
class ConfigReader:
"""
ConfigReader loads configuration settings from a JSON file.
It now uses a single base path for input images.
"""
def __init__(self, json_path: str):
"""
Initialize the ConfigReader with the path to the configuration file.
:param json_path: Path to the JSON configuration file
"""
self.json_path = json_path
self.config = None
self.load_config()
def load_config(self):
"""Load configuration data from the JSON file."""
try:
with open(self.json_path, 'r', encoding='utf-8') as f:
self.config = json.load(f)
except FileNotFoundError:
raise FileNotFoundError(f"Configuration file not found: {self.json_path}")
except json.JSONDecodeError as e:
raise ValueError(f"Error decoding JSON: {e}")
def get_blend_width(self) -> int:
"""Return the blending width."""
return self.config.get("blend_width", 200)
def get_gamma_value(self) -> float:
"""Return the gamma correction value."""
return self.config.get("gamma_value", 1.0)
def get_blend_method(self) -> str:
"""Return the blending method (e.g., 'cosine', 'linear')."""
return self.config.get("blend_method", "linear")
def get_image_path(self) -> str:
"""Return the base directory of the input images."""
return self.config.get("image_path", "")
def get_output_dir(self) -> str:
"""Return the directory for output results."""
return self.config.get("output_dir", "Results")
def get_preview(self) -> bool:
"""Return the preview flag."""
return self.config.get("preview", False)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
class ConfigReader:
"""
ConfigReader loads configuration settings from a JSON file.
It now uses a single base path for input images.
"""
def __init__(self, json_path: str):
"""
Initialize the ConfigReader with the path to the configuration file.
:param json_path: Path to the JSON configuration file
"""
self.json_path = json_path
self.config = None
self.load_config()
def load_config(self):
"""Load configuration data from the JSON file."""
try:
with open(self.json_path, 'r', encoding='utf-8') as f:
self.config = json.load(f)
except FileNotFoundError:
raise FileNotFoundError(f"Configuration file not found: {self.json_path}")
except json.JSONDecodeError as e:
raise ValueError(f"Error decoding JSON: {e}")
def get_blend_width(self) -> int:
"""Return the blending width."""
return self.config.get("blend_width", 200)
def get_gamma_value(self) -> float:
"""Return the gamma correction value."""
return self.config.get("gamma_value", 1.0)
def get_blend_method(self) -> str:
"""Return the blending method (e.g., 'cosine', 'linear')."""
return self.config.get("blend_method", "linear")
def get_image_path(self) -> str:
"""Return the base directory of the input images."""
return self.config.get("image_path", "")
def get_output_dir(self) -> str:
"""Return the directory for output results."""
return self.config.get("output_dir", "Results")
def get_preview(self) -> bool:
"""Return the preview flag."""
return self.config.get("preview", False)
Updated by Zhi Jie YEW 8 days ago · 6 revisions