Initial commit: digital reception monorepo (M1-M11 + demo extensions)
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""Минимальный конвейер mp4 → face-service /embed → tracks → apps/api.
|
||||
|
||||
Замысел: smoke-тест для цепочки «видео → эмбеддинг → трек → БД».
|
||||
Это НЕ боевой video-ingest из ТЗ §6 (RTSP, ByteTrack, GPU) — это
|
||||
скаффолд Фазы 1, чтобы Фаза 0 не блокировала остальную работу.
|
||||
|
||||
Алгоритм:
|
||||
1. Открыть mp4 через OpenCV.
|
||||
2. Для каждого N-го кадра (SAMPLE_EVERY): JPEG → face-service /embed.
|
||||
3. Если эмбеддинг есть — single-camera трекинг по cos-дистанции:
|
||||
если дистанция к last_embedding < TRACK_DISTANCE_THRESH и
|
||||
прошло < TRACK_WINDOW_SEC секунд → тот же трек.
|
||||
Иначе — новый трек (создаём через apps/api /ingest/tracks).
|
||||
4. Сохраняем эмбеддинг через face-service /track-embeddings.
|
||||
5. На первом кадре трека постим событие `arrived` через /ingest/track-events.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from .api import FaceServiceClient, ReceptionApiClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IngestConfig:
|
||||
source: Path
|
||||
camera_name: str
|
||||
zone_code: str | None = None
|
||||
sample_every: int = int(os.getenv("SAMPLE_EVERY", "10"))
|
||||
track_distance_thresh: float = float(os.getenv("TRACK_DISTANCE_THRESH", "0.3"))
|
||||
track_window_sec: float = float(os.getenv("TRACK_WINDOW_SEC", "2.0"))
|
||||
jpeg_quality: int = 85
|
||||
real_time_start: datetime | None = None
|
||||
|
||||
|
||||
def cosine_distance(a: np.ndarray, b: np.ndarray) -> float:
|
||||
return float(1.0 - np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
|
||||
|
||||
|
||||
def run(cfg: IngestConfig) -> dict:
|
||||
cap = cv2.VideoCapture(str(cfg.source))
|
||||
if not cap.isOpened():
|
||||
raise RuntimeError(f"Не могу открыть видео: {cfg.source}")
|
||||
|
||||
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
|
||||
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
logger.info(f"Открыто видео {cfg.source} ({total} кадров, {fps:.1f} fps)")
|
||||
|
||||
face = FaceServiceClient()
|
||||
api = ReceptionApiClient()
|
||||
base_time = cfg.real_time_start or datetime.utcnow()
|
||||
|
||||
current_track_id: str | None = None
|
||||
current_track_camera_id: str | None = None
|
||||
last_embedding: np.ndarray | None = None
|
||||
last_capture_time: datetime | None = None
|
||||
tracks_created = 0
|
||||
embeddings_saved = 0
|
||||
frame_idx = 0
|
||||
|
||||
while True:
|
||||
ok, frame = cap.read()
|
||||
if not ok:
|
||||
break
|
||||
frame_idx += 1
|
||||
if frame_idx % cfg.sample_every != 0:
|
||||
continue
|
||||
|
||||
captured_at = base_time + timedelta(seconds=frame_idx / fps)
|
||||
|
||||
ok_jpg, buf = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), cfg.jpeg_quality])
|
||||
if not ok_jpg:
|
||||
logger.warning(f"frame {frame_idx}: jpeg encode failed")
|
||||
continue
|
||||
jpeg_bytes = buf.tobytes()
|
||||
|
||||
try:
|
||||
embed_result = face.embed(jpeg_bytes)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"frame {frame_idx}: face-service /embed failed: {e}")
|
||||
continue
|
||||
|
||||
if embed_result is None:
|
||||
continue
|
||||
|
||||
embedding_list, _quality = embed_result
|
||||
embedding = np.array(embedding_list, dtype=np.float32)
|
||||
|
||||
new_track = True
|
||||
if last_embedding is not None and last_capture_time is not None:
|
||||
dist = cosine_distance(embedding, last_embedding)
|
||||
age_sec = (captured_at - last_capture_time).total_seconds()
|
||||
if dist < cfg.track_distance_thresh and age_sec < cfg.track_window_sec:
|
||||
new_track = False
|
||||
|
||||
if new_track:
|
||||
track_info = api.create_track(camera_name=cfg.camera_name, first_seen_at=captured_at)
|
||||
current_track_id = track_info["trackId"]
|
||||
current_track_camera_id = track_info["cameraId"]
|
||||
api.add_event(
|
||||
track_id=current_track_id,
|
||||
event_type="arrived",
|
||||
camera_name=cfg.camera_name,
|
||||
occurred_at=captured_at,
|
||||
zone_code=cfg.zone_code,
|
||||
)
|
||||
tracks_created += 1
|
||||
logger.info(f"frame {frame_idx}: new track {current_track_id}")
|
||||
|
||||
assert current_track_id and current_track_camera_id
|
||||
|
||||
try:
|
||||
face.save_track_embedding(
|
||||
jpeg_bytes=jpeg_bytes,
|
||||
track_id=current_track_id,
|
||||
camera_id=current_track_camera_id,
|
||||
captured_at=captured_at,
|
||||
)
|
||||
embeddings_saved += 1
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"frame {frame_idx}: save_track_embedding failed: {e}")
|
||||
|
||||
last_embedding = embedding
|
||||
last_capture_time = captured_at
|
||||
|
||||
cap.release()
|
||||
logger.info(
|
||||
f"Готово: создано {tracks_created} треков, сохранено {embeddings_saved} эмбеддингов "
|
||||
f"(всего просмотрено {frame_idx} кадров)"
|
||||
)
|
||||
return {
|
||||
"tracks_created": tracks_created,
|
||||
"embeddings_saved": embeddings_saved,
|
||||
"frames_processed": frame_idx,
|
||||
}
|
||||
Reference in New Issue
Block a user