1
0

webcam copy

This commit is contained in:
Aiden
2026-05-27 12:17:12 +10:00
parent c0304c575c
commit 21f0e455ee
6 changed files with 410 additions and 9 deletions

168
h8536/camera_snapshots.py Normal file
View File

@@ -0,0 +1,168 @@
from __future__ import annotations
import re
import heapq
import threading
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
_SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9_.-]+")
class CameraSnapshots:
"""Small optional OpenCV webcam wrapper for bench-test snapshots."""
def __init__(
self,
*,
camera_index: int,
output_dir: Path,
warmup_seconds: float = 0.5,
width: int | None = None,
height: int | None = None,
) -> None:
self.camera_index = camera_index
self.output_dir = output_dir
self.warmup_seconds = max(0.0, warmup_seconds)
self.width = width
self.height = height
self.cv2: Any | None = None
self.device: Any | None = None
self._condition = threading.Condition()
self._tasks: list[tuple[float, int, dict[str, Any]]] = []
self._records: list[dict[str, Any]] = []
self._thread: threading.Thread | None = None
self._seq = 0
self._closing = False
def open(self) -> None:
try:
import cv2
except ImportError as exc: # pragma: no cover - depends on local bench environment.
raise SystemExit(
"OpenCV is required for webcam snapshots. Install it with: "
".\\.venv\\Scripts\\python.exe -m pip install opencv-python"
) from exc
self.output_dir.mkdir(parents=True, exist_ok=True)
self.cv2 = cv2
self.device = cv2.VideoCapture(self.camera_index)
if not self.device.isOpened():
raise SystemExit(f"could not open webcam index {self.camera_index}")
if self.width:
self.device.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
if self.height:
self.device.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
self._warm_up()
self._thread = threading.Thread(target=self._run, name="camera-snapshots", daemon=True)
self._thread.start()
def close(self) -> None:
with self._condition:
self._closing = True
self._condition.notify_all()
if self._thread is not None:
self._thread.join()
self._thread = None
if self.device is not None:
self.device.release()
self.device = None
def schedule(
self,
*,
label: str,
frame_text: str,
phase: str,
delay_seconds: float = 0.0,
step_index: int | None = None,
) -> dict[str, Any]:
if self.cv2 is None or self.device is None:
raise RuntimeError("camera snapshotter is not open")
due_monotonic = time.monotonic() + max(0.0, delay_seconds)
due_timestamp = datetime.now() + timedelta(seconds=max(0.0, delay_seconds))
timestamp = due_timestamp.strftime("%Y%m%d-%H%M%S-%f")[:-3]
step_part = f"step{step_index:03d}_" if step_index is not None else ""
frame_part = frame_text.replace(" ", "")
filename = _safe_name(f"{timestamp}_{step_part}{phase}_{label}_{frame_part}.jpg")
path = self.output_dir / filename
record: dict[str, Any] = {
"path": str(path),
"scheduled_timestamp": timestamp,
"camera_index": self.camera_index,
"step_index": step_index,
"phase": phase,
"delay_seconds": delay_seconds,
"label": label,
"frame": frame_text,
"status": "scheduled",
}
with self._condition:
self._seq += 1
heapq.heappush(self._tasks, (due_monotonic, self._seq, record))
self._records.append(record)
self._condition.notify_all()
return record
def records(self) -> list[dict[str, Any]]:
with self._condition:
return [dict(record) for record in self._records]
def _run(self) -> None:
while True:
with self._condition:
while not self._tasks and not self._closing:
self._condition.wait()
if not self._tasks and self._closing:
return
due_monotonic, _seq, record = self._tasks[0]
wait_seconds = due_monotonic - time.monotonic()
if wait_seconds > 0:
self._condition.wait(wait_seconds)
continue
heapq.heappop(self._tasks)
self._write_snapshot(record)
def _write_snapshot(self, record: dict[str, Any]) -> None:
if self.cv2 is None or self.device is None:
record["status"] = "error"
record["error"] = "camera closed before capture"
return
image = None
ok = False
for _attempt in range(3):
ok, image = self.device.read()
if ok:
break
time.sleep(0.020)
if not ok or image is None:
record["status"] = "error"
record["error"] = f"webcam index {self.camera_index} did not return an image"
return
path = Path(str(record["path"]))
if not self.cv2.imwrite(str(path), image):
record["status"] = "error"
record["error"] = f"failed to write webcam snapshot {path}"
return
record["status"] = "written"
record["captured_timestamp"] = datetime.now().strftime("%Y%m%d-%H%M%S-%f")[:-3]
def _warm_up(self) -> None:
if self.device is None:
return
deadline = time.monotonic() + self.warmup_seconds
while time.monotonic() < deadline:
self.device.read()
time.sleep(0.020)
self.device.read()
def _safe_name(text: str) -> str:
cleaned = _SAFE_NAME_RE.sub("_", text.strip()).strip("._")
return cleaned or "snapshot.jpg"