Two operator-value features: evidence that shows HOW it happened, and the one-line answer to "what happened overnight". Evidence timeline (closes the P1-B "前中後 스냅샷 배선" follow-up): - The ring buffer now stores per-frame head boxes, and an event captures the frames nearest T-2.5s / T-1.0s / T-0 (server cap: 3 snapshots). A fall's evidentiary value is in the seconds BEFORE the person hit the floor - one end-state frame never showed how it happened. - Each past frame is blurred with the head positions of ITS OWN moment - blurring history with the current tick's boxes would leave a face exactly where it actually was (PIPA). - Right after startup the ring is short: fewer frames go out, never zero and never duplicated. Detail view captions the sequence 이전/직전/발생 시점 by position (offsets are edge config the server doesn't know). 24h catch-up brief: - The events console renders one line above the stream on load - "지난 24시간 - 감지 N건 (중대 n) · 니어미스 n · 평균 대응 t · 미확인 n건 남음" - reusing GET /api/report?days=1, with a link into /report. High-tinted when the window held critical/high events; hidden entirely until data arrives. Verified: pytest 235 pass (3 new: timeline selection, short-buffer dedup, 3-frame capture through run_once) / PHPUnit 138 pass + Chrome real-render (brief line with severe tint on the console; detail page showing the 3-frame timeline with captions; no console errors).
47 lines
1.8 KiB
Python
47 lines
1.8 KiB
Python
import numpy as np
|
|
from edge.models import BBox
|
|
from edge.snapshot import RingBuffer, FaceBlurrer, Snapshotter
|
|
|
|
def _frame(val=128):
|
|
return np.full((64, 64, 3), val, dtype=np.uint8)
|
|
|
|
def test_ringbuffer_keeps_recent():
|
|
rb = RingBuffer(maxlen=3)
|
|
for i in range(5):
|
|
rb.push(float(i), _frame(i))
|
|
assert len(rb.frames) == 3
|
|
assert rb.frames[-1][0] == 4.0
|
|
|
|
def test_timeline_picks_frames_nearest_to_offsets():
|
|
rb = RingBuffer(maxlen=90)
|
|
for i in range(6): # t = 0..5, 1초 간격
|
|
rb.push(float(i), _frame(i * 10), [BBox(i, 0, i + 1, 1)])
|
|
frames, boxes = rb.timeline(now=5.0, offsets_s=(2.5, 1.0, 0.0))
|
|
assert len(frames) == 3
|
|
# 과거→현재 순: 목표 2.5/4.0/5.0초에 가장 가까운 t=2(또는 3), 4, 5
|
|
assert frames[-1][0, 0, 0] == 50 # 마지막은 현재 프레임
|
|
assert boxes[-1][0].x1 == 5 # 각 프레임의 '자기 시점' 블러 박스
|
|
assert frames[0][0, 0, 0] < frames[1][0, 0, 0] < frames[2][0, 0, 0]
|
|
|
|
|
|
def test_timeline_short_buffer_returns_fewer_without_duplicates():
|
|
rb = RingBuffer(maxlen=90)
|
|
rb.push(0.0, _frame(10), [])
|
|
frames, boxes = rb.timeline(now=0.0, offsets_s=(2.5, 1.0, 0.0))
|
|
assert len(frames) == 1 # 기동 직후: 있는 프레임 하나만, 중복 없음
|
|
assert len(boxes) == 1
|
|
|
|
|
|
def test_face_blur_changes_region():
|
|
fb = FaceBlurrer()
|
|
f = _frame(0)
|
|
f[0:10, 0:10] = 255
|
|
out = fb.blur(f.copy(), [BBox(0, 0, 10, 10)])
|
|
assert out is not None
|
|
assert out.shape == f.shape
|
|
|
|
def test_snapshotter_returns_jpeg_bytes():
|
|
snap = Snapshotter(FaceBlurrer())
|
|
imgs = snap.capture([_frame(100), _frame(120)], [[], []])
|
|
assert 1 <= len(imgs) <= 3
|
|
assert imgs[0][:2] == b"\xff\xd8" # JPEG magic
|