Spaces:
Sleeping
Sleeping
Commit ·
e81ca22
1
Parent(s): 132e289
Decidir quién es intruso por el centro del cuerpo, no por los pies
Browse filesCon la cámara en ángulo, la regla de los pies marcaba como intruso a
quien tenía el cuerpo afuera y apenas pisaba el borde, y no marcaba a
quien tenía el cuerpo sobre la zona pero los pies un poco afuera (7 de
~30 errores en cuadros del ejemplo). El centro del recuadro coincide con
lo que ve quien mira el video.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- camguard/__init__.py +1 -1
- camguard/annotate.py +1 -1
- camguard/events.py +1 -1
- camguard/pipeline.py +4 -3
- camguard/zone.py +11 -10
- tests/test_pipeline.py +21 -1
- tests/test_zone.py +6 -6
camguard/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ Todo corre local, en CPU: el video nunca sale de la máquina.
|
|
| 4 |
|
| 5 |
Módulos:
|
| 6 |
config: perillas ajustables (fps analizados, tope de duración, confianza).
|
| 7 |
-
zone: de lo que pinta el usuario a una máscara, y quién
|
| 8 |
events: de cuadros sueltos a eventos de entrada y salida.
|
| 9 |
video: lectura submuestreada y escritura H.264.
|
| 10 |
detection: detección (RF-DETR) y seguimiento (ByteTrack) de personas.
|
|
|
|
| 4 |
|
| 5 |
Módulos:
|
| 6 |
config: perillas ajustables (fps analizados, tope de duración, confianza).
|
| 7 |
+
zone: de lo que pinta el usuario a una máscara, y quién está sobre ella.
|
| 8 |
events: de cuadros sueltos a eventos de entrada y salida.
|
| 9 |
video: lectura submuestreada y escritura H.264.
|
| 10 |
detection: detección (RF-DETR) y seguimiento (ByteTrack) de personas.
|
camguard/annotate.py
CHANGED
|
@@ -15,7 +15,7 @@ def annotate(
|
|
| 15 |
Args:
|
| 16 |
frame_bgr: Cuadro original en BGR. No se modifica.
|
| 17 |
detections: Personas detectadas, con ``tracker_id``.
|
| 18 |
-
inside: Array booleano: True para las personas
|
| 19 |
zone: Máscara booleana de la zona.
|
| 20 |
|
| 21 |
Returns:
|
|
|
|
| 15 |
Args:
|
| 16 |
frame_bgr: Cuadro original en BGR. No se modifica.
|
| 17 |
detections: Personas detectadas, con ``tracker_id``.
|
| 18 |
+
inside: Array booleano: True para las personas que están sobre la zona.
|
| 19 |
zone: Máscara booleana de la zona.
|
| 20 |
|
| 21 |
Returns:
|
camguard/events.py
CHANGED
|
@@ -36,7 +36,7 @@ def build_events(
|
|
| 36 |
|
| 37 |
Args:
|
| 38 |
frames_in_zone: Para cada persona, los índices (crecientes) de los cuadros analizados
|
| 39 |
-
en que
|
| 40 |
fps: Cuadros analizados por segundo, para pasar de índices a segundos.
|
| 41 |
max_gap_s: Tolerancia en segundos antes de considerar que la persona salió.
|
| 42 |
|
|
|
|
| 36 |
|
| 37 |
Args:
|
| 38 |
frames_in_zone: Para cada persona, los índices (crecientes) de los cuadros analizados
|
| 39 |
+
en que estuvo sobre la zona.
|
| 40 |
fps: Cuadros analizados por segundo, para pasar de índices a segundos.
|
| 41 |
max_gap_s: Tolerancia en segundos antes de considerar que la persona salió.
|
| 42 |
|
camguard/pipeline.py
CHANGED
|
@@ -19,7 +19,7 @@ from camguard.config import FPS, MAX_SECONDS
|
|
| 19 |
from camguard.detection import PersonTracker
|
| 20 |
from camguard.events import Event, build_events
|
| 21 |
from camguard.video import H264Writer, frame_limit, probe, sample_frames, sampling_step
|
| 22 |
-
from camguard.zone import
|
| 23 |
|
| 24 |
|
| 25 |
class Tracker(Protocol):
|
|
@@ -100,8 +100,9 @@ def analyze(
|
|
| 100 |
with H264Writer(out_path, fps, info.width, info.height) as writer:
|
| 101 |
for i, frame in enumerate(sample_frames(video_path, step, max_frames)):
|
| 102 |
detections = tracker.update(frame)
|
| 103 |
-
|
| 104 |
-
|
|
|
|
| 105 |
annotated = annotate(frame, detections, inside, zone)
|
| 106 |
for track_id in detections.tracker_id[inside].tolist():
|
| 107 |
if track_id not in frames_in_zone:
|
|
|
|
| 19 |
from camguard.detection import PersonTracker
|
| 20 |
from camguard.events import Event, build_events
|
| 21 |
from camguard.video import H264Writer, frame_limit, probe, sample_frames, sampling_step
|
| 22 |
+
from camguard.zone import points_in_zone, zone_from_layers
|
| 23 |
|
| 24 |
|
| 25 |
class Tracker(Protocol):
|
|
|
|
| 100 |
with H264Writer(out_path, fps, info.width, info.height) as writer:
|
| 101 |
for i, frame in enumerate(sample_frames(video_path, step, max_frames)):
|
| 102 |
detections = tracker.update(frame)
|
| 103 |
+
# Intruso = el centro de su cuerpo está sobre la zona (ver points_in_zone).
|
| 104 |
+
centers = detections.get_anchors_coordinates(sv.Position.CENTER)
|
| 105 |
+
inside = points_in_zone(centers, zone)
|
| 106 |
annotated = annotate(frame, detections, inside, zone)
|
| 107 |
for track_id in detections.tracker_id[inside].tolist():
|
| 108 |
if track_id not in frames_in_zone:
|
camguard/zone.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Zona restringida: de lo que pinta el usuario a una máscara, y quién
|
| 2 |
|
| 3 |
La zona es una máscara booleana del tamaño del video (True = restringido) en vez de un
|
| 4 |
polígono, porque el usuario la pinta a mano: puede tener cualquier forma o ser varias zonas.
|
|
@@ -79,22 +79,23 @@ def fill_outlines(mask: np.ndarray) -> np.ndarray:
|
|
| 79 |
return cv2.erode(filled, _disk(half_stroke)).astype(bool)
|
| 80 |
|
| 81 |
|
| 82 |
-
def
|
| 83 |
-
"""Indica qué
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
| 88 |
|
| 89 |
Args:
|
| 90 |
-
|
| 91 |
zone: Máscara booleana (alto, ancho).
|
| 92 |
|
| 93 |
Returns:
|
| 94 |
-
Array booleano (N,): True si
|
| 95 |
"""
|
| 96 |
h, w = zone.shape
|
| 97 |
# clip: un punto justo en el borde (x == ancho) se saldría del array al indexar.
|
| 98 |
-
x = np.clip(
|
| 99 |
-
y = np.clip(
|
| 100 |
return zone[y, x]
|
|
|
|
| 1 |
+
"""Zona restringida: de lo que pinta el usuario a una máscara, y quién está sobre ella.
|
| 2 |
|
| 3 |
La zona es una máscara booleana del tamaño del video (True = restringido) en vez de un
|
| 4 |
polígono, porque el usuario la pinta a mano: puede tener cualquier forma o ser varias zonas.
|
|
|
|
| 79 |
return cv2.erode(filled, _disk(half_stroke)).astype(bool)
|
| 80 |
|
| 81 |
|
| 82 |
+
def points_in_zone(points_xy: np.ndarray, zone: np.ndarray) -> np.ndarray:
|
| 83 |
+
"""Indica qué puntos caen dentro de la zona.
|
| 84 |
|
| 85 |
+
El pipeline pasa el centro del recuadro de cada persona: es lo que usa el ojo para decir
|
| 86 |
+
si alguien "está" sobre la zona en la imagen. Primero se usaron los pies, más fieles a
|
| 87 |
+
dónde pisa, pero con la cámara en ángulo marcaban como intruso a quien tenía el cuerpo
|
| 88 |
+
afuera y apenas pisaba el borde, y al revés (7 de ~30 errores en cuadros del ejemplo).
|
| 89 |
|
| 90 |
Args:
|
| 91 |
+
points_xy: Array (N, 2) con coordenadas (x, y), una por persona.
|
| 92 |
zone: Máscara booleana (alto, ancho).
|
| 93 |
|
| 94 |
Returns:
|
| 95 |
+
Array booleano (N,): True si ese punto cae en la zona.
|
| 96 |
"""
|
| 97 |
h, w = zone.shape
|
| 98 |
# clip: un punto justo en el borde (x == ancho) se saldría del array al indexar.
|
| 99 |
+
x = np.clip(points_xy[:, 0].astype(int), 0, w - 1)
|
| 100 |
+
y = np.clip(points_xy[:, 1].astype(int), 0, h - 1)
|
| 101 |
return zone[y, x]
|
tests/test_pipeline.py
CHANGED
|
@@ -17,7 +17,7 @@ class FakeTracker:
|
|
| 17 |
self.frame = 0
|
| 18 |
|
| 19 |
def update(self, frame_bgr):
|
| 20 |
-
x = 75 if 2 <= self.frame <= 5 else 25 #
|
| 21 |
self.frame += 1
|
| 22 |
# Como el tracker real: solo personas (class_id = PERSON_ID) y con número de seguimiento.
|
| 23 |
return sv.Detections(
|
|
@@ -56,6 +56,26 @@ def test_analyze_detects_the_intrusion(video, zone_layers):
|
|
| 56 |
assert progress[-1] == 1.0
|
| 57 |
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
def test_analyze_respects_max_seconds(video, zone_layers, monkeypatch):
|
| 60 |
monkeypatch.setattr("camguard.pipeline.MAX_SECONDS", 1)
|
| 61 |
result = analyze(video, zone_layers, make_tracker=FakeTracker)
|
|
|
|
| 17 |
self.frame = 0
|
| 18 |
|
| 19 |
def update(self, frame_bgr):
|
| 20 |
+
x = 75 if 2 <= self.frame <= 5 else 25 # centro del cuerpo en (x, 65)
|
| 21 |
self.frame += 1
|
| 22 |
# Como el tracker real: solo personas (class_id = PERSON_ID) y con número de seguimiento.
|
| 23 |
return sv.Detections(
|
|
|
|
| 56 |
assert progress[-1] == 1.0
|
| 57 |
|
| 58 |
|
| 59 |
+
class StraddlingTracker:
|
| 60 |
+
"""Una persona parada sobre el borde de arriba: pies apenas adentro, cuerpo afuera."""
|
| 61 |
+
|
| 62 |
+
def __init__(self, fps):
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
def update(self, frame_bgr):
|
| 66 |
+
return sv.Detections(
|
| 67 |
+
xyxy=np.array([[65.0, 20.0, 85.0, 55.0]]), # centro en y=37 (afuera); pies en y=55
|
| 68 |
+
class_id=np.array([PERSON_ID]),
|
| 69 |
+
tracker_id=np.array([1]),
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_person_counts_by_body_center_not_feet(video, zone_layers):
|
| 74 |
+
# En la imagen se la ve afuera de la zona, que es lo que importa a quien mira el video.
|
| 75 |
+
result = analyze(video, zone_layers, make_tracker=StraddlingTracker)
|
| 76 |
+
assert result.events == []
|
| 77 |
+
|
| 78 |
+
|
| 79 |
def test_analyze_respects_max_seconds(video, zone_layers, monkeypatch):
|
| 80 |
monkeypatch.setattr("camguard.pipeline.MAX_SECONDS", 1)
|
| 81 |
result = analyze(video, zone_layers, make_tracker=FakeTracker)
|
tests/test_zone.py
CHANGED
|
@@ -4,7 +4,7 @@ import cv2
|
|
| 4 |
import numpy as np
|
| 5 |
import pytest
|
| 6 |
|
| 7 |
-
from camguard.zone import
|
| 8 |
|
| 9 |
|
| 10 |
def outline(gap=0):
|
|
@@ -67,7 +67,7 @@ def zone():
|
|
| 67 |
|
| 68 |
|
| 69 |
@pytest.mark.parametrize(
|
| 70 |
-
("
|
| 71 |
[
|
| 72 |
pytest.param((7, 7), True, id="adentro"),
|
| 73 |
pytest.param((2, 2), False, id="afuera"),
|
|
@@ -77,12 +77,12 @@ def zone():
|
|
| 77 |
pytest.param((50, 50), True, id="fuera-de-la-imagen-se-lleva-al-borde"),
|
| 78 |
],
|
| 79 |
)
|
| 80 |
-
def
|
| 81 |
-
assert
|
| 82 |
|
| 83 |
|
| 84 |
-
def
|
| 85 |
-
assert
|
| 86 |
|
| 87 |
|
| 88 |
def test_zone_from_layers_combines_all_layers():
|
|
|
|
| 4 |
import numpy as np
|
| 5 |
import pytest
|
| 6 |
|
| 7 |
+
from camguard.zone import fill_outlines, points_in_zone, zone_from_layers
|
| 8 |
|
| 9 |
|
| 10 |
def outline(gap=0):
|
|
|
|
| 67 |
|
| 68 |
|
| 69 |
@pytest.mark.parametrize(
|
| 70 |
+
("point", "expected"),
|
| 71 |
[
|
| 72 |
pytest.param((7, 7), True, id="adentro"),
|
| 73 |
pytest.param((2, 2), False, id="afuera"),
|
|
|
|
| 77 |
pytest.param((50, 50), True, id="fuera-de-la-imagen-se-lleva-al-borde"),
|
| 78 |
],
|
| 79 |
)
|
| 80 |
+
def test_points_in_zone(zone, point, expected):
|
| 81 |
+
assert points_in_zone(np.array([point]), zone).tolist() == [expected]
|
| 82 |
|
| 83 |
|
| 84 |
+
def test_points_in_zone_without_people(zone):
|
| 85 |
+
assert points_in_zone(np.empty((0, 2)), zone).tolist() == []
|
| 86 |
|
| 87 |
|
| 88 |
def test_zone_from_layers_combines_all_layers():
|