Project

General

Profile

Codes » History » Revision 5

Revision 4 (Zhi Jie YEW, 11/06/2025 02:48 PM) → Revision 5/7 (Zhi Jie YEW, 11/06/2025 02:50 PM)

[[Wiki|← Back to Start Page]] 

 h1. Codes 

 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> 

 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> 

 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> 

 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> 

 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>