Codes » History » Revision 6
Revision 5 (Zhi Jie YEW, 11/06/2025 02:50 PM) → Revision 6/7 (Zhi Jie YEW, 11/06/2025 02:51 PM)
[[Wiki|← Back to Start Page]] h1. Codes h2. gui.py <pre><code class="python"> # 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") </code></pre> h2. main_alpha_blender.py <pre><code class="python"> #!/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() </code></pre> h2. main.py <pre><code class="python"> 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() </code></pre> h2. video_processor.py <pre><code class="python"> 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)) </code></pre> h2. config_reader.py <pre><code class="python"> #!/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) </code></pre>