Версия цифровой рецепции с резализованным механизмом отслеживания трека пациента по зонам
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

144 lines
5.3 KiB

"""Минимальный конвейер 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,
}