/
spaceswimmer
/
NeoVid
Обзор
Документация
Войти
/
spaceswimmer
/
NeoVid
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/gui.py
387 строк
15 KB
spaceswimmer
feat: implemented parallel solving with threads
11 июн 2026, 17:00
11 июн 2026, 17:00
5dceeb1
Код
Авторство
О чём код?
#!/usr/bin/env python3 """GUI wrapper for overlay_depth_v2.py — cross-platform, PyInstaller-friendly.""" import os import queue import sys import threading import tkinter as tk from tkinter import filedialog, messagebox, ttk from overlay_depth_v2 import ( find_video_files, get_video_info, parse_depth_data, process_video, ) class StopProcessing(Exception): pass class DepthOverlayApp: def __init__(self, root): self.root = root self.root.title("NeoVid — Depth Overlay") self.root.resizable(True, True) self.root.minsize(580, 520) self.depth_path = tk.StringVar() self.video_dir = tk.StringVar() self.offset = tk.DoubleVar(value=0.0) self.jobs = tk.IntVar(value=min(4, os.cpu_count() or 1)) self.running = False self.stop_event = threading.Event() self.worker = None self.result_queue = queue.Queue() self.video_files = [] self.current_file_idx = 0 self.frames_done = 0 self.total_all_frames = 0 self.frames_per_file = {} self._build_ui() # ------------------------------------------------------------------ # UI construction # ------------------------------------------------------------------ def _build_ui(self): pad = {"padx": 10, "pady": 5, "sticky": "ew"} # --- Depth file --- frm = ttk.LabelFrame(self.root, text="Depth data file", padding=8) frm.pack(fill="x", padx=10, pady=(10, 2)) frm.columnconfigure(0, weight=1) ttk.Entry(frm, textvariable=self.depth_path).grid(row=0, column=0, **pad) ttk.Button(frm, text="Browse...", command=self._browse_depth).grid(row=0, column=1, padx=(2, 10)) # --- Video folder --- frm = ttk.LabelFrame(self.root, text="Video folder", padding=8) frm.pack(fill="x", padx=10, pady=2) frm.columnconfigure(0, weight=1) ttk.Entry(frm, textvariable=self.video_dir).grid(row=0, column=0, **pad) ttk.Button(frm, text="Browse...", command=self._browse_folder).grid(row=0, column=1, padx=(2, 10)) ttk.Label( frm, text="⚠ The folder must contain ONLY the video files you want to process.", foreground="#cc6600", ).grid(row=1, column=0, columnspan=2, padx=10, pady=(0, 5), sticky="w") # --- Offset --- frm = ttk.LabelFrame(self.root, text="Offset", padding=8) frm.pack(fill="x", padx=10, pady=2) ttk.Label(frm, text="Shift depth data relative to video (seconds):").pack(side="left", padx=(0, 8)) ttk.Spinbox(frm, textvariable=self.offset, from_=-999.0, to=999.0, increment=0.5, width=8).pack( side="left" ) # --- Workers --- frm = ttk.LabelFrame(self.root, text="Workers", padding=8) frm.pack(fill="x", padx=10, pady=2) max_cpu = os.cpu_count() or 1 ttk.Label(frm, text=f"Parallel video workers (max {max_cpu}):").pack(side="left", padx=(0, 8)) self.jobs_spinbox = ttk.Spinbox( frm, textvariable=self.jobs, from_=1, to=max_cpu, increment=1, width=4, ) self.jobs_spinbox.pack(side="left") # --- Run / Stop buttons --- btn_frm = ttk.Frame(self.root) btn_frm.pack(fill="x", padx=10, pady=8) btn_frm.columnconfigure(0, weight=1) btn_frm.columnconfigure(1, weight=1) self.run_btn = ttk.Button(btn_frm, text="▶ Run", command=self._start_processing) self.run_btn.grid(row=0, column=0, sticky="ew", padx=(0, 4), ipady=4) self.stop_btn = ttk.Button(btn_frm, text="■ Stop", command=self._stop_processing, state="disabled") self.stop_btn.grid(row=0, column=1, sticky="ew", padx=(4, 0), ipady=4) # --- Progress --- frm = ttk.LabelFrame(self.root, text="Progress", padding=8) frm.pack(fill="x", padx=10, pady=2) frm.columnconfigure(0, weight=1) ttk.Label(frm, text="Overall:").grid(row=0, column=0, sticky="w", padx=(0, 8)) self.overall_bar = ttk.Progressbar(frm, mode="determinate") self.overall_bar.grid(row=0, column=1, sticky="ew", padx=(0, 10)) self.overall_label = ttk.Label(frm, text="0 / 0", width=18, anchor="w") self.overall_label.grid(row=0, column=2) ttk.Label(frm, text="Current file:").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=(4, 0)) self.file_bar = ttk.Progressbar(frm, mode="determinate") self.file_bar.grid(row=1, column=1, sticky="ew", padx=(0, 10), pady=(4, 0)) self.file_label = ttk.Label(frm, text="—", anchor="w") self.file_label.grid(row=2, column=0, columnspan=3, sticky="ew", padx=0, pady=(2, 0)) # --- Log --- frm = ttk.LabelFrame(self.root, text="Log", padding=8) frm.pack(fill="both", expand=True, padx=10, pady=(2, 10)) frm.columnconfigure(0, weight=1) frm.rowconfigure(0, weight=1) self.log = tk.Text(frm, height=10, wrap="word", state="disabled", font=("TkFixedFont", 9)) self.log.grid(row=0, column=0, sticky="nsew") scroll = ttk.Scrollbar(frm, orient="vertical", command=self.log.yview) scroll.grid(row=0, column=1, sticky="ns") self.log.configure(yscrollcommand=scroll.set) # Poll the result queue periodically self.root.after(100, self._poll_queue) # ------------------------------------------------------------------ # Browse helpers # ------------------------------------------------------------------ def _browse_depth(self): path = filedialog.askopenfilename( title="Select depth data file", filetypes=[("Text files", "*.txt"), ("All files", "*.*")], ) if path: self.depth_path.set(path) def _browse_folder(self): path = filedialog.askdirectory(title="Select video folder") if path: self.video_dir.set(path) # ------------------------------------------------------------------ # Processing # ------------------------------------------------------------------ def _start_processing(self): depth_file = self.depth_path.get().strip() video_dir = self.video_dir.get().strip() if not depth_file: messagebox.showwarning("Missing input", "Please select a depth data file.") return if not video_dir: messagebox.showwarning("Missing input", "Please select a video folder.") return if not os.path.isfile(depth_file): messagebox.showerror("Error", f"Depth file not found:\n{depth_file}") return if not os.path.isdir(video_dir): messagebox.showerror("Error", f"Video folder not found:\n{video_dir}") return self.video_files = find_video_files(video_dir) if not self.video_files: messagebox.showerror("Error", f"No .AVI files found in:\n{video_dir}") return self.running = True self.stop_event.clear() self.run_btn.configure(state="disabled", text="⏳ Processing...") self.stop_btn.configure(state="normal") self.jobs_spinbox.configure(state="disabled") self.log_clear() self.log_append(f"Depth data: {depth_file}") self.log_append(f"Videos found: {len(self.video_files)}") self.log_append(f"Offset: {self.offset.get()}s") self.log_append(f"Workers: {self.jobs.get()}\n") self.frames_done = 0 self.total_all_frames = 0 self.frames_per_file = {} self.overall_bar.configure(maximum=1, value=0) self.overall_label.configure(text="0 / 0 frames") self.file_bar.configure(value=0) self.file_label.configure(text="Pre-scanning videos...") self.worker = threading.Thread(target=self._process_all, daemon=True) self.worker.start() def _stop_processing(self): if not self.running: return self.stop_event.set() self.stop_btn.configure(state="disabled", text="Stopping...") self.log_append("\n--- Stopping (after current file finishes) ---") def _process_all(self): from concurrent.futures import ThreadPoolExecutor, as_completed depth_file = self.depth_path.get().strip() video_dir = self.video_dir.get().strip() offset = self.offset.get() jobs = self.jobs.get() try: times, depths = parse_depth_data(depth_file) self.result_queue.put(("log", f"Parsed {len(depths)} depth readings")) except Exception as e: self.result_queue.put(("error", f"Failed to parse depth file: {e}")) self.result_queue.put(("done", None)) return pre_scan = [] cumulative_time = 0.0 for idx, path in enumerate(self.video_files): info = get_video_info(path, cumulative_time) if info is None: self.result_queue.put(("error", f"Cannot open {os.path.basename(path)}")) self.result_queue.put(("done", None)) return pre_scan.append((idx, info)) cumulative_time += info["duration"] self.result_queue.put(("log", f" {os.path.basename(path)}: " f"{info['total_frames']} frames, {info['duration']:.1f}s")) total_frames_all = sum(info["total_frames"] for _, info in pre_scan) self.result_queue.put(("scan_done", total_frames_all)) with ThreadPoolExecutor(max_workers=jobs) as executor: futures = {} for idx, info in pre_scan: name, ext = os.path.splitext(os.path.basename(info["path"])) out_path = os.path.join(video_dir, f"{name}_depth{ext}") def make_callback(f_idx, f_total, f_path): def cb(frame_i, total, step): if self.stop_event.is_set(): raise StopProcessing() if frame_i % step == 0 or frame_i + 1 >= total: self.result_queue.put( ("frame_progress", (f_idx, f_path, frame_i, f_total)) ) return cb self.result_queue.put(("file_start", (idx, info["path"]))) future = executor.submit( process_video, info["path"], out_path, times, depths, info["global_start_time"], offset, progress_callback=make_callback(idx, info["total_frames"], info["path"]), ) futures[future] = (idx, info) total_frames = 0 for future in as_completed(futures): idx, info = futures[future] if self.stop_event.is_set(): break try: frames, duration = future.result() total_frames += frames self.result_queue.put(("file_done", (idx, frames, info["total_frames"]))) except StopProcessing: self.result_queue.put(("log", f" Stopped: {os.path.basename(info['path'])}")) self.result_queue.put(("file_done", (idx, 0, info["total_frames"]))) except Exception as e: self.result_queue.put(("log", f" ERROR in {os.path.basename(info['path'])}: {e}")) self.result_queue.put(("file_done", (idx, 0, info["total_frames"]))) self.result_queue.put( ("log", f"\n--- Done. {total_frames} total frames across {len(self.video_files)} videos ---") ) self.result_queue.put(("done", None)) # ------------------------------------------------------------------ # Queue polling — runs on main thread # ------------------------------------------------------------------ def _poll_queue(self): try: while True: msg = self.result_queue.get_nowait() self._handle_message(msg) except queue.Empty: pass self.root.after(100, self._poll_queue) def _handle_message(self, msg): kind, payload = msg if kind == "log": self.log_append(payload) elif kind == "error": self.log_append(f"ERROR: {payload}") messagebox.showerror("Error", payload) elif kind == "scan_done": self.total_all_frames = payload self.overall_bar.configure(maximum=self.total_all_frames, value=0) self.overall_label.configure( text=f"0 / {self.total_all_frames} frames" ) self.file_label.configure( text=f"Processing {len(self.video_files)} files with " f"{self.jobs.get()} worker(s)..." ) elif kind == "file_start": idx, path = payload self.current_file_idx = idx basename = os.path.basename(path) self.log_append(f"\n[{idx + 1}/{len(self.video_files)}] {basename}") elif kind == "frame_progress": f_idx, f_path, frame_i, file_total = payload self.frames_per_file[f_idx] = frame_i total_done = sum(self.frames_per_file.values()) self.overall_bar.configure(value=total_done) pct = total_done * 100 // self.total_all_frames if self.total_all_frames else 0 self.overall_label.configure( text=f"{total_done} / {self.total_all_frames} frames ({pct}%)" ) if file_total > 0: file_pct = frame_i * 100 // file_total basename = os.path.basename(f_path) self.file_bar.configure(maximum=file_total, value=frame_i) self.file_label.configure( text=f"{basename} — {file_pct}% ({frame_i}/{file_total})" ) elif kind == "file_done": idx, frames, file_total = payload self.frames_per_file[idx] = file_total total_done = sum(self.frames_per_file.values()) self.overall_bar.configure(value=total_done) self.overall_label.configure( text=f"{total_done} / {self.total_all_frames} frames" ) self.log_append( f" Done: {os.path.basename(self.video_files[idx])} — {frames} frames" ) elif kind == "done": self.running = False self.stop_event.clear() self.run_btn.configure(state="normal", text="▶ Run") self.stop_btn.configure(state="disabled", text="■ Stop") self.jobs_spinbox.configure(state="normal") self.file_bar.configure(value=0) self.file_label.configure(text="Done.") # ------------------------------------------------------------------ # Log helpers # ------------------------------------------------------------------ def log_append(self, text): self.log.configure(state="normal") self.log.insert("end", text + "\n") self.log.see("end") self.log.configure(state="disabled") def log_clear(self): self.log.configure(state="normal") self.log.delete("1.0", "end") self.log.configure(state="disabled") # ------------------------------------------------------------------ # Entry point # ------------------------------------------------------------------ def main(): root = tk.Tk() app = DepthOverlayApp(root) root.mainloop() if __name__ == "__main__": main()