Code » History » Version 5
Sandeep GANESAN, 11/13/2025 01:50 PM
| 1 | 1 | Yaroslav MAYDEBURA | !https://media.tenor.com/4h8aXFiZ2rEAAAAM/coding-programming.gif! |
|---|---|---|---|
| 2 | |||
| 3 | h1. 💻 Code & Documentation |
||
| 4 | |||
| 5 | --- |
||
| 6 | |||
| 7 | 2 | Yaroslav MAYDEBURA | All source code is developed collaboratively using Redmine and documented via **Doxygen**. |
| 8 | 5 | Sandeep GANESAN | This section will contain references to important code snippets, flow explanations. |
| 9 | 1 | Yaroslav MAYDEBURA | |
| 10 | --- |
||
| 11 | |||
| 12 | h2. 📁 Repository Links |
||
| 13 | 2 | Yaroslav MAYDEBURA | * [Redmine Repository (Private)](http://www.dh.is.ritsumei.ac.jp/redmine/projects/g12-2025-final-project/wiki/) |
| 14 | 4 | Sandeep GANESAN | |
| 15 | 1 | Yaroslav MAYDEBURA | |
| 16 | --- |
||
| 17 | |||
| 18 | h2. 🧩 Core Modules |
||
| 19 | |_. Module |_. Description | |
||
| 20 | | Image Processing | Core blending and correction logic | |
||
| 21 | | Calibration | Overlap detection and adjustment | |
||
| 22 | | UI / CLI | Interface for testing and visualization | |
||
| 23 | |||
| 24 | --- |
||
| 25 | |||
| 26 | 3 | Sandeep GANESAN | h2. 📘 The code |
| 27 | |||
| 28 | *ConfigReader.py* |
||
| 29 | |||
| 30 | <pre><code class="python"> |
||
| 31 | import configparser |
||
| 32 | |||
| 33 | class ConfigReader: |
||
| 34 | """ |
||
| 35 | @brief Configuration file reader class |
||
| 36 | |||
| 37 | @details This class provides methods to read, parse and retrieve |
||
| 38 | configuration parameters from INI-style configuration files. |
||
| 39 | It uses Python's configparser module internally. |
||
| 40 | """ |
||
| 41 | def __init__(self, config_path): |
||
| 42 | """ |
||
| 43 | @brief Constructor for ConfigReader class |
||
| 44 | |||
| 45 | @param config_path Path to the configuration file |
||
| 46 | @exception FileNotFoundError If configuration file is not found |
||
| 47 | """ |
||
| 48 | self.config_path = config_path |
||
| 49 | self.config = configparser.ConfigParser() |
||
| 50 | self.config.read(self.config_path) |
||
| 51 | |||
| 52 | def get_value(self, section, key, fallback=None): |
||
| 53 | """ |
||
| 54 | @brief Get value from configuration file |
||
| 55 | |||
| 56 | @param section Configuration section name |
||
| 57 | @param key Configuration key name |
||
| 58 | @param fallback Default value if key not found |
||
| 59 | @return Configuration value as string or fallback value |
||
| 60 | """ |
||
| 61 | try: |
||
| 62 | return self.config.get(section, key) |
||
| 63 | except (configparser.NoSectionError, configparser.NoOptionError): |
||
| 64 | return fallback |
||
| 65 | |||
| 66 | def get_linear_parameter(self): |
||
| 67 | """ |
||
| 68 | @brief Get linear parameter from configuration |
||
| 69 | |||
| 70 | @return Linear parameter as float value |
||
| 71 | """ |
||
| 72 | return float(self.get_value('Parameters', 'linear_parameter')) |
||
| 73 | |||
| 74 | def get_image_path(self): |
||
| 75 | """ |
||
| 76 | @brief Get image path from configuration |
||
| 77 | |||
| 78 | @return Image file path as string |
||
| 79 | """ |
||
| 80 | return self.get_value('Parameters', 'image_path') |
||
| 81 | |||
| 82 | def get_video_path(self): |
||
| 83 | """ |
||
| 84 | @brief Get video path from configuration |
||
| 85 | |||
| 86 | @return Video file path as string |
||
| 87 | """ |
||
| 88 | return self.get_value('Parameters', 'video_path') |
||
| 89 | |||
| 90 | def get_overlap(self): |
||
| 91 | """ |
||
| 92 | @brief Get overlap parameter from configuration |
||
| 93 | |||
| 94 | @return Overlap value as string |
||
| 95 | """ |
||
| 96 | return self.get_value('Parameters', 'overlap') |
||
| 97 | |||
| 98 | def get_blend_mode(self): |
||
| 99 | """ |
||
| 100 | @brief Get blend mode from configuration |
||
| 101 | |||
| 102 | @return Blend mode as string |
||
| 103 | """ |
||
| 104 | return self.get_value('Parameters', 'blend_mode') |
||
| 105 | |||
| 106 | def save_config(self): |
||
| 107 | """ |
||
| 108 | @brief Save current configuration to file |
||
| 109 | |||
| 110 | @details Writes the current configuration state back to the file. |
||
| 111 | This can be used to persist changes made to configuration values. |
||
| 112 | """ |
||
| 113 | with open(self.config_path, 'w') as configfile: |
||
| 114 | self.config.write(configfile) |
||
| 115 | </code></pre> |
||
| 116 | |||
| 117 | *ui.py* |
||
| 118 | |||
| 119 | <pre><code class="python"> |
||
| 120 | import tkinter as tk |
||
| 121 | from tkinter import ttk, messagebox |
||
| 122 | from PIL import Image, ImageTk |
||
| 123 | import os |
||
| 124 | import altmain |
||
| 125 | import video_processing |
||
| 126 | import cv2 |
||
| 127 | import config_reader |
||
| 128 | |||
| 129 | class ImageDisplayApp: |
||
| 130 | """ |
||
| 131 | @brief Main GUI application class for image processing and display |
||
| 132 | |||
| 133 | @details This class provides a graphical interface for splitting images, |
||
| 134 | processing videos, and displaying results with various blending options. |
||
| 135 | """ |
||
| 136 | def __init__(self, root): |
||
| 137 | """ |
||
| 138 | @brief Constructor for ImageDisplayApp class |
||
| 139 | |||
| 140 | @param root Tkinter root window object |
||
| 141 | """ |
||
| 142 | self.root = root |
||
| 143 | self.root.title("Image Split & Update GUI") |
||
| 144 | self.root.geometry("1000x750") |
||
| 145 | self.root.configure(bg="#f5f5f5") |
||
| 146 | self.cfg = config_reader.ConfigReader("config.ini") |
||
| 147 | |||
| 148 | self.image_paths = { |
||
| 149 | "main": self.cfg.get_image_path(), |
||
| 150 | "left": "left.png", |
||
| 151 | "right": "right.png" |
||
| 152 | } |
||
| 153 | |||
| 154 | self.image_frame = ttk.Frame(self.root, padding=10) |
||
| 155 | self.image_frame.pack(pady=20) |
||
| 156 | self.processor = altmain.ProjectionSplit() |
||
| 157 | self.video_processor = video_processing.VideoProcessing("input.mp4") |
||
| 158 | self.fps = 60 |
||
| 159 | self.is_running = True |
||
| 160 | |||
| 161 | self.labels = {} |
||
| 162 | for key in self.image_paths: |
||
| 163 | lbl = ttk.Label(self.image_frame) |
||
| 164 | lbl.pack(side=tk.LEFT, padx=10) |
||
| 165 | self.labels[key] = lbl |
||
| 166 | |||
| 167 | control_frame = ttk.Frame(self.root, padding=10) |
||
| 168 | control_frame.pack(pady=10) |
||
| 169 | |||
| 170 | ttk.Label(control_frame, text="Overlap (pixels):").grid(row=0, column=0, padx=5) |
||
| 171 | self.param_var = tk.IntVar(value=self.cfg.get_overlap()) |
||
| 172 | entry = ttk.Entry(control_frame, textvariable=self.param_var, width=8) |
||
| 173 | entry.grid(row=0, column=1, padx=5) |
||
| 174 | |||
| 175 | ttk.Label(control_frame, text="Blend Type:").grid(row=0, column=2, padx=5) |
||
| 176 | self.blend_var = tk.StringVar(value=self.cfg.get_blend_mode()) |
||
| 177 | blend_box = ttk.Combobox( |
||
| 178 | control_frame, |
||
| 179 | textvariable=self.blend_var, |
||
| 180 | values=["linear", "quadratic", "gaussian"], |
||
| 181 | state="readonly", |
||
| 182 | width=12 |
||
| 183 | ) |
||
| 184 | blend_box.grid(row=0, column=3, padx=5) |
||
| 185 | |||
| 186 | entry.bind("<FocusOut>", lambda e: self.debounced_update()) |
||
| 187 | blend_box.bind("<<ComboboxSelected>>", lambda e: self.debounced_update()) |
||
| 188 | |||
| 189 | timer_frame = ttk.Frame(self.root, padding=10) |
||
| 190 | timer_frame.pack(pady=5) |
||
| 191 | |||
| 192 | ttk.Label(timer_frame, text="Start Time (HH:MM:SS):").pack(side=tk.LEFT, padx=5) |
||
| 193 | self.start_time_var = tk.StringVar(value="14:30:00") # default example |
||
| 194 | ttk.Entry(timer_frame, textvariable=self.start_time_var, width=10).pack(side=tk.LEFT, padx=5) |
||
| 195 | |||
| 196 | ttk.Button( |
||
| 197 | timer_frame, |
||
| 198 | text="Start at Time", |
||
| 199 | command=self.schedule_start |
||
| 200 | ).pack(side=tk.LEFT, padx=10) |
||
| 201 | |||
| 202 | |||
| 203 | fullscreen_frame = ttk.Frame(self.root, padding=10) |
||
| 204 | fullscreen_frame.pack(pady=5) |
||
| 205 | |||
| 206 | ttk.Button( |
||
| 207 | fullscreen_frame, |
||
| 208 | text="Show Left Fullscreen", |
||
| 209 | command=lambda: self.show_fullscreen("left") |
||
| 210 | ).pack(side=tk.LEFT, padx=10) |
||
| 211 | |||
| 212 | ttk.Button( |
||
| 213 | fullscreen_frame, |
||
| 214 | text="Show Right Fullscreen", |
||
| 215 | command=lambda: self.show_fullscreen("right") |
||
| 216 | ).pack(side=tk.LEFT, padx=10) |
||
| 217 | ttk.Button( |
||
| 218 | fullscreen_frame, |
||
| 219 | text="Run Video", |
||
| 220 | command=self.run_video |
||
| 221 | ).pack(side=tk.LEFT, padx=10) |
||
| 222 | |||
| 223 | self._update_after_id = None |
||
| 224 | |||
| 225 | def debounced_update(self): |
||
| 226 | """ |
||
| 227 | @brief Debounced update to prevent rapid consecutive calls |
||
| 228 | |||
| 229 | @details Cancels pending updates and schedules a new one after delay |
||
| 230 | to prevent multiple rapid updates when parameters change. |
||
| 231 | """ |
||
| 232 | if self._update_after_id is not None: |
||
| 233 | self.root.after_cancel(self._update_after_id) |
||
| 234 | self._update_after_id = self.root.after(500, self.run_and_update) |
||
| 235 | |||
| 236 | def show_fullscreen(self, key): |
||
| 237 | """ |
||
| 238 | @brief Display image in fullscreen mode |
||
| 239 | |||
| 240 | @param key Image identifier ("left" or "right") |
||
| 241 | @exception FileNotFoundError If image file does not exist |
||
| 242 | """ |
||
| 243 | path = self.image_paths.get(key) |
||
| 244 | if not os.path.exists(path): |
||
| 245 | messagebox.showwarning("Missing Image", f"{path} not found.") |
||
| 246 | return |
||
| 247 | |||
| 248 | fullscreen = tk.Toplevel(self.root) |
||
| 249 | fullscreen.attributes("-fullscreen", True) |
||
| 250 | fullscreen.configure(bg="black") |
||
| 251 | fullscreen.bind("<Escape>", lambda e: fullscreen.destroy()) |
||
| 252 | |||
| 253 | lbl = ttk.Label(fullscreen, background="black") |
||
| 254 | lbl.pack(expand=True) |
||
| 255 | |||
| 256 | def update_fullscreen(): |
||
| 257 | """ |
||
| 258 | @brief Update fullscreen display with current image |
||
| 259 | |||
| 260 | @details Continuously updates the fullscreen display with |
||
| 261 | the current image, resizing to fit screen while maintaining aspect ratio. |
||
| 262 | """ |
||
| 263 | if not os.path.exists(path): |
||
| 264 | return |
||
| 265 | try: |
||
| 266 | img = Image.open(path) |
||
| 267 | screen_w = fullscreen.winfo_screenwidth() |
||
| 268 | screen_h = fullscreen.winfo_screenheight() |
||
| 269 | |||
| 270 | img_ratio = img.width / img.height |
||
| 271 | screen_ratio = screen_w / screen_h |
||
| 272 | |||
| 273 | if img_ratio > screen_ratio: |
||
| 274 | new_w = screen_w |
||
| 275 | new_h = int(screen_w / img_ratio) |
||
| 276 | else: |
||
| 277 | new_h = screen_h |
||
| 278 | new_w = int(screen_h * img_ratio) |
||
| 279 | |||
| 280 | img = img.resize((new_w, new_h), Image.LANCZOS) |
||
| 281 | photo = ImageTk.PhotoImage(img) |
||
| 282 | lbl.configure(image=photo) |
||
| 283 | lbl.image = photo |
||
| 284 | except Exception as e: |
||
| 285 | print("Fullscreen update error:", e) |
||
| 286 | |||
| 287 | fullscreen.after(100, update_fullscreen) |
||
| 288 | |||
| 289 | update_fullscreen() |
||
| 290 | |||
| 291 | def _update_image_widget(self, key, np_image): |
||
| 292 | """ |
||
| 293 | @brief Update individual image widget with new image data |
||
| 294 | |||
| 295 | @param key Widget identifier |
||
| 296 | @param np_image Numpy array containing image data |
||
| 297 | """ |
||
| 298 | if np_image is None: |
||
| 299 | self.labels[key].configure(text=f"No image data", image="") |
||
| 300 | self.labels[key].image = None |
||
| 301 | return |
||
| 302 | |||
| 303 | if np_image.shape[2] == 4: |
||
| 304 | np_image = cv2.cvtColor(np_image, cv2.COLOR_BGRA2RGBA) |
||
| 305 | else: |
||
| 306 | np_image = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB) |
||
| 307 | |||
| 308 | img = Image.fromarray(np_image) |
||
| 309 | img = img.resize((300, 300)) |
||
| 310 | |||
| 311 | photo = ImageTk.PhotoImage(img) |
||
| 312 | |||
| 313 | self.labels[key].configure(image=photo, text="") |
||
| 314 | self.labels[key].image = photo |
||
| 315 | |||
| 316 | def run_and_update(self): |
||
| 317 | """ |
||
| 318 | @brief Process images and update display |
||
| 319 | |||
| 320 | @details Applies current parameters to process images and |
||
| 321 | updates all image displays with the results. |
||
| 322 | |||
| 323 | @exception Exception Catches and displays any processing errors |
||
| 324 | """ |
||
| 325 | try: |
||
| 326 | overlap_value = int(self.param_var.get()) |
||
| 327 | blend_type = self.blend_var.get() |
||
| 328 | self.processor.process_images(overlap_value, blend_type) |
||
| 329 | self.update_images_from_arrays({"left": self.processor.image_left, "right": self.processor.image_right, |
||
| 330 | "main": self.processor.image_main}) |
||
| 331 | except Exception as e: |
||
| 332 | import traceback |
||
| 333 | traceback.print_exc() |
||
| 334 | print("TYPE OF E:", type(e)) |
||
| 335 | print("VALUE OF E:", repr(e)) |
||
| 336 | messagebox.showerror("Error", repr(e)) |
||
| 337 | def run_video(self): |
||
| 338 | """ |
||
| 339 | @brief Process and display next video frame |
||
| 340 | |||
| 341 | @details Retrieves next frame from video, processes it with |
||
| 342 | current parameters, and updates the display. |
||
| 343 | """ |
||
| 344 | |||
| 345 | if not self.is_running: |
||
| 346 | return |
||
| 347 | |||
| 348 | image = self.video_processor.get_next_frame() |
||
| 349 | if image is None: |
||
| 350 | print("End of video.") |
||
| 351 | self.is_running = False |
||
| 352 | return |
||
| 353 | |||
| 354 | try: |
||
| 355 | overlap_value = int(self.param_var.get()) |
||
| 356 | blend_type = self.blend_var.get() |
||
| 357 | self.processor.process_frame(image, overlap_value, blend_type) |
||
| 358 | self.update_images_from_arrays({ |
||
| 359 | "left": self.processor.image_left, |
||
| 360 | "right": self.processor.image_right, |
||
| 361 | "main": self.processor.image_main |
||
| 362 | }) |
||
| 363 | except Exception as e: |
||
| 364 | import traceback |
||
| 365 | traceback.print_exc() |
||
| 366 | messagebox.showerror("Error", repr(e)) |
||
| 367 | self.is_running = False |
||
| 368 | return |
||
| 369 | |||
| 370 | delay = int(1000 / self.fps) |
||
| 371 | self.root.after(delay, self.run_video) |
||
| 372 | |||
| 373 | def start_video(self): |
||
| 374 | """ |
||
| 375 | @brief Start video processing and display |
||
| 376 | """ |
||
| 377 | self.is_running = True |
||
| 378 | self.run_video() |
||
| 379 | |||
| 380 | def stop_video(self): |
||
| 381 | """ |
||
| 382 | @brief Stop video processing and release resources |
||
| 383 | """ |
||
| 384 | self.is_running = False |
||
| 385 | if self.video_processor.cap is not None: |
||
| 386 | self.video_processor.release() |
||
| 387 | |||
| 388 | def update_images_from_arrays(self, images: dict): |
||
| 389 | """ |
||
| 390 | @brief Update all image widgets from numpy arrays |
||
| 391 | |||
| 392 | @param images Dictionary of image identifiers and numpy arrays |
||
| 393 | """ |
||
| 394 | for key, np_img in images.items(): |
||
| 395 | self._update_image_widget(key, np_img) |
||
| 396 | |||
| 397 | def schedule_start(self): |
||
| 398 | """ |
||
| 399 | @brief Schedule video to start at specified time |
||
| 400 | |||
| 401 | @details Parses time string and starts checking loop to begin |
||
| 402 | video playback at the specified time. |
||
| 403 | """ |
||
| 404 | |||
| 405 | import datetime |
||
| 406 | target_str = self.start_time_var.get().strip() |
||
| 407 | try: |
||
| 408 | target_time = datetime.datetime.strptime(target_str, "%H:%M:%S").time() |
||
| 409 | print(f"[Timer] Scheduled video start at {target_time}.") |
||
| 410 | self._check_time_loop(target_time) |
||
| 411 | except ValueError: |
||
| 412 | print("[Timer] Invalid time format. Please use HH:MM:SS (e.g., 14:30:00).") |
||
| 413 | |||
| 414 | def _check_time_loop(self, target_time): |
||
| 415 | """ |
||
| 416 | @brief Internal method to check if target time has been reached |
||
| 417 | |||
| 418 | @param target_time Target time to start video |
||
| 419 | """ |
||
| 420 | |||
| 421 | import datetime |
||
| 422 | now = datetime.datetime.now().time() |
||
| 423 | |||
| 424 | if now >= target_time: |
||
| 425 | print("[Timer] Target time reached — starting video.") |
||
| 426 | self.start_video() |
||
| 427 | return |
||
| 428 | |||
| 429 | self.root.after(1000, lambda: self._check_time_loop(target_time)) |
||
| 430 | |||
| 431 | |||
| 432 | |||
| 433 | |||
| 434 | if __name__ == "__main__": |
||
| 435 | """ |
||
| 436 | @brief Main entry point for the application |
||
| 437 | |||
| 438 | @details Creates Tkinter root window and starts the GUI application |
||
| 439 | """ |
||
| 440 | root = tk.Tk() |
||
| 441 | app = ImageDisplayApp(root) |
||
| 442 | root.mainloop() |
||
| 443 | </code></pre> |
||
| 444 | |||
| 445 | *altmain.py* |
||
| 446 | |||
| 447 | <pre><code class="python"> |
||
| 448 | """ |
||
| 449 | @file altmain.py |
||
| 450 | @brief Projection Splitter with Overlap Blending using PyTorch. |
||
| 451 | |||
| 452 | @details |
||
| 453 | This module provides a projection image processing system designed for |
||
| 454 | multi-projector setups or panoramic displays. It takes an input image, |
||
| 455 | splits it into two halves (left and right), and applies an overlap blending |
||
| 456 | region between them to ensure seamless projection alignment. |
||
| 457 | |||
| 458 | It supports multiple blending modes (linear, quadratic, and Gaussian) |
||
| 459 | and performs GPU-accelerated computation via PyTorch for high efficiency. |
||
| 460 | """ |
||
| 461 | |||
| 462 | import cv2 |
||
| 463 | import numpy as np |
||
| 464 | import math |
||
| 465 | from scipy.special import erf |
||
| 466 | import torch |
||
| 467 | import config_reader |
||
| 468 | |||
| 469 | |||
| 470 | class ProjectionSplit: |
||
| 471 | """ |
||
| 472 | @brief Handles image splitting and blending for projection alignment. |
||
| 473 | |||
| 474 | @details |
||
| 475 | The ProjectionSplit class processes both static images and individual |
||
| 476 | frames from video sources. It divides the input image into two parts |
||
| 477 | with a configurable overlap and applies smooth blending transitions |
||
| 478 | using selected mathematical models. |
||
| 479 | |||
| 480 | The blending operations are optimized with PyTorch and support the |
||
| 481 | Metal backend for GPU acceleration on macOS devices. |
||
| 482 | """ |
||
| 483 | |||
| 484 | def __init__(self): |
||
| 485 | """@brief Initialize the ProjectionSplit object and configuration. |
||
| 486 | |||
| 487 | @details |
||
| 488 | This constructor initializes placeholders for the left, right, |
||
| 489 | and main images, and loads configuration parameters (e.g., |
||
| 490 | blending coefficients) from the `config.ini` file via ConfigReader. |
||
| 491 | """ |
||
| 492 | self.image_left = None |
||
| 493 | self.image_right = None |
||
| 494 | self.image_main = None |
||
| 495 | self.cfg = config_reader.ConfigReader("config.ini") |
||
| 496 | |||
| 497 | def process_frame(self, image, overlap: int = 75, blend_type: str = "exponential"): |
||
| 498 | """@brief Process a single input frame into left and right projections with overlap blending. |
||
| 499 | |||
| 500 | @details |
||
| 501 | This method divides an input image frame into two halves and applies |
||
| 502 | a blending function to the overlapping region between them. The |
||
| 503 | blending type determines the transition smoothness between projectors. |
||
| 504 | |||
| 505 | Available blend types: |
||
| 506 | - **linear**: Simple linear transition. |
||
| 507 | - **quadratic**: Smoother parabolic blending. |
||
| 508 | - **gaussian**: Natural soft transition curve based on Gaussian distribution. |
||
| 509 | |||
| 510 | The blending is executed on GPU via PyTorch for efficient computation. |
||
| 511 | |||
| 512 | @param image The input image (NumPy array) in BGR or BGRA format. |
||
| 513 | @param overlap Integer specifying the pixel width of the overlapping area. Default: 75. |
||
| 514 | @param blend_type String specifying the blending function ("linear", "quadratic", "gaussian"). |
||
| 515 | @throws FileNotFoundError If the image is None or not found. |
||
| 516 | @throws ValueError If an invalid blend_type is specified. |
||
| 517 | """ |
||
| 518 | if image is None: |
||
| 519 | raise FileNotFoundError("Error: input.png not found or could not be loaded.") |
||
| 520 | self.image_main = image |
||
| 521 | |||
| 522 | # Ensure alpha channel |
||
| 523 | if image.shape[2] == 3: |
||
| 524 | image = cv2.cvtColor(image, cv2.COLOR_BGR2BGRA) |
||
| 525 | |||
| 526 | height, width = image.shape[:2] |
||
| 527 | split_x = width // 2 |
||
| 528 | |||
| 529 | # Define overlapping regions |
||
| 530 | left_end = split_x + overlap // 2 |
||
| 531 | right_start = split_x - overlap // 2 |
||
| 532 | |||
| 533 | left_img = image[:, :left_end].copy() |
||
| 534 | right_img = image[:, right_start:].copy() |
||
| 535 | |||
| 536 | # Generate normalized overlap vector |
||
| 537 | x = np.linspace(0, 1, overlap) |
||
| 538 | |||
| 539 | # Select blending function |
||
| 540 | if blend_type == "linear": |
||
| 541 | alpha_left_curve = 1 - self.cfg.get_linear_parameter() * x |
||
| 542 | alpha_right_curve = 1 - self.cfg.get_linear_parameter() + self.cfg.get_linear_parameter() * x |
||
| 543 | elif blend_type == "quadratic": |
||
| 544 | alpha_left_curve = (1 - x) ** 2 |
||
| 545 | alpha_right_curve = x ** 2 |
||
| 546 | elif blend_type == "gaussian": |
||
| 547 | sigma = 0.25 |
||
| 548 | g = 0.5 * (1 + erf((x - 0.5) / (sigma * np.sqrt(2)))) |
||
| 549 | alpha_left_curve = 1 - g |
||
| 550 | alpha_right_curve = g |
||
| 551 | else: |
||
| 552 | raise ValueError(f"Unknown blend_type '{blend_type}'") |
||
| 553 | |||
| 554 | # GPU accelerated blending using PyTorch |
||
| 555 | device = "mps" # Metal backend (for macOS) |
||
| 556 | left_img_t = torch.from_numpy(left_img).to(device, dtype=torch.float32) |
||
| 557 | right_img_t = torch.from_numpy(right_img).to(device, dtype=torch.float32) |
||
| 558 | alpha_left_t = torch.from_numpy(alpha_left_curve).to(device, dtype=torch.float32) |
||
| 559 | alpha_right_t = torch.from_numpy(alpha_right_curve).to(device, dtype=torch.float32) |
||
| 560 | |||
| 561 | # Expand alpha for broadcast along image width |
||
| 562 | alpha_left_2d = alpha_left_t.unsqueeze(0).unsqueeze(-1) |
||
| 563 | alpha_right_2d = alpha_right_t.unsqueeze(0).unsqueeze(-1) |
||
| 564 | |||
| 565 | # Apply blending on alpha channel |
||
| 566 | left_img_t[:, -overlap:, 3] *= alpha_left_2d.squeeze(-1) |
||
| 567 | right_img_t[:, :overlap, 3] *= alpha_right_2d.squeeze(-1) |
||
| 568 | |||
| 569 | # Convert back to CPU for saving |
||
| 570 | left_img = left_img_t.cpu().numpy().astype(np.uint8) |
||
| 571 | right_img = right_img_t.cpu().numpy().astype(np.uint8) |
||
| 572 | |||
| 573 | self.image_left = left_img |
||
| 574 | self.image_right = right_img |
||
| 575 | |||
| 576 | cv2.imwrite("left.png", left_img) |
||
| 577 | cv2.imwrite("right.png", right_img) |
||
| 578 | |||
| 579 | def process_images(self, overlap: int = 75, blend_type: str = "exponential"): |
||
| 580 | """@brief Process a static image file and generate blended left/right outputs. |
||
| 581 | |||
| 582 | @details |
||
| 583 | Reads 'input.png' from the current working directory, applies |
||
| 584 | image splitting and overlap blending, and saves the processed |
||
| 585 | halves as 'left.png' and 'right.png'. |
||
| 586 | |||
| 587 | This function uses the same internal logic as process_frame() |
||
| 588 | but is intended for static image files instead of real-time frames. |
||
| 589 | |||
| 590 | @param overlap Integer pixel width of the overlapping region. Default: 75. |
||
| 591 | @param blend_type String specifying blending mode ("linear", "quadratic", "gaussian"). |
||
| 592 | @throws FileNotFoundError If 'input.png' is not found. |
||
| 593 | @throws ValueError If an invalid blending type is selected. |
||
| 594 | """ |
||
| 595 | image = cv2.imread("input.png", cv2.IMREAD_UNCHANGED) |
||
| 596 | self.image_main = image |
||
| 597 | if image is None: |
||
| 598 | raise FileNotFoundError("Error: input.png not found.") |
||
| 599 | |||
| 600 | # Ensure image has alpha channel |
||
| 601 | if image.shape[2] == 3: |
||
| 602 | image = cv2.cvtColor(image, cv2.COLOR_BGR2BGRA) |
||
| 603 | |||
| 604 | height, width = image.shape[:2] |
||
| 605 | split_x = width // 2 |
||
| 606 | |||
| 607 | left_end = split_x + overlap // 2 |
||
| 608 | right_start = split_x - overlap // 2 |
||
| 609 | |||
| 610 | left_img = image[:, :left_end].copy() |
||
| 611 | right_img = image[:, right_start:].copy() |
||
| 612 | |||
| 613 | # Create blend curve |
||
| 614 | x = np.linspace(0, 1, overlap) |
||
| 615 | if blend_type == "linear": |
||
| 616 | alpha_left_curve = 1 - self.cfg.get_linear_parameter() * x |
||
| 617 | alpha_right_curve = 1 - self.cfg.get_linear_parameter() + self.cfg.get_linear_parameter() * x |
||
| 618 | elif blend_type == "quadratic": |
||
| 619 | alpha_left_curve = (1 - x) ** 2 |
||
| 620 | alpha_right_curve = x ** 2 |
||
| 621 | elif blend_type == "gaussian": |
||
| 622 | sigma = 0.25 |
||
| 623 | g = 0.5 * (1 + erf((x - 0.5) / (sigma * np.sqrt(2)))) |
||
| 624 | alpha_left_curve = 1 - g |
||
| 625 | alpha_right_curve = g |
||
| 626 | else: |
||
| 627 | raise ValueError(f"Unknown blend_type '{blend_type}'") |
||
| 628 | |||
| 629 | # GPU blending with PyTorch |
||
| 630 | device = "mps" |
||
| 631 | left_img_t = torch.from_numpy(left_img).to(device, dtype=torch.float32) |
||
| 632 | right_img_t = torch.from_numpy(right_img).to(device, dtype=torch.float32) |
||
| 633 | alpha_left_t = torch.from_numpy(alpha_left_curve).to(device, dtype=torch.float32) |
||
| 634 | alpha_right_t = torch.from_numpy(alpha_right_curve).to(device, dtype=torch.float32) |
||
| 635 | |||
| 636 | alpha_left_2d = alpha_left_t.unsqueeze(0).unsqueeze(-1) |
||
| 637 | alpha_right_2d = alpha_right_t.unsqueeze(0).unsqueeze(-1) |
||
| 638 | |||
| 639 | left_img_t[:, -overlap:, 3] *= alpha_left_2d.squeeze(-1) |
||
| 640 | right_img_t[:, :overlap, 3] *= alpha_right_2d.squeeze(-1) |
||
| 641 | |||
| 642 | left_img = left_img_t.cpu().numpy().astype(np.uint8) |
||
| 643 | right_img = right_img_t.cpu().numpy().astype(np.uint8) |
||
| 644 | |||
| 645 | self.image_left = left_img |
||
| 646 | self.image_right = right_img |
||
| 647 | |||
| 648 | cv2.imwrite("left.png", left_img) |
||
| 649 | cv2.imwrite("right.png", right_img) |
||
| 650 | |||
| 651 | </code></pre> |
||
| 652 | |||
| 653 | *video_processing.py* |
||
| 654 | |||
| 655 | <pre><code class="python"> |
||
| 656 | import cv2 |
||
| 657 | |||
| 658 | class VideoProcessing: |
||
| 659 | """@brief A class for loading, reading, and managing video streams using OpenCV. |
||
| 660 | |||
| 661 | @details |
||
| 662 | This class provides basic functionality for working with video files: |
||
| 663 | - Loading a video from a file path. |
||
| 664 | - Reading frames sequentially. |
||
| 665 | - Releasing system resources after use. |
||
| 666 | |||
| 667 | It is designed for modular integration with image-processing workflows, |
||
| 668 | enabling real-time or frame-by-frame analysis from video inputs. |
||
| 669 | """ |
||
| 670 | |||
| 671 | def __init__(self, video_path=None): |
||
| 672 | """@brief Initialize the VideoProcessing object. |
||
| 673 | |||
| 674 | @details |
||
| 675 | Optionally takes a video file path at initialization. If provided, |
||
| 676 | the constructor automatically loads the video for reading. |
||
| 677 | |||
| 678 | @param video_path (optional) The file path to the video file to load. |
||
| 679 | """ |
||
| 680 | self.cap = None |
||
| 681 | if video_path: |
||
| 682 | self.load_video(video_path) |
||
| 683 | |||
| 684 | def load_video(self, video_path): |
||
| 685 | """@brief Load a video file for processing. |
||
| 686 | |||
| 687 | @details |
||
| 688 | Opens a video file using OpenCV’s `VideoCapture`. If another video is |
||
| 689 | already loaded, it is safely released before opening the new one. |
||
| 690 | |||
| 691 | @param video_path The file path to the video file to load. |
||
| 692 | |||
| 693 | @throws ValueError If the video cannot be opened successfully. |
||
| 694 | """ |
||
| 695 | if self.cap is not None: |
||
| 696 | self.cap.release() |
||
| 697 | |||
| 698 | self.cap = cv2.VideoCapture(video_path) |
||
| 699 | |||
| 700 | if not self.cap.isOpened(): |
||
| 701 | raise ValueError(f"Cannot open video file: {video_path}") |
||
| 702 | |||
| 703 | def get_next_frame(self): |
||
| 704 | """@brief Retrieve the next frame from the video stream. |
||
| 705 | |||
| 706 | @details |
||
| 707 | Reads the next available frame from the video. If the end of the video |
||
| 708 | is reached or the video is not properly loaded, returns `None`. |
||
| 709 | |||
| 710 | @return The next video frame as a NumPy array, or `None` if no frame is available. |
||
| 711 | |||
| 712 | @throws RuntimeError If no video is loaded before calling this function. |
||
| 713 | """ |
||
| 714 | if self.cap is None: |
||
| 715 | raise RuntimeError("No video loaded. Call load_video() first.") |
||
| 716 | |||
| 717 | ret, frame = self.cap.read() |
||
| 718 | if not ret: |
||
| 719 | return None # End of video or error |
||
| 720 | |||
| 721 | return frame |
||
| 722 | |||
| 723 | def release(self): |
||
| 724 | """@brief Release the video capture resource. |
||
| 725 | |||
| 726 | @details |
||
| 727 | Safely releases the OpenCV `VideoCapture` object to free up system resources. |
||
| 728 | This should always be called when video processing is complete. |
||
| 729 | """ |
||
| 730 | if self.cap is not None: |
||
| 731 | self.cap.release() |
||
| 732 | self.cap = None |
||
| 733 | |||
| 734 | </code></pre> |