···4444- OpenCV (`cv2`)
4545- NumPy
4646- requests
4747-- Ultralytics YOLO / PyTorch for object detection
4747+4848+Object detection is optional. WildCam installs Ultralytics YOLO / PyTorch on
4949+first use into the user's app-data directory instead of bundling it with the
5050+main app.
48514952## Run
5053···5356source .venv/bin/activate
5457pip install -r requirements.txt
5558python3 main.py
5959+```
6060+6161+To preinstall the optional object-detection stack in a development environment:
6262+6363+```bash
6464+pip install -r requirements-detection.txt
5665```
57665867`./start_wildcam.sh` activates `.venv`/`venv` automatically. If no virtual
···245254`dog`, `cat`, `bird`, `horse`, `sheep`, and `cow`. Wild animals such as deer,
246255foxes, or boars usually need a custom fine-tuned model for reliable alerts.
247256248248-Release builds include the Python detection libraries. YOLO model weights are
249249-resolved by Ultralytics from the configured model name/path, so the first run
250250-must either be able to download the model or point `detection.model` to a local
251251-weights file.
257257+Release builds do not bundle the Python detection libraries. When object
258258+detection is enabled or **Install/test runtime/model** is clicked for the first time,
259259+WildCam installs the optional runtime into the user's app-data directory:
260260+261261+```text
262262+Linux: ~/.local/share/wildcam/detection-runtime/py<version>
263263+macOS: ~/Library/Application Support/WildCam/detection-runtime/py<version>
264264+Windows: %LOCALAPPDATA%\WildCam\detection-runtime\py<version>
265265+```
266266+267267+In standalone builds this requires a matching external Python with `pip` and
268268+internet access. Linux installs CPU-only PyTorch wheels by default to avoid
269269+bundling CUDA/NVIDIA libraries. YOLO model weights are resolved by Ultralytics
270270+from the configured model name/path, so the first detection run must either be
271271+able to download the model or point `detection.model` to a local weights file.
252272253253-The **Download/test model** button stores known YOLO weights in:
273273+The **Install/test runtime/model** button stores known YOLO weights in:
254274255275```text
256276<recording_path>/models
···405425406426- The packaged app bundles WildCam and helper files such as `docker-compose.yml` and `reolinkproxy_manager.py`.
407427- The actual ReolinkProxy runtime is **not** bundled into the app archive.
428428+- The optional object-detection runtime is **not** bundled into the app archive;
429429+ it is installed on first use.
408430- If you use only normal RTSP cameras, the standalone app is enough.
409431- If you use Reolink WLAN / battery cameras, you must additionally run ReolinkProxy externally.
410432
+154
detection.py
···22import time
33import math
44from dataclasses import dataclass
55+import importlib
66+import os
57from pathlib import Path
68import shutil
99+import subprocess
1010+import sys
711from typing import Any
812913import cv2
···6569 pass
667067717272+class DetectionDependencyError(RuntimeError):
7373+ pass
7474+7575+7676+_DEPENDENCY_LOCK = threading.Lock()
7777+_DEPENDENCY_PATH_ADDED = False
7878+7979+8080+ULTRALYTICS_PACKAGE = "ultralytics>=8.3.0"
8181+8282+8383+DETECTION_SUPPORT_PACKAGES = [
8484+ "pillow>=10.0.0",
8585+ "pyyaml>=6.0.0",
8686+ "scipy>=1.10.0",
8787+ "psutil>=5.9.0",
8888+ "matplotlib>=3.7.0",
8989+ "polars>=0.20.0",
9090+ "ultralytics-thop>=2.0.0",
9191+]
9292+9393+9494+def _app_data_dir() -> Path:
9595+ if sys.platform == "win32":
9696+ base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
9797+ return Path(base).expanduser() / "WildCam" if base else Path.home() / "AppData" / "Local" / "WildCam"
9898+ if sys.platform == "darwin":
9999+ return Path.home() / "Library" / "Application Support" / "WildCam"
100100+ base = os.environ.get("XDG_DATA_HOME")
101101+ return Path(base).expanduser() / "wildcam" if base else Path.home() / ".local" / "share" / "wildcam"
102102+103103+104104+def detection_runtime_dir() -> Path:
105105+ version = f"py{sys.version_info.major}.{sys.version_info.minor}"
106106+ return _app_data_dir() / "detection-runtime" / version
107107+108108+109109+def _add_detection_runtime_to_path() -> None:
110110+ global _DEPENDENCY_PATH_ADDED
111111+ runtime_dir = detection_runtime_dir()
112112+ if _DEPENDENCY_PATH_ADDED and str(runtime_dir) in sys.path:
113113+ return
114114+ if runtime_dir.exists():
115115+ sys.path.insert(0, str(runtime_dir))
116116+ _DEPENDENCY_PATH_ADDED = True
117117+118118+119119+def _has_detection_dependencies() -> bool:
120120+ _add_detection_runtime_to_path()
121121+ return all(
122122+ importlib.util.find_spec(module_name) is not None
123123+ for module_name in ("ultralytics", "torch", "torchvision")
124124+ )
125125+126126+127127+def _python_version(command: list[str]) -> tuple[int, int] | None:
128128+ try:
129129+ result = subprocess.run(
130130+ [*command, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
131131+ check=True,
132132+ capture_output=True,
133133+ text=True,
134134+ timeout=10,
135135+ )
136136+ except Exception:
137137+ return None
138138+ try:
139139+ major, minor = result.stdout.strip().split(".", 1)
140140+ return int(major), int(minor)
141141+ except Exception:
142142+ return None
143143+144144+145145+def _find_install_python() -> list[str]:
146146+ if not getattr(sys, "frozen", False):
147147+ return [sys.executable]
148148+149149+ wanted = (sys.version_info.major, sys.version_info.minor)
150150+ candidates = [
151151+ [f"python{wanted[0]}.{wanted[1]}"],
152152+ ["python3"],
153153+ ["python"],
154154+ ]
155155+ if sys.platform == "win32":
156156+ candidates = [["py", f"-{wanted[0]}.{wanted[1]}"], ["py", "-3"], *candidates]
157157+158158+ for command in candidates:
159159+ version = _python_version(command)
160160+ if version == wanted:
161161+ return command
162162+163163+ needed = f"{wanted[0]}.{wanted[1]}"
164164+ raise DetectionDependencyError(
165165+ "Objekterkennung benötigt Python "
166166+ f"{needed} mit pip, um die optionalen ML-Pakete beim ersten Start zu installieren."
167167+ )
168168+169169+170170+def _run_pip(command: list[str]) -> None:
171171+ try:
172172+ subprocess.run(command, check=True)
173173+ except subprocess.CalledProcessError as exc:
174174+ raise DetectionDependencyError(
175175+ "Installation der optionalen Objekterkennungs-Pakete fehlgeschlagen. "
176176+ "Bitte Internetverbindung und pip prüfen."
177177+ ) from exc
178178+179179+180180+def ensure_detection_dependencies() -> None:
181181+ if _has_detection_dependencies():
182182+ return
183183+184184+ with _DEPENDENCY_LOCK:
185185+ if _has_detection_dependencies():
186186+ return
187187+188188+ runtime_dir = detection_runtime_dir()
189189+ runtime_dir.mkdir(parents=True, exist_ok=True)
190190+ python = _find_install_python()
191191+ base_cmd = [*python, "-m", "pip", "install", "--upgrade", "--target", str(runtime_dir)]
192192+193193+ if sys.platform.startswith("linux"):
194194+ _run_pip(
195195+ [
196196+ *base_cmd,
197197+ "--index-url",
198198+ "https://download.pytorch.org/whl/cpu",
199199+ "torch>=2.2.0",
200200+ "torchvision>=0.17.0",
201201+ ]
202202+ )
203203+ else:
204204+ _run_pip([*base_cmd, "torch>=2.2.0", "torchvision>=0.17.0"])
205205+206206+ _run_pip([*base_cmd, *DETECTION_SUPPORT_PACKAGES])
207207+ _run_pip([*base_cmd, "--no-deps", ULTRALYTICS_PACKAGE])
208208+ importlib.invalidate_caches()
209209+ _add_detection_runtime_to_path()
210210+211211+ if not _has_detection_dependencies():
212212+ raise DetectionDependencyError(
213213+ f"Optionale Objekterkennungs-Pakete wurden installiert, aber nicht vollständig gefunden: {runtime_dir}"
214214+ )
215215+216216+68217def default_model_dir(recording_path: str) -> Path:
69218 return Path(recording_path).expanduser() / "models"
70219···952449624597246def prepare_model_path(model_name: str, model_dir: str | Path) -> Path:
247247+ ensure_detection_dependencies()
248248+98249 raw = str(model_name or "").strip() or DEFAULT_DETECTION_CONFIG["model"]
99250 expanded = Path(raw).expanduser()
100251 if expanded.exists():
···211362 self.msleep(500)
212363213364 def _load_model(self):
365365+ self.status.emit("Objekterkennung-Runtime wird geprüft/installiert...")
366366+ ensure_detection_dependencies()
367367+214368 try:
215369 from ultralytics import YOLO
216370 except Exception as exc:
+4-4
i18n.py
···4444 "btn.record_all": "Alle aufnehmen",
4545 "btn.record_all_stop": "Alle stoppen",
4646 "btn.email_test": "Testmail versenden",
4747- "btn.model_test": "Modell herunterladen/testen",
4747+ "btn.model_test": "Runtime/Modell installieren/testen",
4848 "label.camera_count": "Kameras: {total} | Aktiv: {active}",
4949 "big.select_camera": "Kamera auswählen…",
5050 "status.ready": "Bereit - CPU-optimiert für parallele Streams",
···8181 "status.email_test_sending": "Testmail wird versendet...",
8282 "status.email_test_sent": "Testmail erfolgreich versendet",
8383 "status.email_test_failed": "Testmail fehlgeschlagen: {error}",
8484- "status.model_test_running": "Modell wird geprüft/heruntergeladen...",
8484+ "status.model_test_running": "Runtime und Modell werden geprüft/installiert...",
8585 "status.model_test_ok": "Modell bereit: {path}",
8686 "status.model_test_failed": "Modelltest fehlgeschlagen: {error}",
8787 "status.model_retry_scheduled": "Modell-Laden fehlgeschlagen; neuer Versuch in {seconds}s",
···204204 "btn.record_all": "● Record all",
205205 "btn.record_all_stop": "■ Stop all",
206206 "btn.email_test": "Send test mail",
207207- "btn.model_test": "Download/test model",
207207+ "btn.model_test": "Install/test runtime/model",
208208 "label.camera_count": "Cameras: {total} | Active: {active}",
209209 "big.select_camera": "Select a camera…",
210210 "status.ready": "Ready - CPU-optimized for parallel streams",
···241241 "status.email_test_sending": "Sending test mail...",
242242 "status.email_test_sent": "Test mail sent successfully",
243243 "status.email_test_failed": "Test mail failed: {error}",
244244- "status.model_test_running": "Checking/downloading model...",
244244+ "status.model_test_running": "Checking/installing runtime and model...",
245245 "status.model_test_ok": "Model ready: {path}",
246246 "status.model_test_failed": "Model test failed: {error}",
247247 "status.model_retry_scheduled": "Model load failed; retrying in {seconds}s",
+5
requirements-detection.txt
···11+# Optional object detection runtime.
22+# Installed on first use into the user's WildCam app-data directory.
33+ultralytics>=8.3.0
44+torch>=2.2.0
55+torchvision>=0.17.0