About Multi-camera viewer optimized for RTSP streams
0

Configure Feed

Select the types of activity you want to include in your feed.

linx build refinement

+203 -24
+28 -6
README.md
··· 44 44 - OpenCV (`cv2`) 45 45 - NumPy 46 46 - requests 47 - - Ultralytics YOLO / PyTorch for object detection 47 + 48 + Object detection is optional. WildCam installs Ultralytics YOLO / PyTorch on 49 + first use into the user's app-data directory instead of bundling it with the 50 + main app. 48 51 49 52 ## Run 50 53 ··· 53 56 source .venv/bin/activate 54 57 pip install -r requirements.txt 55 58 python3 main.py 59 + ``` 60 + 61 + To preinstall the optional object-detection stack in a development environment: 62 + 63 + ```bash 64 + pip install -r requirements-detection.txt 56 65 ``` 57 66 58 67 `./start_wildcam.sh` activates `.venv`/`venv` automatically. If no virtual ··· 245 254 `dog`, `cat`, `bird`, `horse`, `sheep`, and `cow`. Wild animals such as deer, 246 255 foxes, or boars usually need a custom fine-tuned model for reliable alerts. 247 256 248 - Release builds include the Python detection libraries. YOLO model weights are 249 - resolved by Ultralytics from the configured model name/path, so the first run 250 - must either be able to download the model or point `detection.model` to a local 251 - weights file. 257 + Release builds do not bundle the Python detection libraries. When object 258 + detection is enabled or **Install/test runtime/model** is clicked for the first time, 259 + WildCam installs the optional runtime into the user's app-data directory: 260 + 261 + ```text 262 + Linux: ~/.local/share/wildcam/detection-runtime/py<version> 263 + macOS: ~/Library/Application Support/WildCam/detection-runtime/py<version> 264 + Windows: %LOCALAPPDATA%\WildCam\detection-runtime\py<version> 265 + ``` 266 + 267 + In standalone builds this requires a matching external Python with `pip` and 268 + internet access. Linux installs CPU-only PyTorch wheels by default to avoid 269 + bundling CUDA/NVIDIA libraries. YOLO model weights are resolved by Ultralytics 270 + from the configured model name/path, so the first detection run must either be 271 + able to download the model or point `detection.model` to a local weights file. 252 272 253 - The **Download/test model** button stores known YOLO weights in: 273 + The **Install/test runtime/model** button stores known YOLO weights in: 254 274 255 275 ```text 256 276 <recording_path>/models ··· 405 425 406 426 - The packaged app bundles WildCam and helper files such as `docker-compose.yml` and `reolinkproxy_manager.py`. 407 427 - The actual ReolinkProxy runtime is **not** bundled into the app archive. 428 + - The optional object-detection runtime is **not** bundled into the app archive; 429 + it is installed on first use. 408 430 - If you use only normal RTSP cameras, the standalone app is enough. 409 431 - If you use Reolink WLAN / battery cameras, you must additionally run ReolinkProxy externally. 410 432
+154
detection.py
··· 2 2 import time 3 3 import math 4 4 from dataclasses import dataclass 5 + import importlib 6 + import os 5 7 from pathlib import Path 6 8 import shutil 9 + import subprocess 10 + import sys 7 11 from typing import Any 8 12 9 13 import cv2 ··· 65 69 pass 66 70 67 71 72 + class DetectionDependencyError(RuntimeError): 73 + pass 74 + 75 + 76 + _DEPENDENCY_LOCK = threading.Lock() 77 + _DEPENDENCY_PATH_ADDED = False 78 + 79 + 80 + ULTRALYTICS_PACKAGE = "ultralytics>=8.3.0" 81 + 82 + 83 + DETECTION_SUPPORT_PACKAGES = [ 84 + "pillow>=10.0.0", 85 + "pyyaml>=6.0.0", 86 + "scipy>=1.10.0", 87 + "psutil>=5.9.0", 88 + "matplotlib>=3.7.0", 89 + "polars>=0.20.0", 90 + "ultralytics-thop>=2.0.0", 91 + ] 92 + 93 + 94 + def _app_data_dir() -> Path: 95 + if sys.platform == "win32": 96 + base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") 97 + return Path(base).expanduser() / "WildCam" if base else Path.home() / "AppData" / "Local" / "WildCam" 98 + if sys.platform == "darwin": 99 + return Path.home() / "Library" / "Application Support" / "WildCam" 100 + base = os.environ.get("XDG_DATA_HOME") 101 + return Path(base).expanduser() / "wildcam" if base else Path.home() / ".local" / "share" / "wildcam" 102 + 103 + 104 + def detection_runtime_dir() -> Path: 105 + version = f"py{sys.version_info.major}.{sys.version_info.minor}" 106 + return _app_data_dir() / "detection-runtime" / version 107 + 108 + 109 + def _add_detection_runtime_to_path() -> None: 110 + global _DEPENDENCY_PATH_ADDED 111 + runtime_dir = detection_runtime_dir() 112 + if _DEPENDENCY_PATH_ADDED and str(runtime_dir) in sys.path: 113 + return 114 + if runtime_dir.exists(): 115 + sys.path.insert(0, str(runtime_dir)) 116 + _DEPENDENCY_PATH_ADDED = True 117 + 118 + 119 + def _has_detection_dependencies() -> bool: 120 + _add_detection_runtime_to_path() 121 + return all( 122 + importlib.util.find_spec(module_name) is not None 123 + for module_name in ("ultralytics", "torch", "torchvision") 124 + ) 125 + 126 + 127 + def _python_version(command: list[str]) -> tuple[int, int] | None: 128 + try: 129 + result = subprocess.run( 130 + [*command, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"], 131 + check=True, 132 + capture_output=True, 133 + text=True, 134 + timeout=10, 135 + ) 136 + except Exception: 137 + return None 138 + try: 139 + major, minor = result.stdout.strip().split(".", 1) 140 + return int(major), int(minor) 141 + except Exception: 142 + return None 143 + 144 + 145 + def _find_install_python() -> list[str]: 146 + if not getattr(sys, "frozen", False): 147 + return [sys.executable] 148 + 149 + wanted = (sys.version_info.major, sys.version_info.minor) 150 + candidates = [ 151 + [f"python{wanted[0]}.{wanted[1]}"], 152 + ["python3"], 153 + ["python"], 154 + ] 155 + if sys.platform == "win32": 156 + candidates = [["py", f"-{wanted[0]}.{wanted[1]}"], ["py", "-3"], *candidates] 157 + 158 + for command in candidates: 159 + version = _python_version(command) 160 + if version == wanted: 161 + return command 162 + 163 + needed = f"{wanted[0]}.{wanted[1]}" 164 + raise DetectionDependencyError( 165 + "Objekterkennung benötigt Python " 166 + f"{needed} mit pip, um die optionalen ML-Pakete beim ersten Start zu installieren." 167 + ) 168 + 169 + 170 + def _run_pip(command: list[str]) -> None: 171 + try: 172 + subprocess.run(command, check=True) 173 + except subprocess.CalledProcessError as exc: 174 + raise DetectionDependencyError( 175 + "Installation der optionalen Objekterkennungs-Pakete fehlgeschlagen. " 176 + "Bitte Internetverbindung und pip prüfen." 177 + ) from exc 178 + 179 + 180 + def ensure_detection_dependencies() -> None: 181 + if _has_detection_dependencies(): 182 + return 183 + 184 + with _DEPENDENCY_LOCK: 185 + if _has_detection_dependencies(): 186 + return 187 + 188 + runtime_dir = detection_runtime_dir() 189 + runtime_dir.mkdir(parents=True, exist_ok=True) 190 + python = _find_install_python() 191 + base_cmd = [*python, "-m", "pip", "install", "--upgrade", "--target", str(runtime_dir)] 192 + 193 + if sys.platform.startswith("linux"): 194 + _run_pip( 195 + [ 196 + *base_cmd, 197 + "--index-url", 198 + "https://download.pytorch.org/whl/cpu", 199 + "torch>=2.2.0", 200 + "torchvision>=0.17.0", 201 + ] 202 + ) 203 + else: 204 + _run_pip([*base_cmd, "torch>=2.2.0", "torchvision>=0.17.0"]) 205 + 206 + _run_pip([*base_cmd, *DETECTION_SUPPORT_PACKAGES]) 207 + _run_pip([*base_cmd, "--no-deps", ULTRALYTICS_PACKAGE]) 208 + importlib.invalidate_caches() 209 + _add_detection_runtime_to_path() 210 + 211 + if not _has_detection_dependencies(): 212 + raise DetectionDependencyError( 213 + f"Optionale Objekterkennungs-Pakete wurden installiert, aber nicht vollständig gefunden: {runtime_dir}" 214 + ) 215 + 216 + 68 217 def default_model_dir(recording_path: str) -> Path: 69 218 return Path(recording_path).expanduser() / "models" 70 219 ··· 95 244 96 245 97 246 def prepare_model_path(model_name: str, model_dir: str | Path) -> Path: 247 + ensure_detection_dependencies() 248 + 98 249 raw = str(model_name or "").strip() or DEFAULT_DETECTION_CONFIG["model"] 99 250 expanded = Path(raw).expanduser() 100 251 if expanded.exists(): ··· 211 362 self.msleep(500) 212 363 213 364 def _load_model(self): 365 + self.status.emit("Objekterkennung-Runtime wird geprüft/installiert...") 366 + ensure_detection_dependencies() 367 + 214 368 try: 215 369 from ultralytics import YOLO 216 370 except Exception as exc:
+4 -4
i18n.py
··· 44 44 "btn.record_all": "Alle aufnehmen", 45 45 "btn.record_all_stop": "Alle stoppen", 46 46 "btn.email_test": "Testmail versenden", 47 - "btn.model_test": "Modell herunterladen/testen", 47 + "btn.model_test": "Runtime/Modell installieren/testen", 48 48 "label.camera_count": "Kameras: {total} | Aktiv: {active}", 49 49 "big.select_camera": "Kamera auswählen…", 50 50 "status.ready": "Bereit - CPU-optimiert für parallele Streams", ··· 81 81 "status.email_test_sending": "Testmail wird versendet...", 82 82 "status.email_test_sent": "Testmail erfolgreich versendet", 83 83 "status.email_test_failed": "Testmail fehlgeschlagen: {error}", 84 - "status.model_test_running": "Modell wird geprüft/heruntergeladen...", 84 + "status.model_test_running": "Runtime und Modell werden geprüft/installiert...", 85 85 "status.model_test_ok": "Modell bereit: {path}", 86 86 "status.model_test_failed": "Modelltest fehlgeschlagen: {error}", 87 87 "status.model_retry_scheduled": "Modell-Laden fehlgeschlagen; neuer Versuch in {seconds}s", ··· 204 204 "btn.record_all": "● Record all", 205 205 "btn.record_all_stop": "■ Stop all", 206 206 "btn.email_test": "Send test mail", 207 - "btn.model_test": "Download/test model", 207 + "btn.model_test": "Install/test runtime/model", 208 208 "label.camera_count": "Cameras: {total} | Active: {active}", 209 209 "big.select_camera": "Select a camera…", 210 210 "status.ready": "Ready - CPU-optimized for parallel streams", ··· 241 241 "status.email_test_sending": "Sending test mail...", 242 242 "status.email_test_sent": "Test mail sent successfully", 243 243 "status.email_test_failed": "Test mail failed: {error}", 244 - "status.model_test_running": "Checking/downloading model...", 244 + "status.model_test_running": "Checking/installing runtime and model...", 245 245 "status.model_test_ok": "Model ready: {path}", 246 246 "status.model_test_failed": "Model test failed: {error}", 247 247 "status.model_retry_scheduled": "Model load failed; retrying in {seconds}s",
+5
requirements-detection.txt
··· 1 + # Optional object detection runtime. 2 + # Installed on first use into the user's WildCam app-data directory. 3 + ultralytics>=8.3.0 4 + torch>=2.2.0 5 + torchvision>=0.17.0
-5
requirements.txt
··· 10 10 # HTTP Requests für Kamera-API 11 11 requests>=2.31.0 12 12 13 - # Objekterkennung (AGPL-3.0 / Ultralytics-Lizenz beachten) 14 - ultralytics>=8.3.0 15 - torch>=2.2.0 16 - torchvision>=0.17.0 17 - 18 13 # Optional: Bessere Performance mit opencv-contrib 19 14 # opencv-contrib-python>=4.8.0
+4 -3
scripts/build_linux.sh
··· 64 64 --collect-all "PyQt6" \ 65 65 --collect-all "cv2" \ 66 66 --collect-all "numpy" \ 67 - --collect-all "ultralytics" \ 68 - --collect-all "torch" \ 69 - --collect-all "torchvision" \ 67 + --exclude-module "ultralytics" \ 68 + --exclude-module "torch" \ 69 + --exclude-module "torchvision" \ 70 + --exclude-module "nvidia" \ 70 71 --hidden-import "detection" \ 71 72 --hidden-import "notifications" \ 72 73 "$ENTRY_POINT"
+4 -3
scripts/build_macos.sh
··· 63 63 --collect-all "PyQt6" \ 64 64 --collect-all "cv2" \ 65 65 --collect-all "numpy" \ 66 - --collect-all "ultralytics" \ 67 - --collect-all "torch" \ 68 - --collect-all "torchvision" \ 66 + --exclude-module "ultralytics" \ 67 + --exclude-module "torch" \ 68 + --exclude-module "torchvision" \ 69 + --exclude-module "nvidia" \ 69 70 --hidden-import "detection" \ 70 71 --hidden-import "notifications" \ 71 72 "$ENTRY_POINT"
+4 -3
scripts/build_windows.ps1
··· 46 46 --collect-all "PyQt6" ` 47 47 --collect-all "cv2" ` 48 48 --collect-all "numpy" ` 49 - --collect-all "ultralytics" ` 50 - --collect-all "torch" ` 51 - --collect-all "torchvision" ` 49 + --exclude-module "ultralytics" ` 50 + --exclude-module "torch" ` 51 + --exclude-module "torchvision" ` 52 + --exclude-module "nvidia" ` 52 53 --hidden-import "detection" ` 53 54 --hidden-import "notifications" ` 54 55 $EntryPoint