Pabloler21 Claude Opus 5.5 commited on
Commit
e81ca22
·
1 Parent(s): 132e289

Decidir quién es intruso por el centro del cuerpo, no por los pies

Browse files

Con 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 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 la pisa.
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 con los pies en la zona.
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 tuvo los pies en 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
 
 
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 feet_in_zone, zone_from_layers
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
- feet = detections.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
104
- inside = feet_in_zone(feet, zone)
 
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 la pisa.
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 feet_in_zone(feet_xy: np.ndarray, zone: np.ndarray) -> np.ndarray:
83
- """Indica qué personas tienen los pies dentro de la zona.
84
 
85
- Se usan los pies (centro del borde inferior del recuadro) y no el recuadro entero porque,
86
- con la cámara en perspectiva, alguien que pasa delante de una puerta la tapa con el cuerpo
87
- sin estar en ella: los pies dicen dónde está parado de verdad.
 
88
 
89
  Args:
90
- feet_xy: Array (N, 2) con las coordenadas (x, y) de los pies de cada persona.
91
  zone: Máscara booleana (alto, ancho).
92
 
93
  Returns:
94
- Array booleano (N,): True si los pies de esa persona caen en la zona.
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(feet_xy[:, 0].astype(int), 0, w - 1)
99
- y = np.clip(feet_xy[:, 1].astype(int), 0, h - 1)
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 # pies en (x, 75)
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 feet_in_zone, fill_outlines, zone_from_layers
8
 
9
 
10
  def outline(gap=0):
@@ -67,7 +67,7 @@ def zone():
67
 
68
 
69
  @pytest.mark.parametrize(
70
- ("feet", "expected"),
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 test_feet_in_zone(zone, feet, expected):
81
- assert feet_in_zone(np.array([feet]), zone).tolist() == [expected]
82
 
83
 
84
- def test_feet_in_zone_without_people(zone):
85
- assert feet_in_zone(np.empty((0, 2)), zone).tolist() == []
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():