This is only the first Version. Please have a look at my Github-Repo for further informations and the actual version of this script. You can find it here: https://github.com/duke2421/AutotrackPythonScript.git
-
-
Save duke2421/0828b5a62e35eee2e3c2a4bc41c573eb to your computer and use it in GitHub Desktop.
AutoTracker workflow using GLOMAP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| AutoTracker GUI (Python) – Cross-Platform (preferred layout) | |
| + i18n (DE/EN auto + dropdown) | |
| + Linux installer/compilers from release archives (COLMAP 3.12.3 / GLOMAP 1.1.0) | |
| + Project-structure creation prompt, PATH fallback search, runtime timer, FPS options | |
| This file is based on polyfjords AutoTracker_v1.4.bat | |
| """ | |
| import os, shutil | |
| from pathlib import Path | |
| def _win_ensure_angle_dlls(colmap_dir: Path, sources_dir: Path, log): | |
| """Ensure libEGL/libGLESv2/opengl32sw/d3dcompiler_47 are present in colmap\bin. | |
| Try to extract them from official COLMAP zips (nocuda/cuda) if missing. | |
| """ | |
| try: | |
| if os.name != 'nt': | |
| return | |
| bin_dir = colmap_dir / 'bin' | |
| needed = ['libEGL.dll', 'libGLESv2.dll', 'opengl32sw.dll', 'd3dcompiler_47.dll'] | |
| missing = [n for n in needed if not (bin_dir / n).exists()] | |
| if not missing: | |
| log('[INSTALL] ANGLE/Software-OpenGL DLLs vorhanden.') | |
| return | |
| log(f"[INSTALL] ANGLE-DLLs fehlen: {', '.join(missing)} -> versuche aus COLMAP-ZIPs zu ergänzen…") | |
| zips = [ | |
| ('colmap-x64-windows-nocuda.zip', 'https://github.com/colmap/colmap/releases/download/3.12.3/colmap-x64-windows-nocuda.zip'), | |
| ('colmap-x64-windows-cuda.zip', 'https://github.com/colmap/colmap/releases/download/3.12.3/colmap-x64-windows-cuda.zip'), | |
| ] | |
| from zipfile import ZipFile | |
| for fn, url in zips: | |
| zpath = sources_dir / fn | |
| if not zpath.exists(): | |
| try: | |
| download_file(url, zpath, log) | |
| except Exception as e: | |
| log(f"[INSTALL] Hinweis: Konnte {fn} nicht laden: {e}") | |
| continue | |
| try: | |
| with ZipFile(zpath) as zf: | |
| members = {n: None for n in needed} | |
| for zi in zf.infolist(): | |
| lower = zi.filename.lower() | |
| for dll in needed: | |
| if lower.endswith('/bin/' + dll.lower()): | |
| members[dll] = zi | |
| any_extracted = False | |
| for dll, zi in members.items(): | |
| if zi is None: continue | |
| target = bin_dir / dll | |
| bin_dir.mkdir(parents=True, exist_ok=True) | |
| with zf.open(zi) as src, open(target, 'wb') as dst: | |
| dst.write(src.read()) | |
| any_extracted = True | |
| if any_extracted: | |
| still_missing = [n for n in needed if not (bin_dir / n).exists()] | |
| if not still_missing: | |
| log('[INSTALL] ANGLE-DLLs ergänzt (aus COLMAP-Zip).') | |
| return | |
| else: | |
| log(f"[INSTALL] Noch fehlend: {', '.join(still_missing)} (nächster Versuch)…") | |
| except Exception as e: | |
| log(f"[INSTALL] Fehler beim Entpacken {zpath.name}: {e}") | |
| log('[INSTALL] Warnung: ANGLE-DLLs konnten nicht automatisch ergänzt werden.') | |
| except Exception as e: | |
| log(f"[INSTALL] Warnung: ANGLE-DLL-Check fehlgeschlagen: {e}") | |
| def _win_has_vc_redist() -> bool: | |
| try: | |
| if os.name != 'nt': | |
| return True | |
| sysroot = os.environ.get('SystemRoot', r'C:\\Windows') | |
| cand = [ | |
| Path(sysroot) / 'System32' / 'vcruntime140.dll', | |
| Path(sysroot) / 'System32' / 'vcruntime140_1.dll', | |
| Path(sysroot) / 'SysWOW64' / 'vcruntime140.dll', | |
| Path(sysroot) / 'SysWOW64' / 'vcruntime140_1.dll', | |
| ] | |
| return any(p.exists() for p in cand) | |
| except Exception: | |
| return False | |
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| AutoTracker GUI (Python) – Cross-Platform (preferred layout) | |
| + i18n (DE/EN auto + dropdown) | |
| + Linux installer/compilers from release archives (COLMAP 3.12.3 / GLOMAP 1.1.0) | |
| + Project-structure creation prompt, PATH fallback search, runtime timer, FPS options | |
| This file is based on your working version (AutoTracker_GUI_crossplat(12).py) | |
| and merges back the language switch and all installer/compile improvements | |
| without removing any of the controls you liked. | |
| """ | |
| import os, sys, shlex, subprocess, threading, time, platform, shutil, tarfile, zipfile, tempfile, ssl, urllib.request, locale | |
| from pathlib import Path | |
| import webbrowser | |
| IS_WINDOWS = (os.name == "nt") | |
| OS_NAME = platform.system() | |
| # ------------------------- i18n ------------------------- | |
| def detect_lang(): | |
| try: | |
| lang = os.environ.get("LANG") or locale.getdefaultlocale()[0] or "" | |
| except Exception: | |
| lang = "" | |
| lang = lang.lower() | |
| if lang.startswith("de"): return "de" | |
| return "en" | |
| I18N = { | |
| "de": { | |
| "app_title": "AutoTracker GUI (Python) – {os}", | |
| "paths_tools": "Pfade & Tools", | |
| "project_top": "Projekt-Top-Ordner:", | |
| "browse": "Durchsuchen…", | |
| "rediscover": "Neu erkennen", | |
| "os_detected": "Erkanntes Betriebssystem: {os} {distro}", | |
| "search_path_cb": "(Linux) Systempfad durchsuchen, falls in Projektordnern nicht gefunden", | |
| "search_path_btn": "Systempfad durchsuchen", | |
| "install_tools": "Tools installieren…", | |
| "ffmpeg_label": "ffmpeg:", | |
| "colmap_label": "COLMAP:", | |
| "glomap_label": "GLOMAP (optional):", | |
| "ffmpeg_placeholder": "Bitte die ausführbare Datei für FFMPEG auswählen", | |
| "colmap_placeholder": "Bitte die ausführbare Datei für COLMAP auswählen", | |
| "glomap_placeholder": "Bitte die ausführbare Datei für GLOMAP auswählen (optional)", | |
| "options": "Optionen", | |
| "res_title": "Frame-Extraktionsauflösung:", | |
| "res_keep": "Original behalten", | |
| "res_only_w": "Nur Breite", | |
| "res_only_h": "Nur Höhe", | |
| "res_wh": "Breite × Höhe", | |
| "gpu_check": "GPU verwenden (falls unterstützt) – SIFT Extraction & Matching", | |
| "jpeg_q": "JPEG-Qualität (-qscale:v):", | |
| "sift_max": "SiftExtraction.max_image_size:", | |
| "seq_overlap": "SequentialMatching.overlap:", | |
| "fps_title": "Frame-Reduktion:", | |
| "fps_all": "Alle Frames", | |
| "fps_every": "Jeden", | |
| "fps_every_suffix": "-ten Frame (z. B. 2 = halbe Frames)", | |
| "fps_target": "Ziel-FPS", | |
| "fps_hint": "(z. B. 5, 10, 15)", | |
| "videos": "Videos", | |
| "add_videos": "Videos hinzufügen…", | |
| "remove_sel": "Auswahl entfernen", | |
| "clear_list": "Liste leeren", | |
| "scenes_dir": "Scenes-Ausgabeordner:", | |
| "start": "Start", | |
| "test_tools": "Tools testen", | |
| "elapsed": "Laufzeit", | |
| "dlg_pick_dir": "Ordner auswählen", | |
| "dlg_pick_file": "Datei auswählen", | |
| "dlg_pick_videos": "Videos auswählen", | |
| "dlg_create_structure_title": "Projektstruktur", | |
| "dlg_create_structure_msg": "Es wurden nicht alle erwarteten Ordner gefunden.\n\nSollen alle benötigten Ordner jetzt erstellt werden?", | |
| "dlg_create_structure_where": "Wo sollen die Ordner erstellt werden?", | |
| "dlg_done": "Fertig", | |
| "dlg_structure_created": "Ordnerstruktur wurde erstellt.", | |
| "msg_linux_only": "Der Systempfad-Fallback ist nur unter Linux aktiv.", | |
| "msg_found": "Gefunden:", | |
| "msg_found_none": "Keine zusätzlichen Tools im Systempfad gefunden.", | |
| "msg_installer_linux_only": "Der Tool-Installer ist auf Linux ausgerichtet.", | |
| "installer_title": "Tools installieren", | |
| "installer_what": "Was soll installiert/gebaut werden?", | |
| "installer_ffmpeg": "ffmpeg (Paketmanager)", | |
| "installer_colmap": "COLMAP aus Quelle bauen", | |
| "installer_glomap": "GLOMAP aus Quelle bauen", | |
| "installer_source": "Quelle (Git-URL oder Release-Archiv):", | |
| "installer_use_cuda": "CUDA nutzen, falls verfügbar", | |
| "installer_try_cuda_pkg": "(Optional) CUDA Toolkit jetzt per Paketmanager versuchen zu installieren", | |
| "installer_start": "Installieren starten", | |
| "installer_close": "Schließen", | |
| "installer_note": "Hinweis: Projektlokale Installation nach 01 GLOMAP/<tool>.\nRelease-Archive (.tar.gz/.zip) werden automatisch nach 06 Sources/<tool> entpackt.", | |
| "warn_running": "Ein Durchlauf ist bereits aktiv.", | |
| "warn_no_videos": "Bitte mindestens ein Video hinzufügen.", | |
| "err_ffmpeg": "Bitte die ausführbare Datei für FFMPEG auswählen.", | |
| "err_colmap": "Bitte die ausführbare Datei für COLMAP auswählen.", | |
| "run_extract": "Frames extrahieren (ffmpeg)…", | |
| "run_feat": "COLMAP feature_extractor…", | |
| "run_match": "COLMAP sequential_matcher…", | |
| "run_mapper": "Sparse Reconstruction (mapper)…", | |
| "done_all": "Alles erledigt.", | |
| "tools_test_begin": "### Tools testen ###", | |
| "tools_test_end": "### Test abgeschlossen ###", | |
| "lang_label": "Sprache:", | |
| "lang_de": "Deutsch", | |
| "lang_en": "English", | |
| "info_btn": "Info", | |
| "about_title": "Info", | |
| "about_text": "Basiert auf dem Script von ", | |
| "about_link": "Polyfjord", | |
| }, | |
| "en": { | |
| "app_title": "AutoTracker GUI (Python) – {os}", | |
| "paths_tools": "Paths & Tools", | |
| "project_top": "Project top folder:", | |
| "browse": "Browse…", | |
| "rediscover": "Re-detect", | |
| "os_detected": "Detected OS: {os} {distro}", | |
| "search_path_cb": "(Linux) Search system PATH if not found in project folders", | |
| "search_path_btn": "Search PATH", | |
| "install_tools": "Install tools…", | |
| "ffmpeg_label": "ffmpeg:", | |
| "colmap_label": "COLMAP:", | |
| "glomap_label": "GLOMAP (optional):", | |
| "ffmpeg_placeholder": "Please select the executable for FFMPEG", | |
| "colmap_placeholder": "Please select the executable for COLMAP", | |
| "glomap_placeholder": "Please select the executable for GLOMAP (optional)", | |
| "options": "Options", | |
| "res_title": "Frame extraction resolution:", | |
| "res_keep": "Keep original", | |
| "res_only_w": "Width only", | |
| "res_only_h": "Height only", | |
| "res_wh": "Width × Height", | |
| "gpu_check": "Use GPU (if supported) – SIFT extraction & matching", | |
| "jpeg_q": "JPEG quality (-qscale:v):", | |
| "sift_max": "SiftExtraction.max_image_size:", | |
| "seq_overlap": "SequentialMatching.overlap:", | |
| "fps_title": "Frame reduction:", | |
| "fps_all": "All frames", | |
| "fps_every": "Every", | |
| "fps_every_suffix": "th frame (e.g., 2 = half the frames)", | |
| "fps_target": "Target FPS", | |
| "fps_hint": "(e.g., 5, 10, 15)", | |
| "videos": "Videos", | |
| "add_videos": "Add videos…", | |
| "remove_sel": "Remove selected", | |
| "clear_list": "Clear list", | |
| "scenes_dir": "Scenes output folder:", | |
| "start": "Start", | |
| "test_tools": "Test tools", | |
| "elapsed": "Elapsed", | |
| "dlg_pick_dir": "Select folder", | |
| "dlg_pick_file": "Select file", | |
| "dlg_pick_videos": "Select videos", | |
| "dlg_create_structure_title": "Project structure", | |
| "dlg_create_structure_msg": "Not all expected folders were found.\n\nCreate all required folders now?", | |
| "dlg_create_structure_where": "Where should the folders be created?", | |
| "dlg_done": "Done", | |
| "dlg_structure_created": "Project structure created.", | |
| "msg_linux_only": "System PATH fallback is active on Linux only.", | |
| "msg_found": "Found:", | |
| "msg_found_none": "No additional tools found in PATH.", | |
| "msg_installer_linux_only": "The tool installer is Linux-focused.", | |
| "installer_title": "Install tools", | |
| "installer_what": "What should be installed/built?", | |
| "installer_ffmpeg": "ffmpeg (package manager)", | |
| "installer_colmap": "Build COLMAP from source", | |
| "installer_glomap": "Build GLOMAP from source", | |
| "installer_source": "Source (Git URL or release archive):", | |
| "installer_use_cuda": "Use CUDA if available", | |
| "installer_try_cuda_pkg": "(Optional) Try installing CUDA Toolkit via package manager now", | |
| "installer_start": "Start installing", | |
| "installer_close": "Close", | |
| "installer_note": "Note: Local project install to 01 GLOMAP/<tool>.\nRelease archives (.tar.gz/.zip) are unpacked to 06 Sources/<tool>.", | |
| "warn_running": "A run is already active.", | |
| "warn_no_videos": "Please add at least one video.", | |
| "err_ffmpeg": "Please select the executable for FFMPEG.", | |
| "err_colmap": "Please select the executable for COLMAP.", | |
| "run_extract": "Extracting frames (ffmpeg)…", | |
| "run_feat": "COLMAP feature_extractor…", | |
| "run_match": "COLMAP sequential_matcher…", | |
| "run_mapper": "Sparse reconstruction (mapper)…", | |
| "done_all": "All done.", | |
| "tools_test_begin": "### Testing tools ###", | |
| "tools_test_end": "### Test finished ###", | |
| "lang_label": "Language:", | |
| "lang_de": "Deutsch", | |
| "lang_en": "English", | |
| "info_btn": "Info", | |
| "about_title": "Info", | |
| "about_text": "Based on the script by ", | |
| "about_link": "Polyfjord", | |
| } | |
| } | |
| # ------------------------- small utilities (from working version) ------------------------- | |
| def _detect_pkg_manager(): | |
| if shutil.which("apt"): return "apt" | |
| if shutil.which("dnf"): return "dnf" | |
| if shutil.which("zypper"): return "zypper" | |
| if shutil.which("pacman"): return "pacman" | |
| if shutil.which("apk"): return "apk" | |
| return None | |
| def _read_os_release(): | |
| data = {} | |
| try: | |
| with open("/etc/os-release", "r", encoding="utf-8") as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line or "=" not in line: continue | |
| k, v = line.split("=", 1) | |
| v = v.strip().strip('"') | |
| data[k] = v | |
| except Exception: | |
| pass | |
| return data | |
| def _tk_install_command(pm): | |
| if pm == "apt": return "apt update && apt install -y python3-tk" | |
| if pm == "dnf": return "dnf install -y python3-tkinter" | |
| if pm == "zypper": return "zypper --non-interactive install -y python3-tk" | |
| if pm == "pacman": return "pacman -Sy --noconfirm tk" | |
| if pm == "apk": return "apk add --no-cache tcl tk" | |
| return None | |
| def _sudo_wrap(cmd): | |
| # prefer GUI polkit if present (pkexec) | |
| if shutil.which("pkexec"): return ["pkexec", "bash", "-lc", cmd] | |
| if shutil.which("sudo"): return ["sudo", "bash", "-lc", cmd] | |
| return ["bash", "-lc", cmd] | |
| def ensure_tkinter(): | |
| try: | |
| import importlib; importlib.import_module("tkinter"); return True | |
| except ModuleNotFoundError: | |
| if OS_NAME != "Linux": | |
| sys.stderr.write("[Error] Tkinter missing and cannot be auto-installed on this OS.\n") | |
| return False | |
| pm = _detect_pkg_manager(); cmd = _tk_install_command(pm) if pm else None | |
| sys.stderr.write("Tkinter (python3-tk) not found.\n") | |
| if not cmd: | |
| sys.stderr.write("No supported package manager detected. Install Tkinter manually.\n") | |
| return False | |
| # Ask in terminal (pre-GUI). pkexec/sudo will open GUI prompt if available. | |
| sys.stderr.write(f"Detected package manager: {pm}\nInstall Tkinter now? [Y/n]: ") | |
| try: choice = input().strip().lower() | |
| except EOFError: choice = "y" | |
| if choice in ("", "y", "yes", "j", "ja"): | |
| res = subprocess.run(_sudo_wrap(cmd)) | |
| if res.returncode != 0: | |
| sys.stderr.write(f"Installation failed (code {res.returncode}). Manually run:\n {cmd}\n") | |
| return False | |
| try: | |
| import importlib; importlib.import_module("tkinter") | |
| os.execv(sys.executable, [sys.executable] + sys.argv) | |
| except ModuleNotFoundError: | |
| sys.stderr.write("Tkinter still cannot be imported.\n") | |
| return False | |
| else: | |
| sys.stderr.write("Cancelled. Please install Tkinter manually.\n") | |
| return False | |
| return True | |
| if not ensure_tkinter(): | |
| sys.exit(1) | |
| import tkinter as tk | |
| from tkinter import filedialog, messagebox, ttk | |
| APP_TITLE = f"AutoTracker GUI (Python) – {OS_NAME}" | |
| DEFAULT_DIRS = { | |
| "sfm": "01 GLOMAP", | |
| "videos": "02 VIDEOS", | |
| "ffmpeg": "03 FFMPEG", | |
| "scenes": "04 SCENES", | |
| "sources": "06 Sources", | |
| } | |
| def run_cmd(cmd_list, cwd=None, log_fn=None): | |
| """Run a command, stream output, and on Windows retry COLMAP if Qt/GL fallback is needed.""" | |
| def _popen(env=None): | |
| return subprocess.Popen(cmd_list, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, | |
| universal_newlines=True, bufsize=1, env=env) | |
| # Prepare env for first attempt (inject Qt paths for COLMAP/GLOMAP on Windows) | |
| env = None | |
| try: | |
| from pathlib import Path as _P | |
| exe_str = cmd_list[0] | |
| exe = _P(str(exe_str).strip('"')) | |
| name = exe.name.lower() | |
| if os.name == 'nt' and exe.exists() and ('colmap' in name or 'glomap' in name): | |
| env = os.environ.copy() | |
| bin_dir = exe.parent | |
| if 'colmap' in name: | |
| colmap_root = bin_dir.parent | |
| plugins_root = colmap_root / 'plugins' | |
| platforms_dir = plugins_root / 'platforms' | |
| if not platforms_dir.exists(): | |
| platforms_dir = bin_dir / 'platforms' | |
| plugins_root = bin_dir | |
| env['QT_PLUGIN_PATH'] = str(plugins_root) | |
| env['QT_QPA_PLATFORM_PLUGIN_PATH'] = str(platforms_dir) | |
| env['QT_QPA_PLATFORM'] = 'windows' | |
| env['PATH'] = str(bin_dir) + os.pathsep + env.get('PATH','') | |
| if log_fn: | |
| log_fn(f"[WIN][Qt][COLMAP] plugins={plugins_root} platforms={platforms_dir}") | |
| else: | |
| base = bin_dir.parent.parent | |
| colmap_root = base / 'colmap' | |
| plugins_root = colmap_root / 'plugins' | |
| platforms_dir = plugins_root / 'platforms' | |
| if not platforms_dir.exists(): | |
| platforms_dir = colmap_root / 'bin' / 'platforms' | |
| if not platforms_dir.exists(): | |
| platforms_dir = bin_dir / 'platforms' | |
| plugins_root = bin_dir | |
| env['QT_PLUGIN_PATH'] = str(plugins_root) | |
| env['QT_QPA_PLATFORM_PLUGIN_PATH'] = str(platforms_dir) | |
| env['QT_QPA_PLATFORM'] = 'windows' | |
| env['PATH'] = str(bin_dir) + os.pathsep + str(colmap_root / 'bin') + os.pathsep + env.get('PATH','') | |
| if log_fn: | |
| log_fn(f"[WIN][Qt][GLOMAP] plugins={plugins_root} platforms={platforms_dir}") | |
| except Exception: | |
| pass | |
| # First run | |
| try: | |
| proc = _popen(env) | |
| except FileNotFoundError as e: | |
| if log_fn: log_fn(f"[ERROR] {e}") | |
| return 1 | |
| lines = [] | |
| for line in proc.stdout: | |
| s = line.rstrip() | |
| lines.append(s) | |
| if log_fn: log_fn(s) | |
| proc.stdout.close() | |
| rc = proc.wait() | |
| # If failed on Windows with typical Qt/GL missing libs, retry offscreen/software | |
| if rc != 0 and os.name == 'nt': | |
| joined = '\n'.join(lines) | |
| if any(k in joined for k in ['Failed to load libEGL', 'Failed to load opengl32sw', 'WGL/OpenGL functions', 'opengl_utils.cc']): | |
| if log_fn: log_fn('[WIN][Qt] Fallback: retry offscreen + software OpenGL') | |
| env2 = (env or os.environ).copy() | |
| env2['QT_QPA_PLATFORM'] = 'offscreen' | |
| env2['QT_OPENGL'] = 'software' | |
| try: | |
| proc2 = _popen(env2) | |
| for line in proc2.stdout: | |
| s = line.rstrip() | |
| if log_fn: log_fn(s) | |
| proc2.stdout.close() | |
| rc2 = proc2.wait() | |
| return rc2 | |
| except Exception as e: | |
| if log_fn: log_fn(f"[WIN][Qt] Fallback start failed: {e}") | |
| return rc | |
| return rc | |
| def run_and_capture(cmd_list, cwd=None): | |
| try: | |
| res = subprocess.run(cmd_list, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) | |
| return res.returncode, res.stdout or "" | |
| except FileNotFoundError as e: | |
| return 127, str(e) | |
| def which_first(names): | |
| for n in names: | |
| p = shutil.which(n) | |
| if p: return p | |
| return None | |
| def detect_cuda(): | |
| return bool(shutil.which("nvcc") or shutil.which("nvidia-smi") or Path("/usr/local/cuda").exists()) | |
| def pkg_install(pm, pkgs, log_fn): | |
| if pm is None or not pkgs: return 1 | |
| if pm == "apt": cmd = f"apt update && apt install -y {' '.join(pkgs)}" | |
| elif pm == "dnf": cmd = f"dnf install -y {' '.join(pkgs)}" | |
| elif pm == "zypper": cmd = f"zypper --non-interactive install -y {' '.join(pkgs)}" | |
| elif pm == "pacman": cmd = f"pacman -Sy --noconfirm {' '.join(pkgs)}" | |
| elif pm == "apk": cmd = f"apk add --no-cache {' '.join(pkgs)}" | |
| else: return 1 | |
| log_fn(f"[pkg] {cmd}") | |
| return subprocess.run(_sudo_wrap(cmd)).returncode | |
| def log_cmd(cmd, log_fn, cwd=None): | |
| txt = " ".join(shlex.quote(str(c)) for c in cmd) | |
| if cwd: txt += f" (cwd={cwd})" | |
| log_fn(txt) | |
| # ---- Download + Entpacken von Release-Archiven (tar.gz/zip) ---- | |
| def is_archive_url(url: str) -> bool: | |
| u = url.lower() | |
| return u.endswith(".tar.gz") or u.endswith(".tgz") or u.endswith(".zip") | |
| def download_file(url: str, dest_file: Path, log_fn) -> bool: | |
| try: | |
| log_fn(f"[dl] Lade {url} …") | |
| dest_file.parent.mkdir(parents=True, exist_ok=True) | |
| # 1) urllib + certifi-Context | |
| try: | |
| try: | |
| import certifi | |
| cafile = certifi.where() | |
| ctx = ssl.create_default_context(cafile=cafile) | |
| except Exception: | |
| # try to install certifi on Windows if missing | |
| if platform.system() == "Windows": | |
| try: | |
| subprocess.run([sys.executable, "-m", "pip", "install", "--user", "--upgrade", "pip", "certifi"], check=False) | |
| import importlib; certifi = importlib.import_module("certifi") | |
| ctx = ssl.create_default_context(cafile=certifi.where()) | |
| except Exception: | |
| ctx = ssl.create_default_context() | |
| else: | |
| ctx = ssl.create_default_context() | |
| with urllib.request.urlopen(url, context=ctx, timeout=120) as r, open(dest_file, "wb") as f: | |
| shutil.copyfileobj(r, f, length=1024*512) | |
| log_fn(f"[dl] Gespeichert: {dest_file}") | |
| return True | |
| except Exception as e1: | |
| log_fn(f"[dl] urllib Fehler: {e1}") | |
| # 2) curl Fallback (Windows 10+ hat curl) | |
| curl = shutil.which("curl") | |
| if curl: | |
| try: | |
| cmd = [curl, "-L", url, "-o", str(dest_file)] | |
| res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=300) | |
| if res.returncode == 0 and dest_file.exists() and dest_file.stat().st_size > 0: | |
| log_fn("[dl] Download via curl erfolgreich.") | |
| return True | |
| else: | |
| log_fn(f"[dl] curl Fehler (rc={res.returncode}): {res.stdout[-400:]}") | |
| except Exception as e2: | |
| log_fn(f"[dl] curl Exception: {e2}") | |
| # 3) PowerShell Fallback (erzwinge TLS1.2) | |
| if platform.system() == "Windows": | |
| ps = shutil.which("powershell") or shutil.which("pwsh") | |
| if ps: | |
| try: | |
| ps_script = ( | |
| "[Net.ServicePointManager]::SecurityProtocol = " | |
| "[Net.SecurityProtocolType]::Tls12 -bor " | |
| "[Net.SecurityProtocolType]::Tls11 -bor " | |
| "[Net.SecurityProtocolType]::Tls; " | |
| f"Invoke-WebRequest -UseBasicParsing -Uri '{url}' -OutFile '{str(dest_file)}'" | |
| ) | |
| res = subprocess.run([ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_script], | |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=300) | |
| if res.returncode == 0 and dest_file.exists() and dest_file.stat().st_size > 0: | |
| log_fn("[dl] Download via PowerShell erfolgreich.") | |
| return True | |
| else: | |
| log_fn(f"[dl] PowerShell Fehler (rc={res.returncode}): {res.stdout[-400:]}") | |
| except Exception as e3: | |
| log_fn(f"[dl] PowerShell Exception: {e3}") | |
| log_fn("[dl] Alle Download-Methoden fehlgeschlagen.") | |
| return False | |
| except Exception as e: | |
| log_fn(f"[dl] Fehler: {e}") | |
| return False | |
| def extract_archive(archive: Path, target_dir: Path, log_fn) -> Path | None: | |
| try: | |
| if target_dir.exists(): | |
| shutil.rmtree(target_dir, ignore_errors=True) | |
| target_dir.parent.mkdir(parents=True, exist_ok=True) | |
| tmp_extract = target_dir.parent / (target_dir.name + "_extract_tmp") | |
| if tmp_extract.exists(): shutil.rmtree(tmp_extract, ignore_errors=True) | |
| tmp_extract.mkdir(parents=True, exist_ok=True) | |
| if str(archive).lower().endswith((".tar.gz",".tgz")): | |
| with tarfile.open(archive, "r:gz") as tar: | |
| tar.extractall(tmp_extract) | |
| elif str(archive).lower().endswith(".zip"): | |
| with zipfile.ZipFile(archive, "r") as z: | |
| z.extractall(tmp_extract) | |
| else: | |
| log_fn(f"[dl] Unbekanntes Archivformat: {archive}") | |
| return None | |
| entries = list(tmp_extract.iterdir()) | |
| if len(entries) == 1 and entries[0].is_dir(): | |
| shutil.move(str(entries[0]), str(target_dir)) | |
| else: | |
| target_dir.mkdir(parents=True, exist_ok=True) | |
| for e in entries: | |
| shutil.move(str(e), str(target_dir)) | |
| shutil.rmtree(tmp_extract, ignore_errors=True) | |
| log_fn(f"[dl] Entpackt nach: {target_dir}") | |
| return target_dir | |
| except Exception as e: | |
| log_fn(f"[dl] Entpacken fehlgeschlagen: {e}") | |
| return None | |
| def ensure_source_from_url(url: str, dest_dir: Path, log_fn) -> Path | None: | |
| if is_archive_url(url): | |
| filename = url.split("/")[-1] | |
| archive_path = dest_dir.parent / filename | |
| if archive_path.exists(): | |
| try: archive_path.unlink() | |
| except Exception: pass | |
| ok = download_file(url, archive_path, log_fn) | |
| if not ok: return None | |
| src = extract_archive(archive_path, dest_dir, log_fn) | |
| try: archive_path.unlink() | |
| except Exception: pass | |
| return src | |
| else: | |
| return ensure_git_clone_or_refresh(url, dest_dir, "", log_fn) == 0 and dest_dir or None | |
| # ---- Git fallback ---- | |
| def ensure_git_clone_or_refresh(url: str, dest: Path, branch: str, log_fn): | |
| dest = Path(dest) | |
| if dest.exists() and not (dest / ".git").is_dir(): | |
| log_fn(f"[git] Ziel existiert, ist aber kein Git-Repo: {dest} → entferne Ordner…") | |
| try: shutil.rmtree(dest) | |
| except Exception as e: | |
| log_fn(f"[git] Entfernen fehlgeschlagen: {e}") | |
| return 1 | |
| if not dest.exists(): | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| cmd = ["git", "clone", url, str(dest)]; log_cmd(cmd, log_fn) | |
| code = run_cmd(cmd, log_fn=log_fn) | |
| if code != 0: return code | |
| cmd = ["git", "-C", str(dest), "fetch", "--all", "--tags"]; log_cmd(cmd, log_fn); run_cmd(cmd, log_fn=log_fn) | |
| if branch and ("://" not in branch) and (not branch.endswith(".git")): | |
| cmd = ["git", "-C", str(dest), "checkout", branch]; log_cmd(cmd, log_fn); run_cmd(cmd, log_fn=log_fn) | |
| cmd = ["git", "-C", str(dest), "pull", "--ff-only"]; log_cmd(cmd, log_fn); return run_cmd(cmd, log_fn=log_fn) | |
| def cmake_configure_ninja(src, build_dir, log_fn, extra_args=None): | |
| build_dir = Path(build_dir) | |
| if build_dir.exists(): | |
| shutil.rmtree(build_dir, ignore_errors=True) | |
| build_dir.mkdir(parents=True, exist_ok=True) | |
| args = extra_args or [] | |
| cfg = ["cmake", "-S", str(src), "-B", str(build_dir), "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release"] + args | |
| log_cmd(cfg, log_fn) | |
| return run_cmd(cfg, log_fn=log_fn) | |
| def ninja_build(build_dir, log_fn): | |
| cmd = ["ninja"]; log_cmd(cmd, log_fn, cwd=build_dir); return run_cmd(cmd, cwd=str(build_dir), log_fn=log_fn) | |
| def ninja_install(build_dir, log_fn): | |
| cmd = ["ninja", "install"]; log_cmd(cmd, log_fn, cwd=build_dir); return run_cmd(cmd, cwd=str(build_dir), log_fn=log_fn) | |
| def find_binary(root, names): | |
| root = Path(root) | |
| best = None; best_depth = 10**9 | |
| for dirpath, dirnames, filenames in os.walk(root): | |
| for n in names: | |
| if n in filenames: | |
| p = Path(dirpath) / n | |
| depth = len(Path(dirpath).parts) | |
| if depth < best_depth: | |
| best = p; best_depth = depth | |
| return best | |
| def ensure_binary_installed(bin_expected: Path, build_dir: Path, binary_name: str, log_fn): | |
| try: bin_expected.parent.mkdir(parents=True, exist_ok=True) | |
| except Exception as e: log_fn(f"[INSTALL] Konnte Zielordner nicht erstellen: {bin_expected.parent} ({e})") | |
| if bin_expected.exists(): | |
| try: os.chmod(bin_expected, 0o755) | |
| except Exception: pass | |
| return True | |
| candidate = find_binary(build_dir, [binary_name]) | |
| if candidate and Path(candidate).exists(): | |
| try: | |
| shutil.copy2(candidate, bin_expected); os.chmod(bin_expected, 0o755) | |
| log_fn(f"[INSTALL] Binary via Fallback kopiert: {candidate} -> {bin_expected}") | |
| return True | |
| except Exception as e: | |
| log_fn(f"[INSTALL] Fallback-Kopie fehlgeschlagen: {e}") | |
| else: | |
| log_fn(f"[INSTALL] Fallback: Binary {binary_name} nicht im Build-Ordner gefunden.") | |
| return False | |
| # ---- CUDA/GCC-Heuristik ---- | |
| def _get_version_output(prog): | |
| try: | |
| res = subprocess.run([prog, "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) | |
| if res.returncode == 0: | |
| return res.stdout or "" | |
| except Exception: | |
| pass | |
| return "" | |
| def _detect_nvcc_gcc_combo(): | |
| nvcc_out = _get_version_output("nvcc") | |
| gxx_out = _get_version_output("g++") | |
| nvcc_major = None; nvcc_minor = None | |
| import re | |
| m = re.search(r"release\s+(\d+)\.(\d+)", nvcc_out or "", re.I) | |
| if m: | |
| nvcc_major, nvcc_minor = int(m.group(1)), int(m.group(2)) | |
| gxx_major = None | |
| m2 = re.search(r"g\+\+\s+\(.*\)\s+(\d+)\.", gxx_out or "") | |
| if m2: | |
| gxx_major = int(m2.group(1)) | |
| return (nvcc_major, nvcc_minor, gxx_major) | |
| def _maybe_cuda_host_flag(pm, log_fn): | |
| nvcc_major, nvcc_minor, gxx_major = _detect_nvcc_gcc_combo() | |
| if nvcc_major is None or gxx_major is None: | |
| return [] | |
| if nvcc_major == 12 and (gxx_major is not None and gxx_major >= 13): | |
| from shutil import which | |
| host = which("g++-12") | |
| if host is None and pm == "apt": | |
| log_fn("[INSTALL] g++-12 nicht gefunden – versuche Installation (apt)…") | |
| pkg_install(pm, ["gcc-12", "g++-12"], log_fn) | |
| host = which("g++-12") | |
| if host: | |
| log_fn(f"[INSTALL] Setze CUDA Host-Compiler: {host}") | |
| return [f"-DCMAKE_CUDA_HOST_COMPILER={host}", "-DCUDA_ENABLED=ON"] | |
| else: | |
| log_fn("[INSTALL] Warnung: g++-12 nicht verfügbar. Fallback auf CPU-Build (CUDA wird deaktiviert).") | |
| return ["-DCUDA_ENABLED=OFF"] | |
| return ["-DCUDA_ENABLED=ON"] | |
| # ----------------------------- GUI helpers ----------------------------- | |
| def find_in_subdir_with_bin(top: Path, subdir: str, names): | |
| base = top / subdir | |
| for n in names: | |
| p = base / n | |
| if p.exists(): return str(p.resolve()) | |
| bin_dir = base / "bin" | |
| for n in names: | |
| p = bin_dir / n | |
| if p.exists(): return str(p.resolve()) | |
| return None | |
| def find_in_nested_subdir_with_bin(top: Path, base_subdir: str, program_subdir: str, names): | |
| base = top / base_subdir / program_subdir | |
| for n in names: | |
| p = base / n | |
| if p.exists(): return str(p.resolve()) | |
| bin_dir = base / "bin" | |
| for n in names: | |
| p = bin_dir / n | |
| if p.exists(): return str(p.resolve()) | |
| return None | |
| def looks_like_05_script(name: str) -> bool: | |
| s = name.strip().lower().replace(" ", "").replace("-", "").replace("_", "") | |
| return s in ("05script", "05scripts", "05scriptfolder") | |
| class PlaceholderEntry(ttk.Entry): | |
| def __init__(self, master=None, placeholder="", textvariable=None, **kw): | |
| super().__init__(master, textvariable=textvariable, **kw) | |
| self._placeholder = placeholder; self._placeholder_active = False | |
| self._normal_fg = self.cget("foreground") if "foreground" in self.keys() else None | |
| self._placeholder_fg = "#888" | |
| self._var = textvariable if textvariable is not None else tk.StringVar() | |
| if textvariable is None: self.configure(textvariable=self._var) | |
| self.bind("<FocusIn>", self._clear_placeholder); self.bind("<FocusOut>", self._add_placeholder_if_empty) | |
| self._add_placeholder_if_empty() | |
| def set_text(self, text: str): | |
| self._placeholder_active = False; self.configure(foreground=self._normal_fg); self._var.set(text) | |
| def set_placeholder(self, text: str): | |
| self._placeholder = text; self._add_placeholder_if_empty() | |
| def get_text(self) -> str: | |
| return "" if self._placeholder_active else self._var.get() | |
| def _clear_placeholder(self, *_): | |
| if self._placeholder_active: | |
| self._var.set(""); self.configure(foreground=self._normal_fg); self._placeholder_active = False | |
| def _add_placeholder_if_empty(self, *_): | |
| if not self._var.get(): | |
| self._placeholder_active = True; self.configure(foreground=self._placeholder_fg); self._var.set(self._placeholder) | |
| class AutoTrackerGUI(tk.Tk): | |
| def __init__(self): | |
| super().__init__() | |
| # language | |
| self.lang = detect_lang() | |
| self.S = I18N[self.lang] | |
| self.title(self.S["app_title"].format(os=OS_NAME)) | |
| self.geometry("1120x930"); self.minsize(1000, 830) | |
| self._worker = None; self._stop_flag = False; self._elapsed_start = None; self._elapsed_job = None | |
| # --- top bar with language dropdown --- | |
| topbar = ttk.Frame(self); topbar.pack(fill="x", padx=10, pady=(10, 0)) | |
| ttk.Label(topbar, text=self.S["lang_label"]).pack(side="left") | |
| self.lang_var = tk.StringVar(value=self.S["lang_de"] if self.lang=="de" else self.S["lang_en"]) | |
| self.lang_combo = ttk.Combobox(topbar, width=12, state="readonly", | |
| values=[I18N["de"]["lang_de"], I18N["en"]["lang_en"]], | |
| textvariable=self.lang_var) | |
| self.lang_combo.pack(side="left", padx=(6, 0)) | |
| self.lang_combo.bind("<<ComboboxSelected>>", self._on_lang_changed) | |
| self.info_btn = ttk.Button(topbar, text=self.S["info_btn"], command=self._open_about_dialog) | |
| self.info_btn.pack(side="right") | |
| # --- paths & tools --- | |
| self.paths_frame = ttk.LabelFrame(self, text=self.S["paths_tools"]); self.paths_frame.pack(fill="x", padx=10, pady=(10, 6)) | |
| script_dir = Path(__file__).resolve().parent; parent = script_dir.parent | |
| if looks_like_05_script(script_dir.name) or (parent / DEFAULT_DIRS["sfm"]).exists() or (parent / DEFAULT_DIRS["ffmpeg"]).exists(): | |
| top_candidate = parent | |
| else: top_candidate = script_dir | |
| self.top_dir_var = tk.StringVar(value=str(top_candidate)) | |
| row = 0 | |
| self.lbl_project_top = ttk.Label(self.paths_frame, text=self.S["project_top"]) | |
| self.lbl_project_top.grid(row=row, column=0, sticky="w", padx=8, pady=6) | |
| self.top_entry = ttk.Entry(self.paths_frame, textvariable=self.top_dir_var, width=80); self.top_entry.grid(row=row, column=1, sticky="we", padx=(0, 6), pady=6) | |
| self.paths_frame.grid_columnconfigure(1, weight=1) | |
| top_btns = ttk.Frame(self.paths_frame); top_btns.grid(row=row, column=2, padx=6, pady=6, sticky="e") | |
| self.btn_browse_top = ttk.Button(top_btns, text=self.S["browse"], command=lambda: self._browse(self.top_dir_var, is_dir=True)); self.btn_browse_top.pack(side="left") | |
| self.btn_redetect = ttk.Button(top_btns, text=self.S["rediscover"], command=self._auto_detect_tools); self.btn_redetect.pack(side="left", padx=(6, 0)) | |
| row += 1 | |
| os_frame = ttk.Frame(self.paths_frame); os_frame.grid(row=row, column=0, columnspan=3, sticky="we", padx=8, pady=(0, 6)) | |
| osr = _read_os_release() if OS_NAME == "Linux" else {}; distro = f"{osr.get('NAME','')} {osr.get('VERSION','')}".strip() | |
| self.lbl_os = ttk.Label(os_frame, text=self.S["os_detected"].format(os=OS_NAME, distro=('– ' + distro) if distro else '')); self.lbl_os.pack(side="left") | |
| self.use_path_linux_var = tk.BooleanVar(value=True) # default True: auto PATH fallback if not found | |
| self.cb_search_path = ttk.Checkbutton(os_frame, text=self.S["search_path_cb"], variable=self.use_path_linux_var); self.cb_search_path.pack(side="left", padx=(12, 0)) | |
| self.search_path_btn = ttk.Button(os_frame, text=self.S["search_path_btn"], command=self._detect_from_system_path); self.search_path_btn.pack(side="left", padx=(12, 0)) | |
| self.install_tools_btn = ttk.Button(os_frame, text=self.S["install_tools"], command=self._open_installer_dialog); self.install_tools_btn.pack(side="left", padx=(12, 0)) | |
| if IS_WINDOWS or OS_NAME != "Linux": | |
| self.cb_search_path.state(["disabled"]); self.search_path_btn.state(["disabled"]) | |
| if OS_NAME not in ("Linux", "Windows"): self.install_tools_btn.state(["disabled"]) | |
| row += 1 | |
| self.ffmpeg_placeholder = self.S["ffmpeg_placeholder"] | |
| self.colmap_placeholder = self.S["colmap_placeholder"] | |
| self.glomap_placeholder = self.S["glomap_placeholder"] | |
| self.lbl_ffmpeg = ttk.Label(self.paths_frame, text=self.S["ffmpeg_label"]); self.lbl_ffmpeg.grid(row=row, column=0, sticky="w", padx=8, pady=6) | |
| self.ffmpeg_var = tk.StringVar(); self.ffmpeg_entry = PlaceholderEntry(self.paths_frame, placeholder=self.ffmpeg_placeholder, textvariable=self.ffmpeg_var, width=80) | |
| self.ffmpeg_entry.grid(row=row, column=1, sticky="we", padx=(0, 6), pady=6) | |
| self.btn_ffmpeg_browse = ttk.Button(self.paths_frame, text=self.S["browse"], command=lambda: self._browse_exe(self.ffmpeg_entry)); self.btn_ffmpeg_browse.grid(row=row, column=2, padx=6, pady=6) | |
| row += 1 | |
| self.lbl_colmap = ttk.Label(self.paths_frame, text=self.S["colmap_label"]); self.lbl_colmap.grid(row=row, column=0, sticky="w", padx=8, pady=6) | |
| self.colmap_var = tk.StringVar(); self.colmap_entry = PlaceholderEntry(self.paths_frame, placeholder=self.colmap_placeholder, textvariable=self.colmap_var, width=80) | |
| self.colmap_entry.grid(row=row, column=1, sticky="we", padx=(0, 6), pady=6) | |
| self.btn_colmap_browse = ttk.Button(self.paths_frame, text=self.S["browse"], command=lambda: self._browse_exe(self.colmap_entry)); self.btn_colmap_browse.grid(row=row, column=2, padx=6, pady=6) | |
| row += 1 | |
| self.lbl_glomap = ttk.Label(self.paths_frame, text=self.S["glomap_label"]); self.lbl_glomap.grid(row=row, column=0, sticky="w", padx=8, pady=6) | |
| self.glomap_var = tk.StringVar(); self.glomap_entry = PlaceholderEntry(self.paths_frame, placeholder=self.glomap_placeholder, textvariable=self.glomap_var, width=80) | |
| self.glomap_entry.grid(row=row, column=1, sticky="we", padx=(0, 6), pady=6) | |
| self.btn_glomap_browse = ttk.Button(self.paths_frame, text=self.S["browse"], command=lambda: self._browse_exe(self.glomap_entry)); self.btn_glomap_browse.grid(row=row, column=2, padx=6, pady=6) | |
| row += 1 | |
| # --- options frame --- | |
| self.opts_frame = ttk.LabelFrame(self, text=self.S["options"]); self.opts_frame.pack(fill="x", padx=10, pady=6) | |
| self.res_mode = tk.StringVar(value="keep"); self.width_var = tk.StringVar(value=""); self.height_var = tk.StringVar(value="") | |
| res_frame = ttk.Frame(self.opts_frame); res_frame.pack(fill="x", padx=8, pady=6) | |
| self.lbl_res_title = ttk.Label(res_frame, text=self.S["res_title"]); self.lbl_res_title.grid(row=0, column=0, sticky="w") | |
| self.rb_keep = ttk.Radiobutton(res_frame, text=self.S["res_keep"], variable=self.res_mode, value="keep"); self.rb_keep.grid(row=1, column=0, sticky="w") | |
| self.rb_w = ttk.Radiobutton(res_frame, text=self.S["res_only_w"], variable=self.res_mode, value="w"); self.rb_w.grid(row=1, column=1, sticky="w") | |
| self.entry_w = ttk.Entry(res_frame, width=8, textvariable=self.width_var); self.entry_w.grid(row=1, column=2, sticky="w", padx=(4, 12)) | |
| self.rb_h = ttk.Radiobutton(res_frame, text=self.S["res_only_h"], variable=self.res_mode, value="h"); self.rb_h.grid(row=1, column=3, sticky="w") | |
| self.entry_h = ttk.Entry(res_frame, width=8, textvariable=self.height_var); self.entry_h.grid(row=1, column=4, sticky="w", padx=(4, 12)) | |
| self.rb_wh = ttk.Radiobutton(res_frame, text=self.S["res_wh"], variable=self.res_mode, value="wh"); self.rb_wh.grid(row=1, column=5, sticky="w") | |
| self.entry_w2 = ttk.Entry(res_frame, width=8, textvariable=self.width_var); self.entry_w2.grid(row=1, column=6, sticky="w", padx=(4, 2)) | |
| ttk.Label(res_frame, text="×").grid(row=1, column=7, sticky="w") | |
| self.entry_h2 = ttk.Entry(res_frame, width=8, textvariable=self.height_var); self.entry_h2.grid(row=1, column=8, sticky="w", padx=(2, 0)) | |
| self.use_gpu_var = tk.BooleanVar(value=True) | |
| gpu_frame = ttk.Frame(self.opts_frame); gpu_frame.pack(fill="x", padx=8, pady=(0, 6)) | |
| self.cb_gpu = ttk.Checkbutton(gpu_frame, text=self.S["gpu_check"], variable=self.use_gpu_var); self.cb_gpu.grid(row=0, column=0, sticky="w") | |
| more_opts = ttk.Frame(self.opts_frame); more_opts.pack(fill="x", padx=8, pady=(0, 6)) | |
| self.jpeg_q_var = tk.StringVar(value="2"); self.sift_max_img_var = tk.StringVar(value="4096"); self.seq_overlap_var = tk.StringVar(value="15") | |
| self.lbl_jpeg = ttk.Label(more_opts, text=self.S["jpeg_q"]); self.lbl_jpeg.grid(row=0, column=0, sticky="w") | |
| ttk.Entry(more_opts, width=6, textvariable=self.jpeg_q_var).grid(row=0, column=1, sticky="w", padx=(4, 16)) | |
| self.lbl_sift = ttk.Label(more_opts, text=self.S["sift_max"]); self.lbl_sift.grid(row=0, column=2, sticky="w") | |
| ttk.Entry(more_opts, width=8, textvariable=self.sift_max_img_var).grid(row=0, column=3, sticky="w", padx=(4, 16)) | |
| self.lbl_overlap = ttk.Label(more_opts, text=self.S["seq_overlap"]); self.lbl_overlap.grid(row=0, column=4, sticky="w") | |
| ttk.Entry(more_opts, width=6, textvariable=self.seq_overlap_var).grid(row=0, column=5, sticky="w", padx=(4, 16)) | |
| self.fps_mode = tk.StringVar(value="all"); self.every_n_var = tk.StringVar(value="2"); self.target_fps_var = tk.StringVar(value="5") | |
| fps_frame = ttk.Frame(self.opts_frame); fps_frame.pack(fill="x", padx=8, pady=(0, 6)) | |
| self.lbl_fps = ttk.Label(fps_frame, text=self.S["fps_title"]); self.lbl_fps.grid(row=0, column=0, sticky="w") | |
| self.rb_all = ttk.Radiobutton(fps_frame, text=self.S["fps_all"], variable=self.fps_mode, value="all"); self.rb_all.grid(row=1, column=0, sticky="w") | |
| self.rb_every = ttk.Radiobutton(fps_frame, text=self.S["fps_every"], variable=self.fps_mode, value="every"); self.rb_every.grid(row=1, column=1, sticky="w") | |
| self.entry_every = ttk.Entry(fps_frame, width=4, textvariable=self.every_n_var); self.entry_every.grid(row=1, column=2, sticky="w", padx=(4, 2)) | |
| self.lbl_every_suf = ttk.Label(fps_frame, text=self.S["fps_every_suffix"]); self.lbl_every_suf.grid(row=1, column=3, sticky="w") | |
| self.rb_target = ttk.Radiobutton(fps_frame, text=self.S["fps_target"], variable=self.fps_mode, value="target"); self.rb_target.grid(row=1, column=4, sticky="w", padx=(12, 0)) | |
| self.entry_target = ttk.Entry(fps_frame, width=6, textvariable=self.target_fps_var); self.entry_target.grid(row=1, column=5, sticky="w", padx=(4, 2)) | |
| self.lbl_fps_hint = ttk.Label(fps_frame, text=self.S["fps_hint"]); self.lbl_fps_hint.grid(row=1, column=6, sticky="w") | |
| # --- videos list --- | |
| self.videos_frame = ttk.LabelFrame(self, text=self.S["videos"]); self.videos_frame.pack(fill="both", expand=True, padx=10, pady=6) | |
| self.video_list = tk.Listbox(self.videos_frame, selectmode="extended"); self.video_list.pack(fill="both", expand=True, side="left", padx=(8, 0), pady=8) | |
| btns = ttk.Frame(self.videos_frame); btns.pack(side="left", fill="y", padx=8, pady=8) | |
| self.btn_add_videos = ttk.Button(btns, text=self.S["add_videos"], command=self.add_videos); self.btn_add_videos.pack(fill="x", pady=(0, 4)) | |
| self.btn_remove_sel = ttk.Button(btns, text=self.S["remove_sel"], command=self.remove_selected); self.btn_remove_sel.pack(fill="x") | |
| self.btn_clear_list = ttk.Button(btns, text=self.S["clear_list"], command=self.clear_videos); self.btn_clear_list.pack(fill="x", pady=(4, 0)) | |
| self.scenes_dir_var = tk.StringVar(value=str(Path(self.top_dir_var.get()) / DEFAULT_DIRS["scenes"])) | |
| out_frame = ttk.Frame(self); out_frame.pack(fill="x", padx=10, pady=(0, 6)) | |
| self.lbl_scenes = ttk.Label(out_frame, text=self.S["scenes_dir"]); self.lbl_scenes.pack(side="left") | |
| ttk.Entry(out_frame, textvariable=self.scenes_dir_var).pack(side="left", fill="x", expand=True, padx=8) | |
| self.btn_browse_scenes = ttk.Button(out_frame, text=self.S["browse"], command=lambda: self._browse_dir(self.scenes_dir_var)); self.btn_browse_scenes.pack(side="left") | |
| run_frame = ttk.Frame(self); run_frame.pack(fill="x", padx=10, pady=(6, 6)) | |
| self.run_btn = ttk.Button(run_frame, text=self.S["start"], command=self.start_run); self.run_btn.pack(side="left") | |
| self.btn_test = ttk.Button(run_frame, text=self.S["test_tools"], command=self.test_tools); self.btn_test.pack(side="left", padx=(8, 0)) | |
| self.elapsed_prefix = self.S["elapsed"] | |
| self.elapsed_var = tk.StringVar(value=f"{self.elapsed_prefix}: 00:00:00"); ttk.Label(run_frame, textvariable=self.elapsed_var).pack(side="left", padx=(8, 0)) | |
| self.progress = ttk.Progressbar(run_frame, mode="determinate"); self.progress.pack(side="left", fill="x", expand=True, padx=10) | |
| self.log = tk.Text(self, height=16, wrap="word"); self.log.pack(fill="both", expand=False, padx=10, pady=(6, 10)) | |
| self.top_dir_var.trace_add("write", self._on_top_changed) | |
| self._maybe_offer_create_structure(); self._auto_detect_tools() | |
| # ---- language handlers ---- | |
| def _on_lang_changed(self, *_): | |
| val = self.lang_var.get() | |
| if val == I18N["de"]["lang_de"]: self.lang = "de" | |
| else: self.lang = "en" | |
| self.S = I18N[self.lang] | |
| self._apply_i18n() | |
| def _apply_i18n(self): | |
| try: | |
| self.info_btn.configure(text=self.S["info_btn"]) | |
| except Exception: | |
| pass | |
| self.title(self.S["app_title"].format(os=OS_NAME)) | |
| self.paths_frame.configure(text=self.S["paths_tools"]) | |
| self.lbl_project_top.configure(text=self.S["project_top"]) | |
| self.btn_browse_top.configure(text=self.S["browse"]) | |
| self.btn_redetect.configure(text=self.S["rediscover"]) | |
| osr = _read_os_release() if OS_NAME == "Linux" else {}; distro = f"{osr.get('NAME','')} {osr.get('VERSION','')}".strip() | |
| self.lbl_os.configure(text=self.S["os_detected"].format(os=OS_NAME, distro=('– ' + distro) if distro else '')) | |
| self.cb_search_path.configure(text=self.S["search_path_cb"]) | |
| self.search_path_btn.configure(text=self.S["search_path_btn"]) | |
| self.install_tools_btn.configure(text=self.S["install_tools"]) | |
| # placeholders | |
| self.ffmpeg_placeholder = self.S["ffmpeg_placeholder"] | |
| self.colmap_placeholder = self.S["colmap_placeholder"] | |
| self.glomap_placeholder = self.S["glomap_placeholder"] | |
| if not self.ffmpeg_entry.get_text(): self.ffmpeg_entry.set_placeholder(self.ffmpeg_placeholder) | |
| if not self.colmap_entry.get_text(): self.colmap_entry.set_placeholder(self.colmap_placeholder) | |
| if not self.glomap_entry.get_text(): self.glomap_entry.set_placeholder(self.glomap_placeholder) | |
| self.lbl_ffmpeg.configure(text=self.S["ffmpeg_label"]); self.btn_ffmpeg_browse.configure(text=self.S["browse"]) | |
| self.lbl_colmap.configure(text=self.S["colmap_label"]); self.btn_colmap_browse.configure(text=self.S["browse"]) | |
| self.lbl_glomap.configure(text=self.S["glomap_label"]); self.btn_glomap_browse.configure(text=self.S["browse"]) | |
| self.opts_frame.configure(text=self.S["options"]) | |
| self.lbl_res_title.configure(text=self.S["res_title"]) | |
| self.rb_keep.configure(text=self.S["res_keep"]) | |
| self.rb_w.configure(text=self.S["res_only_w"]) | |
| self.rb_h.configure(text=self.S["res_only_h"]) | |
| self.rb_wh.configure(text=self.S["res_wh"]) | |
| self.cb_gpu.configure(text=self.S["gpu_check"]) | |
| self.lbl_jpeg.configure(text=self.S["jpeg_q"]) | |
| self.lbl_sift.configure(text=self.S["sift_max"]) | |
| self.lbl_overlap.configure(text=self.S["seq_overlap"]) | |
| self.lbl_fps.configure(text=self.S["fps_title"]) | |
| self.rb_all.configure(text=self.S["fps_all"]) | |
| self.rb_every.configure(text=self.S["fps_every"]) | |
| self.lbl_every_suf.configure(text=self.S["fps_every_suffix"]) | |
| self.rb_target.configure(text=self.S["fps_target"]) | |
| self.lbl_fps_hint.configure(text=self.S["fps_hint"]) | |
| self.videos_frame.configure(text=self.S["videos"]) | |
| self.btn_add_videos.configure(text=self.S["add_videos"]) | |
| self.btn_remove_sel.configure(text=self.S["remove_sel"]) | |
| self.btn_clear_list.configure(text=self.S["clear_list"]) | |
| self.lbl_scenes.configure(text=self.S["scenes_dir"]) | |
| self.btn_browse_scenes.configure(text=self.S["browse"]) | |
| self.run_btn.configure(text=self.S["start"]) | |
| self.btn_test.configure(text=self.S["test_tools"]) | |
| self.elapsed_prefix = self.S["elapsed"] | |
| # update displayed string but keep time value | |
| try: | |
| cur = self.elapsed_var.get() | |
| # replace prefix before colon | |
| if ":" in cur: | |
| suffix = cur.split(":", 1)[1].strip() | |
| self.elapsed_var.set(f"{self.elapsed_prefix}: {suffix}") | |
| else: | |
| self.elapsed_var.set(f"{self.elapsed_prefix}: 00:00:00") | |
| except Exception: | |
| self.elapsed_var.set(f"{self.elapsed_prefix}: 00:00:00") | |
| # ---- UI helper ---- | |
| def _browse(self, var, is_dir=False): | |
| title = self.S["dlg_pick_dir"] if is_dir else self.S["dlg_pick_file"] | |
| val = filedialog.askdirectory(title=title) if is_dir else filedialog.askopenfilename(title=title) | |
| if val: var.set(val) | |
| def _browse_exe(self, entry: PlaceholderEntry): | |
| val = filedialog.askopenfilename(title=self.S["dlg_pick_file"]) | |
| if val: entry.set_text(str(Path(val).resolve())) | |
| def _browse_dir(self, var): | |
| val = filedialog.askdirectory(title=self.S["dlg_pick_dir"]) | |
| if val: var.set(val) | |
| def _on_top_changed(self, *args): | |
| top = Path(self.top_dir_var.get()) | |
| self.scenes_dir_var.set(str(top / DEFAULT_DIRS["scenes"])) | |
| self._auto_detect_tools() | |
| def _project_missing_dirs(self, top: Path): | |
| base_dirs = [top / DEFAULT_DIRS["sfm"], top / DEFAULT_DIRS["videos"], top / DEFAULT_DIRS["ffmpeg"], top / DEFAULT_DIRS["scenes"], top / DEFAULT_DIRS["sources"]] | |
| sub_dirs = [top / DEFAULT_DIRS["sfm"] / "colmap" / "bin", | |
| top / DEFAULT_DIRS["sfm"] / "glomap" / "bin", | |
| top / DEFAULT_DIRS["ffmpeg"] / "bin", | |
| top / DEFAULT_DIRS["sources"] / "colmap", | |
| top / DEFAULT_DIRS["sources"] / "glomap"] | |
| return [d for d in base_dirs + sub_dirs if not d.exists()] | |
| def _create_project_structure(self, base: Path): | |
| dirs = [base / DEFAULT_DIRS["sfm"], base / DEFAULT_DIRS["videos"], base / DEFAULT_DIRS["ffmpeg"], base / DEFAULT_DIRS["scenes"], base / DEFAULT_DIRS["sources"], | |
| base / DEFAULT_DIRS["sfm"] / "colmap" / "bin", base / DEFAULT_DIRS["sfm"] / "glomap" / "bin", base / DEFAULT_DIRS["ffmpeg"] / "bin", | |
| base / DEFAULT_DIRS["sources"] / "colmap", base / DEFAULT_DIRS["sources"] / "glomap"] | |
| for d in dirs: d.mkdir(parents=True, exist_ok=True) | |
| def _maybe_offer_create_structure(self): | |
| top = Path(self.top_dir_var.get()); missing = self._project_missing_dirs(top) | |
| if not missing: return | |
| if messagebox.askyesno(self.S["dlg_create_structure_title"], self.S["dlg_create_structure_msg"]): | |
| base_dir = filedialog.askdirectory(title=self.S["dlg_create_structure_where"], initialdir=str(top)) | |
| if not base_dir: return | |
| base = Path(base_dir); self._create_project_structure(base) | |
| self.top_dir_var.set(str(base)); self.scenes_dir_var.set(str(base / DEFAULT_DIRS["scenes"])) | |
| self._auto_detect_tools(); messagebox.showinfo(self.S["dlg_done"], self.S["dlg_structure_created"]) | |
| def _auto_detect_tools(self): | |
| top = Path(self.top_dir_var.get()) | |
| ff_names = ["ffmpeg.exe", "ffmpeg"] if IS_WINDOWS else ["ffmpeg"] | |
| cm_names = ["colmap.exe", "colmap"] if IS_WINDOWS else ["colmap"] | |
| gm_names = ["glomap.exe", "glomap"] if IS_WINDOWS else ["glomap"] | |
| ff_path = find_in_subdir_with_bin(top, DEFAULT_DIRS["ffmpeg"], ff_names) | |
| # also check nested '03 FFMPEG/bin' which find_in_subdir_with_bin already covers; but keep PATH fallback auto if allowed | |
| if (not ff_path) and (OS_NAME == "Linux") and self.use_path_linux_var.get(): | |
| ff_path = which_first(ff_names) | |
| if ff_path: self.ffmpeg_entry.set_text(ff_path) | |
| else: self.ffmpeg_entry.set_placeholder(self.ffmpeg_placeholder) | |
| cm_path = find_in_nested_subdir_with_bin(top, DEFAULT_DIRS["sfm"], "colmap", cm_names) or find_in_subdir_with_bin(top, DEFAULT_DIRS["sfm"], cm_names) | |
| if (not cm_path) and (OS_NAME == "Linux") and self.use_path_linux_var.get(): | |
| cm_path = which_first(cm_names) | |
| if cm_path: self.colmap_entry.set_text(cm_path) | |
| else: self.colmap_entry.set_placeholder(self.colmap_placeholder) | |
| gm_path = find_in_nested_subdir_with_bin(top, DEFAULT_DIRS["sfm"], "glomap", gm_names) or find_in_subdir_with_bin(top, DEFAULT_DIRS["sfm"], gm_names) | |
| if (not gm_path) and (OS_NAME == "Linux") and self.use_path_linux_var.get(): | |
| gm_path = which_first(gm_names) | |
| if gm_path: self.glomap_entry.set_text(gm_path) | |
| else: self.glomap_entry.set_placeholder(self.glomap_placeholder) | |
| def _detect_from_system_path(self): | |
| if OS_NAME != "Linux": | |
| messagebox.showinfo("Info", self.S["msg_linux_only"]); return | |
| found = [] | |
| if not self.ffmpeg_entry.get_text(): | |
| p = which_first(["ffmpeg"]); | |
| if p: self.ffmpeg_entry.set_text(p); found.append(("ffmpeg", p)) | |
| if not self.colmap_entry.get_text(): | |
| p = which_first(["colmap"]); | |
| if p: self.colmap_entry.set_text(p); found.append(("COLMAP", p)) | |
| if not self.glomap_entry.get_text(): | |
| p = which_first(["glomap"]); | |
| if p: self.glomap_entry.set_text(p); found.append(("GLOMAP", p)) | |
| if found: messagebox.showinfo("Info", self.S["msg_found"] + "\n" + "\n".join([f"{n}: {p}" for n,p in found])) | |
| else: messagebox.showinfo("Info", self.S["msg_found_none"]) | |
| def _sync_windows_prebuilt_urls(self): | |
| """Update COLMAP/GLOMAP URL entries to match the CUDA checkbox (Windows only).""" | |
| try: | |
| if OS_NAME != "Windows": | |
| return | |
| want_cuda = False | |
| if hasattr(self, "use_cuda_build_var"): | |
| try: | |
| want_cuda = bool(self.use_cuda_build_var.get()) | |
| except Exception: | |
| want_cuda = False | |
| else: | |
| want_cuda = detect_cuda() | |
| colmap_url_cuda = "https://github.com/colmap/colmap/releases/download/3.12.3/colmap-x64-windows-cuda.zip" | |
| colmap_url_nocuda = "https://github.com/colmap/colmap/releases/download/3.12.3/colmap-x64-windows-nocuda.zip" | |
| glomap_url_cuda = "https://github.com/colmap/glomap/releases/download/1.1.0/glomap-x64-windows-cuda.zip" | |
| glomap_url_nocuda = "https://github.com/colmap/glomap/releases/download/1.1.0/glomap-x64-windows-nocuda.zip" | |
| if hasattr(self, "colmap_url_var"): | |
| self.colmap_url_var.set(colmap_url_cuda if want_cuda else colmap_url_nocuda) | |
| if hasattr(self, "glomap_url_var"): | |
| self.glomap_url_var.set(glomap_url_cuda if want_cuda else glomap_url_nocuda) | |
| except Exception: | |
| pass | |
| def _open_installer_dialog(self): | |
| # Installer unterstützt Linux (Build) und Windows (Prebuilt-Downloads) | |
| # macOS momentan nicht automatisiert. | |
| pass | |
| win = tk.Toplevel(self); win.title(self.S["installer_title"]); win.geometry("820x600"); win.grab_set() | |
| frm = ttk.Frame(win); frm.pack(fill="both", expand=True, padx=12, pady=12) | |
| ttk.Label(frm, text=self.S["installer_what"]).grid(row=0, column=0, columnspan=4, sticky="w", pady=(0,6)) | |
| self.inst_ffmpeg = tk.BooleanVar(value=True); self.inst_colmap = tk.BooleanVar(value=False); self.inst_glomap = tk.BooleanVar(value=False) | |
| ttk.Checkbutton(frm, text=self.S["installer_ffmpeg"], variable=self.inst_ffmpeg).grid(row=1, column=0, sticky="w") | |
| ttk.Checkbutton(frm, text=self.S["installer_colmap"], variable=self.inst_colmap).grid(row=2, column=0, sticky="w", pady=(8,0)) | |
| ttk.Label(frm, text=self.S["installer_source"]).grid(row=3, column=0, sticky="e") | |
| self.colmap_url_var = tk.StringVar(value=("https://github.com/colmap/colmap/releases/download/3.12.3/colmap-x64-windows-cuda.zip" if OS_NAME=="Windows" else "https://github.com/colmap/colmap/archive/refs/tags/3.12.3.tar.gz")) | |
| ttk.Entry(frm, textvariable=self.colmap_url_var, width=70).grid(row=3, column=1, sticky="we", padx=(6,0), columnspan=3) | |
| ttk.Checkbutton(frm, text=self.S["installer_glomap"], variable=self.inst_glomap).grid(row=4, column=0, sticky="w", pady=(8,0)) | |
| ttk.Label(frm, text=self.S["installer_source"]).grid(row=5, column=0, sticky="e") | |
| self.glomap_url_var = tk.StringVar(value=("https://github.com/colmap/glomap/releases/download/1.1.0/glomap-x64-windows-cuda.zip" if OS_NAME=="Windows" else "https://github.com/colmap/glomap/archive/refs/tags/1.1.0.tar.gz")) | |
| ttk.Entry(frm, textvariable=self.glomap_url_var, width=70).grid(row=5, column=1, sticky="we", padx=(6,0), columnspan=3) | |
| self.use_cuda_build_var = tk.BooleanVar(value=True) | |
| try: | |
| self.use_cuda_build_var.trace_add("write", lambda *a: self._sync_windows_prebuilt_urls()) | |
| except Exception: | |
| pass | |
| self._sync_windows_prebuilt_urls() | |
| ttk.Checkbutton(frm, text=self.S["installer_use_cuda"], variable=self.use_cuda_build_var).grid(row=6, column=0, sticky="w", pady=(12,0)) | |
| self.try_install_cuda_var = tk.BooleanVar(value=False) | |
| cb_cuda_pkg = ttk.Checkbutton(frm, text=self.S["installer_try_cuda_pkg"], variable=self.try_install_cuda_var) | |
| if OS_NAME == "Windows": cb_cuda_pkg.state(["disabled"]) | |
| cb_cuda_pkg.grid(row=7, column=0, columnspan=3, sticky="w") | |
| frm.grid_columnconfigure(1, weight=1) | |
| btnbar = ttk.Frame(win); btnbar.pack(fill="x", pady=(8,0)) | |
| ttk.Button(btnbar, text=self.S["installer_start"], command=lambda: self._run_installer(win)).pack(side="left") | |
| ttk.Button(btnbar, text=self.S["installer_close"], command=win.destroy).pack(side="right") | |
| note = ttk.Label(win, text=self.S["installer_note"], foreground="#555") | |
| note.pack(fill="x", padx=12, pady=8) | |
| def _log_install(self, msg): | |
| self.log_line("[INSTALL] " + msg) | |
| def _run_installer(self, win): | |
| win.destroy() | |
| threading.Thread(target=self._installer_worker, daemon=True).start() | |
| def _install_extras(self, pm, tool, log_fn): | |
| if pm == "apt": | |
| if tool == "colmap": | |
| extras = ["libsqlite3-dev","libflann-dev","libglew-dev","libqt5svg5-dev","pkg-config", | |
| "libcgal-dev","libblas-dev","liblapack-dev","libmetis-dev","gcc-12","g++-12"] | |
| elif tool == "glomap": | |
| extras = ["libsqlite3-dev","libflann-dev","libglew-dev","libfreeimage-dev","qtbase5-dev","libqt5opengl5-dev","pkg-config", | |
| "libcgal-dev","libblas-dev","liblapack-dev","libmetis-dev"] | |
| else: | |
| extras = [] | |
| elif pm == "dnf": | |
| extras = ["sqlite-devel","flann-devel","glew-devel","pkgconf-pkg-config","CGAL-devel","blas-devel","lapack-devel","metis-devel"] | |
| if tool == "glomap": extras += ["freeimage-devel","qt5-qtbase-devel","qt5-qtopengl-devel"] | |
| elif pm == "zypper": | |
| extras = ["sqlite3-devel","flann-devel","glew-devel","pkgconf-pkg-config","cgal-devel","blas-devel","lapack-devel","metis-devel"] | |
| if tool == "glomap": extras += ["freeimage-devel","libqt5-qtbase-devel","libqt5-qtopengl-devel"] | |
| elif pm == "pacman": | |
| extras = ["sqlite","flann","glew","pkgconf","cgal","blas","lapack","metis"] | |
| if tool == "glomap": extras += ["freeimage","qt5-base"] | |
| elif pm == "apk": | |
| extras = ["sqlite-dev","flann-dev","glew-dev","pkgconf","cgal-dev","blas-dev","lapack-dev","metis-dev"] | |
| if tool == "glomap": extras += ["freeimage-dev","qt5-qtbase-dev"] | |
| else: | |
| extras = [] | |
| if extras: | |
| pkg_install(pm, extras, log_fn) | |
| def _win_copy_tool_bin(self, extracted_root: Path, exe_name: str, target_bin: Path, log_fn): | |
| try: | |
| exe_path = None | |
| for p in extracted_root.rglob(exe_name): | |
| exe_path = p; break | |
| if not exe_path: | |
| log_fn(f"[WIN] {exe_name} nicht im Archiv gefunden."); return False | |
| src_dir = exe_path.parent | |
| target_bin.mkdir(parents=True, exist_ok=True) | |
| # (removed) shutil already imported at module level | |
| # Kopiere EXE + DLLs und einige übliche Unterordner | |
| for item in src_dir.iterdir(): | |
| dst = target_bin / item.name | |
| try: | |
| if item.is_dir(): | |
| if item.name.lower() in ("lib","share","plugins","shaders"): | |
| if dst.exists(): shutil.rmtree(dst) | |
| shutil.copytree(item, dst) | |
| else: | |
| shutil.copy2(item, dst) | |
| except Exception: | |
| pass | |
| log_fn(f"[WIN] Installiert: {exe_name} → {target_bin}") | |
| return True | |
| except Exception as e: | |
| log_fn(f"[WIN] Kopieren fehlgeschlagen für {exe_name}: {e}") | |
| return False | |
| def _copytree_overwrite(self, src_dir, dst_dir): | |
| # (removed) shutil already imported at module level | |
| from pathlib import Path | |
| src_dir = Path(src_dir) | |
| dst_dir = Path(dst_dir) | |
| dst_dir.mkdir(parents=True, exist_ok=True) | |
| shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True) | |
| def _installer_worker(self): | |
| osr = _read_os_release() if OS_NAME == "Linux" else {} | |
| self._log_install(f"OS: {OS_NAME} {osr.get('NAME','')} {osr.get('VERSION','')}") | |
| has_cuda = detect_cuda(); self._log_install(f"CUDA erkannt: {'Ja' if has_cuda else 'Nein'}") | |
| pm = _detect_pkg_manager() if OS_NAME == "Linux" else None | |
| top = Path(self.top_dir_var.get()) | |
| if OS_NAME == "Windows": | |
| self._log_install("Windows: lade vorcompilierte Pakete…") | |
| has_cuda = detect_cuda() | |
| top = Path(self.top_dir_var.get()) | |
| sources_dir = top / DEFAULT_DIRS["sources"] | |
| colmap_src = sources_dir / "colmap" | |
| glomap_src = sources_dir / "glomap" | |
| # URLs | |
| colmap_url_cuda = "https://github.com/colmap/colmap/releases/download/3.12.3/colmap-x64-windows-cuda.zip" | |
| colmap_url_nocuda = "https://github.com/colmap/colmap/releases/download/3.12.3/colmap-x64-windows-nocuda.zip" | |
| glomap_url_cuda = "https://github.com/colmap/glomap/releases/download/1.1.0/glomap-x64-windows-cuda.zip" | |
| glomap_url_nocuda = "https://github.com/colmap/glomap/releases/download/1.1.0/glomap-x64-windows-nocuda.zip" | |
| ffmpeg_url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip" | |
| prefer_cuda = bool(getattr(self, "use_cuda_build_var", None) and self.use_cuda_build_var.get()) | |
| want_cuda = prefer_cuda if getattr(self, "use_cuda_build_var", None) else has_cuda | |
| # ffmpeg | |
| if getattr(self, "inst_ffmpeg", None) and self.inst_ffmpeg.get(): # Ensure Microsoft Visual C++ Redistributable (x64) is present (needed by COLMAP) | |
| try: | |
| if not _win_has_vc_redist(): | |
| self._log_install("[INSTALL] VC++ Redistributable fehlt -> lade und installiere…") | |
| vc_url = "https://aka.ms/vs/17/release/vc_redist.x64.exe" | |
| from urllib.parse import urlsplit | |
| vc_exe = sources_dir / Path(urlsplit(vc_url).path).name | |
| download_file(vc_url, vc_exe, self._log_install) | |
| cmd = [str(vc_exe), "/install", "/quiet", "/norestart"] | |
| try: | |
| subprocess.run(" ".join(f'"{c}"' for c in cmd), shell=True, check=False) | |
| except Exception as e: | |
| self._log_install(f"[INSTALL] Warnung: VC++ Installer konnte nicht gestartet werden: {e}") | |
| if _win_has_vc_redist(): | |
| self._log_install("[INSTALL] VC++ Redistributable installiert.") | |
| else: | |
| self._log_install("[INSTALL] Warnung: VC++ Redistributable scheint noch zu fehlen.") | |
| else: | |
| self._log_install("[INSTALL] VC++ Redistributable bereits vorhanden.") | |
| except Exception as e: | |
| self._log_install(f"[INSTALL] Warnung: VC++ Prüfung/Installation fehlgeschlagen: {e}") | |
| self._log_install("[INSTALL] ffmpeg (Windows prebuilt)") | |
| from urllib.parse import urlsplit | |
| archive = sources_dir / Path(urlsplit(ffmpeg_url).path).name | |
| download_file(ffmpeg_url, archive, self._log_install) | |
| ff_src = extract_archive(archive, sources_dir / "ffmpeg", self._log_install) | |
| if ff_src: | |
| target_bin = top / DEFAULT_DIRS["ffmpeg"] / "bin" | |
| self._win_copy_tool_bin(ff_src, "ffmpeg.exe", target_bin, self._log_install) | |
| # COLMAP | |
| if getattr(self, "inst_colmap", None) and self.inst_colmap.get(): | |
| url = colmap_url_cuda if want_cuda else colmap_url_nocuda | |
| self._log_install(f"[INSTALL] COLMAP Quellen: {url}") | |
| from urllib.parse import urlsplit | |
| archive = sources_dir / Path(urlsplit(url).path).name | |
| download_file(url, archive, self._log_install) | |
| cm_src = extract_archive(archive, colmap_src, self._log_install) | |
| if cm_src: | |
| # Copy FULL extracted content into 01 GLOMAP/colmap | |
| children = [p for p in (cm_src).iterdir()] | |
| colmap_root = cm_src | |
| if len(children) == 1 and children[0].is_dir(): | |
| colmap_root = children[0] | |
| target_root = top / DEFAULT_DIRS["sfm"] / "colmap" | |
| self._copytree_overwrite(colmap_root, target_root) | |
| self._log_install(f"[WIN] COLMAP nach {target_root} kopiert.") | |
| # GLOMAP | |
| if getattr(self, "inst_glomap", None) and self.inst_glomap.get(): | |
| url = glomap_url_cuda if want_cuda else glomap_url_nocuda | |
| self._log_install(f"[INSTALL] GLOMAP Quellen: {url}") | |
| from urllib.parse import urlsplit | |
| archive = sources_dir / Path(urlsplit(url).path).name | |
| download_file(url, archive, self._log_install) | |
| target_root = top / DEFAULT_DIRS["sfm"] / "glomap" | |
| # Extract only 'bin/' from archive into target_root/bin | |
| try: | |
| import zipfile | |
| shutil.rmtree(target_root / "bin", ignore_errors=True) | |
| (target_root / "bin").mkdir(parents=True, exist_ok=True) | |
| with zipfile.ZipFile(archive, 'r') as zf: | |
| for n in zf.namelist(): | |
| if not n or n.endswith('/'): | |
| continue | |
| parts = [p for p in n.split('/') if p] | |
| if not parts: | |
| continue | |
| if parts[0].lower() == 'bin': | |
| rel = '/'.join(parts[1:]) | |
| elif len(parts) >= 2 and parts[1].lower() == 'bin': | |
| rel = '/'.join(parts[2:]) | |
| else: | |
| continue | |
| if not rel: | |
| continue | |
| dest = (target_root / 'bin' / rel) | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| with zf.open(n) as src, open(dest, 'wb') as dst: | |
| shutil.copyfileobj(src, dst) | |
| self._log_install(f"[WIN] GLOMAP bin extrahiert nach {target_root / 'bin'}") | |
| except Exception as e: | |
| self._log_install(f"[INSTALL] Fehler: Konnte GLOMAP bin nicht extrahieren: {e}") | |
| # (wrapper cleanup skipped; we extract bin directly) | |
| # move its contents up so that target_root/bin/... exists. | |
| try: | |
| if target_root.exists(): | |
| _kids = [p for p in target_root.iterdir()] | |
| if len(_kids) == 1 and _kids[0].is_dir() and _kids[0].name.lower() != 'bin': | |
| _wrapper = _kids[0] | |
| for _item in _wrapper.iterdir(): | |
| # (removed) shutil already imported at module level | |
| shutil.move(str(_item), str(target_root / _item.name)) | |
| # (removed) local import of shutil | |
| _sh.rmtree(_wrapper, ignore_errors=True) | |
| self._log_install(f"[WIN] GLOMAP Wrapper '{_wrapper.name}' entfernt – Inhalte nach {target_root} verschoben.") | |
| except Exception as e: | |
| self._log_install(f"[INSTALL] Warnung: Konnte Wrapper nicht bereinigen: {e}") | |
| if True: | |
| self._log_install(f"[WIN] GLOMAP entpackt nach {target_root}") | |
| # ANGLE/Software-OpenGL DLLs sicherstellen (Windows, nach COLMAP-Entpacken) | |
| try: | |
| colmap_dir = base_dir / '01 GLOMAP' / 'colmap' | |
| _win_ensure_angle_dlls(colmap_dir, sources_dir, self._log_install) | |
| except Exception as e: | |
| self._log_install(f"[INSTALL] Warnung: ANGLE-Ergänzung fehlgeschlagen: {e}") | |
| self._log_install("Installer-Durchlauf (Windows) beendet.") | |
| try: self._auto_detect_tools() | |
| except Exception: pass | |
| return | |
| if getattr(self, "try_install_cuda_var", None) and self.try_install_cuda_var.get(): | |
| cuda_pkgs = [] | |
| if pm == "apt": cuda_pkgs = ["nvidia-cuda-toolkit"] | |
| elif pm == "dnf": cuda_pkgs = ["cuda", "cuda-toolkit"] | |
| elif pm == "zypper": cuda_pkgs = ["cuda"] | |
| elif pm == "pacman": cuda_pkgs = ["cuda"] | |
| if cuda_pkgs: self._log_install(f"Versuche CUDA Toolkit zu installieren: {' '.join(cuda_pkgs)}"); pkg_install(pm, cuda_pkgs, self._log_install) | |
| if getattr(self, "inst_ffmpeg", None) and self.inst_ffmpeg.get(): | |
| self._log_install("ffmpeg Installation über Paketmanager…") | |
| code = pkg_install(pm, ["ffmpeg"], self._log_install) | |
| if code == 0: | |
| path = which_first(["ffmpeg"]); self._log_install(f"ffmpeg installiert. Pfad: {path or 'nicht im PATH gefunden'}") | |
| if path: self.ffmpeg_entry.set_text(path) | |
| else: self._log_install("ffmpeg Installation fehlgeschlagen.") | |
| if getattr(self, "inst_colmap", None) and self.inst_colmap.get(): | |
| self._log_install("COLMAP Abhängigkeiten installieren…") | |
| if pm == "apt": | |
| base = ["build-essential","cmake","git","libboost-all-dev","libeigen3-dev","libsuitesparse-dev","libceres-dev", | |
| "libfreeimage-dev","libgoogle-glog-dev","libgflags-dev","qtbase5-dev","libqt5opengl5-dev","ninja-build"] | |
| elif pm == "dnf": | |
| base = ["gcc-c++","cmake","git","boost-devel","eigen3-devel","suitesparse-devel","ceres-solver-devel", | |
| "freeimage-devel","glog-devel","gflags-devel","qt5-qtbase-devel","qt5-qtopengl-devel","ninja-build"] | |
| elif pm == "zypper": | |
| base = ["gcc-c++","cmake","git","libboost-devel","eigen3-devel","suitesparse-devel","ceres-solver-devel", | |
| "freeimage-devel","glog-devel","gflags-devel","libqt5-qtbase-devel","libqt5-qtopengl-devel","ninja"] | |
| elif pm == "pacman": | |
| base = ["base-devel","cmake","git","boost","eigen","suitesparse","ceres-solver","freeimage","glog","gflags","qt5-base","ninja"] | |
| elif pm == "apk": | |
| base = ["build-base","cmake","git","boost-dev","eigen-dev","suitesparse-dev","ceres-dev", | |
| "freeimage-dev","glog-dev","gflags-dev","qt5-qtbase-dev","ninja"] | |
| else: | |
| base = [] | |
| if base: pkg_install(pm, base, self._log_install) | |
| self._install_extras(pm, "colmap", self._log_install) | |
| colmap_url = self.colmap_url_var.get().strip() if hasattr(self, "colmap_url_var") else "" | |
| if not colmap_url: | |
| self._log_install("COLMAP Quelle nicht gesetzt – überspringe Build.") | |
| else: | |
| src_dir = top / DEFAULT_DIRS["sources"] / "colmap" | |
| self._log_install(f"COLMAP Quelle: {colmap_url}") | |
| if src_dir.exists(): shutil.rmtree(src_dir, ignore_errors=True) | |
| src = ensure_source_from_url(colmap_url, src_dir, self._log_install) | |
| if not src or not Path(src).exists(): | |
| self._log_install("Quellen konnten nicht vorbereitet werden – COLMAP.") | |
| else: | |
| build_dir = src_dir / "build" | |
| install_prefix = top / DEFAULT_DIRS["sfm"] / "colmap" | |
| cuda_args = _maybe_cuda_host_flag(pm, self._log_install) if self.use_cuda_build_var.get() else ["-DCUDA_ENABLED=OFF"] | |
| if "-DCUDA_ENABLED=ON" in cuda_args: | |
| self._log_install("COLMAP: versuche CUDA-Build…") | |
| elif "-DCUDA_ENABLED=OFF" in cuda_args: | |
| self._log_install("COLMAP: CUDA deaktiviert (Konfigurationswahl/Heuristik).") | |
| extra_args = [f"-DCMAKE_INSTALL_PREFIX={install_prefix}", "-DBLA_VENDOR=Intel10_64lp"] + cuda_args | |
| code = cmake_configure_ninja(src_dir, build_dir, self._log_install, extra_args=extra_args) | |
| if code != 0 and ("-DCUDA_ENABLED=ON" in cuda_args): | |
| self._log_install("Configure mit CUDA fehlgeschlagen – Fallback ohne -DBLA_VENDOR und ohne CUDA…") | |
| extra_args = [f"-DCMAKE_INSTALL_PREFIX={install_prefix}", "-DCUDA_ENABLED=OFF"] | |
| code = cmake_configure_ninja(src_dir, build_dir, self._log_install, extra_args=extra_args) | |
| if code != 0: | |
| self._log_install("Configure ohne CUDA, zusätzlicher Fallback ohne -DBLA_VENDOR…") | |
| extra_args = [f"-DCMAKE_INSTALL_PREFIX={install_prefix}"] | |
| code = cmake_configure_ninja(src_dir, build_dir, self._log_install, extra_args=extra_args) | |
| if code == 0: | |
| code = ninja_build(build_dir, self._log_install) | |
| if code == 0: | |
| code = ninja_install(build_dir, self._log_install) | |
| bin_expected = install_prefix / "bin" / "colmap" | |
| if code == 0 and ensure_binary_installed(bin_expected, build_dir, "colmap", self._log_install): | |
| self.colmap_entry.set_text(str(bin_expected.resolve())); self._log_install(f"COLMAP installiert nach: {install_prefix}") | |
| elif code == 0: | |
| self._log_install("`ninja install` erledigt, aber COLMAP-Binary nicht gefunden – siehe Log.") | |
| else: | |
| self._log_install("`ninja` Build fehlgeschlagen (COLMAP).") | |
| else: | |
| self._log_install("CMake Configure für COLMAP fehlgeschlagen.") | |
| if getattr(self, "inst_glomap", None) and self.inst_glomap.get(): | |
| self._log_install("GLOMAP Abhängigkeiten installieren…") | |
| if pm == "apt": | |
| base = ["build-essential","cmake","git","libboost-all-dev","libeigen3-dev","libsuitesparse-dev","libceres-dev", | |
| "libgoogle-glog-dev","libgflags-dev","ninja-build"] | |
| elif pm == "dnf": | |
| base = ["gcc-c++","cmake","git","boost-devel","eigen3-devel","suitesparse-devel","ceres-solver-devel", | |
| "glog-devel","gflags-devel","ninja-build"] | |
| elif pm == "zypper": | |
| base = ["gcc-c++","cmake","git","libboost-devel","eigen3-devel","suitesparse-devel","ceres-solver-devel", | |
| "glog-devel","gflags-devel","ninja"] | |
| elif pm == "pacman": | |
| base = ["base-devel","cmake","git","boost","eigen","suitesparse","ceres-solver","glog","gflags","ninja"] | |
| elif pm == "apk": | |
| base = ["build-base","cmake","git","boost-dev","eigen-dev","suitesparse-dev","ceres-dev","glog-dev","gflags-dev","ninja"] | |
| else: | |
| base = [] | |
| if base: pkg_install(pm, base, self._log_install) | |
| self._install_extras(pm, "glomap", self._log_install) | |
| glomap_url = self.glomap_url_var.get().strip() if hasattr(self, "glomap_url_var") else "" | |
| if not glomap_url: | |
| self._log_install("GLOMAP Quelle nicht gesetzt – überspringe Build.") | |
| else: | |
| src_dir = top / DEFAULT_DIRS["sources"] / "glomap" | |
| self._log_install(f"GLOMAP Quelle: {glomap_url}") | |
| if src_dir.exists(): shutil.rmtree(src_dir, ignore_errors=True) | |
| src = ensure_source_from_url(glomap_url, src_dir, self._log_install) | |
| if not src or not Path(src).exists(): | |
| self._log_install("Quellen konnten nicht vorbereitet werden – GLOMAP.") | |
| else: | |
| build_dir = src_dir / "build" | |
| install_prefix = top / DEFAULT_DIRS["sfm"] / "glomap" | |
| extra_args = [f"-DCMAKE_INSTALL_PREFIX={install_prefix}"] | |
| code = cmake_configure_ninja(src_dir, build_dir, self._log_install, extra_args=extra_args) | |
| if code == 0: | |
| code = ninja_build(build_dir, self._log_install) | |
| if code == 0: | |
| code = ninja_install(build_dir, self._log_install) | |
| bin_expected = install_prefix / "bin" / "glomap" | |
| if code == 0 and ensure_binary_installed(bin_expected, build_dir, "glomap", self._log_install): | |
| self.glomap_entry.set_text(str(bin_expected.resolve())); self._log_install(f"GLOMAP installiert nach: {install_prefix}") | |
| elif code == 0: | |
| self._log_install("`ninja install` erledigt, aber GLOMAP-Binary nicht gefunden – siehe Log.") | |
| else: | |
| self._log_install("`ninja` Build fehlgeschlagen (GLOMAP).") | |
| else: | |
| self._log_install("CMake Configure für GLOMAP fehlgeschlagen.") | |
| self._log_install("Installer-Durchlauf beendet."); self._auto_detect_tools() | |
| def test_tools(self): | |
| self.log.delete("1.0", "end"); self.log_line(self.S["tools_test_begin"]) | |
| osr = _read_os_release() if OS_NAME == "Linux" else {}; self.log_line(f"[System] OS erkannt: {OS_NAME} {osr.get('NAME','')} {osr.get('VERSION','')}") | |
| ffmpeg = self.ffmpeg_entry.get_text() | |
| if ffmpeg: | |
| self.log_line(f"[ffmpeg] Pfad: {ffmpeg}") | |
| code, out = run_and_capture([ffmpeg, "-version"]); self.log_line(f"[ffmpeg] exit={code}") | |
| if out: | |
| for line in out.splitlines()[:10]: self.log_line(" " + line) | |
| else: self.log_line("[ffmpeg] nicht gesetzt – bitte auswählen.") | |
| colmap = self.colmap_entry.get_text() | |
| if colmap: | |
| self.log_line(f"[COLMAP] Pfad: {colmap}") | |
| code, out = run_and_capture([colmap, "-h"]); self.log_line(f"[COLMAP] exit={code}") | |
| if out: | |
| for line in out.splitlines()[:10]: self.log_line(" " + line) | |
| code_fx, out_fx = run_and_capture([colmap, "feature_extractor", "-h"]) | |
| code_sm, out_sm = run_and_capture([colmap, "sequential_matcher", "-h"]) | |
| fx_gpu = "--SiftExtraction.use_gpu" in out_fx if out_fx else False | |
| sm_gpu = "--SiftMatching.use_gpu" in out_sm if out_sm else False | |
| self.log_line(f"[COLMAP] GPU-Optionen: feature_extractor use_gpu={'Ja' if fx_gpu else 'Nein'}, sequential_matcher use_gpu={'Ja' if sm_gpu else 'Nein'}") | |
| else: self.log_line("[COLMAP] nicht gesetzt – bitte auswählen.") | |
| glomap = self.glomap_entry.get_text() | |
| if glomap: | |
| self.log_line(f"[GLOMAP] Pfad: {glomap}") | |
| code, out = run_and_capture([glomap, "--help"]); self.log_line(f"[GLOMAP] exit={code}") | |
| if out: | |
| for line in out.splitlines()[:10]: self.log_line(" " + line) | |
| else: self.log_line("[GLOMAP] nicht gesetzt (optional).") | |
| self.log_line(self.S["tools_test_end"]) | |
| # ---- Video UI ---- | |
| def add_videos(self): | |
| files = filedialog.askopenfilenames(title=self.S["dlg_pick_videos"], | |
| filetypes=[("Video","*.mp4 *.MP4 *.mov *.MOV *.avi *.AVI *.mkv *.MKV *.m4v *.M4V *.wmv *.WMV *.mpg *.MPG *.mpeg *.MPEG"), ("All files","*.*")]) | |
| for f in files: self.video_list.insert("end", f) | |
| def remove_selected(self): | |
| for idx in reversed(self.video_list.curselection()): self.video_list.delete(idx) | |
| def clear_videos(self): | |
| self.video_list.delete(0, "end") | |
| # ---- Laufzeit-Anzeige ---- | |
| def _start_elapsed(self): | |
| self._elapsed_start = time.time(); self.elapsed_var.set(f"{self.elapsed_prefix}: 00:00:00") | |
| if self._elapsed_job is None: self._elapsed_job = self.after(500, self._tick_elapsed) | |
| def _tick_elapsed(self): | |
| if self._elapsed_start is None: self._elapsed_job = None; return | |
| secs = int(time.time() - self._elapsed_start); h, m, s = secs // 3600, (secs % 3600) // 60, secs % 60 | |
| self.elapsed_var.set(f"{self.elapsed_prefix}: {h:02d}:{m:02d}:{s:02d}"); self._elapsed_job = self.after(500, self._tick_elapsed) | |
| def _stop_elapsed(self): | |
| if self._elapsed_job is not None: | |
| try: self.after_cancel(self._elapsed_job) | |
| except Exception: pass | |
| self._elapsed_job = None | |
| # ---- Pipeline ---- | |
| def start_run(self): | |
| if getattr(self, "_worker", None) and self._worker.is_alive(): | |
| messagebox.showinfo("Info", self.S["warn_running"]); return | |
| videos = list(self.video_list.get(0, "end")) | |
| if not videos: messagebox.showwarning("Warnung", self.S["warn_no_videos"]); return | |
| ffmpeg = self.ffmpeg_entry.get_text(); colmap = self.colmap_entry.get_text(); glomap = self.glomap_entry.get_text() | |
| if not ffmpeg or not Path(ffmpeg).exists(): | |
| messagebox.showerror("Fehler", self.S["err_ffmpeg"]); return | |
| if not colmap or not Path(colmap).exists(): | |
| messagebox.showerror("Fehler", self.S["err_colmap"]); return | |
| self._stop_flag = False; self.run_btn.config(state="disabled") | |
| self.progress.config(value=0, maximum=len(videos)); self.log.delete("1.0", "end"); self._start_elapsed() | |
| self._worker = threading.Thread(target=self._run_pipeline, args=(videos, ffmpeg, colmap, glomap), daemon=True); self._worker.start() | |
| def log_line(self, text): | |
| self.log.insert("end", text + "\n"); self.log.see("end"); self.update_idletasks() | |
| def _build_scale_filter(self): | |
| mode = self.res_mode.get(); w = self.width_var.get().strip(); h = self.height_var.get().strip() | |
| if mode == "keep": return None | |
| if mode == "w" and w.isdigit(): return f"scale={w}:-2" | |
| if mode == "h" and h.isdigit(): return f"scale=-2:{h}" | |
| if mode == "wh" and w.isdigit() and h.isdigit(): return f"scale={w}:{h}" | |
| return None | |
| def _build_sampling_filters(self): | |
| filters = []; mode = self.fps_mode.get() | |
| if mode == "every": | |
| try: n = max(1, int(self.every_n_var.get().strip())) | |
| except ValueError: n = 2 | |
| if n > 1: filters.append(f"select=not(mod(n\\,{n}))") | |
| elif mode == "target": | |
| try: tfps = float(self.target_fps_var.get().strip()) | |
| except ValueError: tfps = 5.0 | |
| if tfps > 0: filters.append(f"fps={tfps:g}") | |
| return filters if filters else None | |
| def _ffmpeg_extract(self, ffmpeg, video_path, img_dir): | |
| q = self.jpeg_q_var.get().strip() or "2" | |
| scale_f = self._build_scale_filter(); samp_filters = self._build_sampling_filters() | |
| vf_chain = []; | |
| if samp_filters: vf_chain.extend(samp_filters) | |
| if scale_f: vf_chain.append(scale_f) | |
| vf_arg = ",".join(vf_chain) if vf_chain else None | |
| cmd = [ffmpeg, "-hide_banner", "-loglevel", "info", "-nostdin", "-i", video_path, "-qscale:v", q] | |
| if vf_arg: cmd.extend(["-vf", vf_arg, "-vsync", "vfr"]) | |
| out_pattern = str(Path(img_dir) / "frame_%06d.jpg"); cmd.append(out_pattern) | |
| self.log_line(" ".join(shlex.quote(c) for c in cmd)) | |
| return run_cmd(cmd, log_fn=self.log_line) | |
| def _colmap_feature_extractor(self, colmap, db_path, img_dir, max_img_size, use_gpu: bool): | |
| cmd = [colmap, "feature_extractor", "--database_path", db_path, "--image_path", img_dir, | |
| "--ImageReader.single_camera", "1", "--SiftExtraction.max_image_size", str(max_img_size)] | |
| if use_gpu: | |
| cmd += ["--SiftExtraction.use_gpu", "1"] | |
| self.log_line(" ".join(shlex.quote(c) for c in cmd)); return run_cmd(cmd, log_fn=self.log_line) | |
| def _colmap_sequential_matcher(self, colmap, db_path, overlap, use_gpu: bool): | |
| cmd = [colmap, "sequential_matcher", "--database_path", db_path, "--SequentialMatching.overlap", str(overlap), | |
| "--SiftMatching.use_gpu", "1" if use_gpu else "0"] | |
| self.log_line(" ".join(shlex.quote(c) for c in cmd)); return run_cmd(cmd, log_fn=self.log_line) | |
| def _glomap_mapper(self, glomap, db_path, img_dir, sparse_dir): | |
| cmd = [glomap, "mapper", "--database_path", db_path, "--image_path", img_dir, "--output_path", sparse_dir] | |
| self.log_line(" ".join(shlex.quote(c) for c in cmd)); return run_cmd(cmd, log_fn=self.log_line) | |
| def _colmap_mapper(self, colmap, db_path, img_dir, sparse_dir): | |
| cmd = [colmap, "mapper", "--database_path", db_path, "--image_path", img_dir, "--output_path", sparse_dir] | |
| self.log_line(" ".join(shlex.quote(c) for c in cmd)); return run_cmd(cmd, log_fn=self.log_line) | |
| def _colmap_model_converter(self, colmap, in_path, out_path): | |
| cmd = [colmap, "model_converter", "--input_path", in_path, "--output_path", out_path, "--output_type", "TXT"] | |
| self.log_line(" ".join(shlex.quote(c) for c in cmd)); return run_cmd(cmd, log_fn=self.log_line) | |
| def _run_pipeline(self, videos, ffmpeg, colmap, glomap): | |
| try: | |
| scenes_dir = Path(self.scenes_dir_var.get()); scenes_dir.mkdir(parents=True, exist_ok=True) | |
| overlap = int(self.seq_overlap_var.get().strip() or "15"); max_img = int(self.sift_max_img_var.get().strip() or "4096") | |
| use_gpu = bool(self.use_gpu_var.get()) | |
| for i, video in enumerate(videos, start=1): | |
| if self._stop_flag: break | |
| vpath = Path(video); base = vpath.stem | |
| self.log_line(f"\n=== Verarbeite ({i}/{len(videos)}): {base} ===") | |
| scene_dir = scenes_dir / base; img_dir = scene_dir / "images"; sparse_dir = scene_dir / "sparse"; db_path = scene_dir / "database.db" | |
| img_dir.mkdir(parents=True, exist_ok=True); sparse_dir.mkdir(parents=True, exist_ok=True) | |
| self.log_line(f"[1/4] {self.S['run_extract']}") | |
| code = self._ffmpeg_extract(ffmpeg, str(vpath), str(img_dir)) | |
| if code != 0: | |
| self.log_line(f"[ERROR] ffmpeg fehlgeschlagen für {base}. Überspringe."); self._advance_progress(i, len(videos)); continue | |
| if not any(p.suffix.lower() == ".jpg" for p in img_dir.glob("*.jpg")): | |
| self.log_line(f"[ERROR] Keine Frames extrahiert für {base}. Überspringe."); self._advance_progress(i, len(videos)); continue | |
| self.log_line(f"[2/4] {self.S['run_feat']}") | |
| code = self._colmap_feature_extractor(colmap, str(db_path), str(img_dir), max_img, use_gpu) | |
| if code != 0: | |
| self.log_line(f"[ERROR] feature_extractor fehlgeschlagen für {base}. Überspringe."); self._advance_progress(i, len(videos)); continue | |
| self.log_line(f"[3/4] {self.S['run_match']}") | |
| code = self._colmap_sequential_matcher(colmap, str(db_path), overlap, use_gpu) | |
| if code != 0: | |
| self.log_line(f"[ERROR] sequential_matcher fehlgeschlagen für {base}. Überspringe."); self._advance_progress(i, len(videos)); continue | |
| self.log_line(f"[4/4] {self.S['run_mapper']}") | |
| use_glomap = bool(glomap) and Path(glomap).exists() | |
| code = self._glomap_mapper(glomap, str(db_path), str(img_dir), str(sparse_dir)) if use_glomap \ | |
| else self._colmap_mapper(colmap, str(db_path), str(img_dir), str(sparse_dir)) | |
| if code != 0: | |
| self.log_line(f"[ERROR] mapper fehlgeschlagen für {base}. Überspringe."); self._advance_progress(i, len(videos)); continue | |
| sub0 = sparse_dir / "0" | |
| if sub0.exists(): | |
| self._colmap_model_converter(colmap, str(sub0), str(sub0)); self._colmap_model_converter(colmap, str(sub0), str(sparse_dir)) | |
| self.log_line(f"✓ Fertig: {base} ({i}/{len(videos)})"); self._advance_progress(i, len(videos)) | |
| self.log_line("\\n" + self.S["done_all"]) | |
| except Exception as e: | |
| self.log_line(f"[FATAL] {e}") | |
| finally: | |
| try: self.after(0, self._stop_elapsed); self.after(0, lambda: self.run_btn.config(state="normal")) | |
| except Exception: self.run_btn.config(state="normal") | |
| def _advance_progress(self, i, total): | |
| self.progress.config(maximum=total, value=i); self.update_idletasks() | |
| def _open_about_dialog(self): | |
| url = "https://gist.github.com/polyfjord/fc22f22770cd4dd365bb90db67a4f2dc" | |
| win = tk.Toplevel(self); win.title(self.S["about_title"]); win.resizable(False, False) | |
| frm = ttk.Frame(win); frm.pack(fill="both", expand=True, padx=12, pady=12) | |
| txt = tk.Text(frm, width=48, height=3, wrap="word", borderwidth=0) | |
| txt.pack(fill="both", expand=True) | |
| txt.insert("end", self.S["about_text"]) | |
| start_idx = txt.index("end-1c") | |
| txt.insert("end", self.S["about_link"]) | |
| end_idx = txt.index("end-1c") | |
| txt.tag_add("link", start_idx, end_idx); txt.tag_config("link", foreground="#0b61a4", underline=1) | |
| txt.tag_bind("link", "<Button-1>", lambda e: webbrowser.open_new_tab(url)); txt.configure(state="disabled") | |
| btnbar = ttk.Frame(frm); btnbar.pack(fill="x", pady=(8,0)) | |
| ttk.Button(btnbar, text=self.S.get("installer_close", "Schließen"), command=win.destroy).pack(side="right") | |
| try: | |
| self.update_idletasks(); x = self.winfo_x() + self.winfo_width() - win.winfo_reqwidth() - 40; y = self.winfo_y() + 60; win.geometry(f"+{x}+{y}") | |
| except Exception: | |
| pass | |
| if __name__ == "__main__": | |
| app = AutoTrackerGUI(); app.mainloop() |
Author
Hi, it is only one python script, which can run under Windows or Linux. I created a complete repository on github, where you can find the actual python script and a short readme on how tu use it. Just have a look at it: https://github.com/duke2421/AutotrackPythonScript.git
I am working on a YT Tutorial for using the script and what it can do under Windows and Linux, but this will take some more time I will add a link to it in the readme, as soon as the video is finished and published on youtube.
If something is nt working, please fill in an issue on that repo, thanks.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Looks amazing! Is it ready to go?
I can see it looks like two python files within this one file (judging by the leading shebang lines and comments) - do they need separating? Is the first, shorter script only for windows use, because that's what it seems to me at a skim-reading.