move_detect/edge/app.py
유창욱 01c7f4636e fix(edge): smooth keypoints per-track after tracking, not before (FIX 1)
edge/app.py run_once used ONE shared KeypointSmoother across all persons
BEFORE tracking assigned IDs. The smoother holds per-joint-index One-Euro
filter state, so with >1 person in frame the filter state was shared and
corrupted across different people, and same-timestamp calls within a frame
would pass through unsmoothed while still clobbering the shared state.
Multi-person scenes could get corrupted torso angles -> missed/false falls.

Add TrackSmootherPool (edge/smoothing.py) that lazily allocates one
KeypointSmoother per track_id. run_once now: tracks raw keypoints first,
then smooths each track's latest frame in place using its own pooled
smoother, and forgets a track's smoother when the track is lost (ties into
FIX 5 lost-track cleanup).

BEHAVIOR CHANGE: keypoints are now smoothed after tracking instead of
before. Single-person pipelines are unaffected (still one smoother, one
identity). Multi-person pipelines get correct, non-cross-contaminated
smoothing.

run_once signature: smoother param replaced with pool: TrackSmootherPool.
build_pipeline now constructs "pool" instead of "smoother".

Tests: tests/test_smoothing.py adds TrackSmootherPool identity/forget
coverage; tests/test_app.py adds test_two_persons_get_independent_smoothing
demonstrating each track's smoothed keypoints stay on its own side instead
of being pulled toward the other person.
2026-07-11 22:14:03 +09:00

59 lines
2 KiB
Python

from edge.config import Config
from edge.tracking import IouTracker
from edge.smoothing import TrackSmootherPool
from edge.detectors.fall import FallDetector
from edge.detectors.immobility import ImmobilityWatchdog
from edge.detectors.ghost import GhostIncidentDetector
from edge.detectors.zone import ZoneIntrusionDetector
from edge.engine import EventEngine
from edge.snapshot import RingBuffer, FaceBlurrer, Snapshotter
from edge.models import BBox
def _head_boxes(persons: list) -> list:
# 배경인물 머리영역 근사: 코 주변 작은 박스 (MVP; 실제는 얼굴 검출 포트로 교체)
boxes = []
for p in persons:
nose = p.keypoints[0]
boxes.append(BBox(nose.x - 20, nose.y - 25, nose.x + 20, nose.y + 15))
return boxes
def run_once(source, pose, tracker, pool, engine, ring, snapshotter, sink, now_fn):
ok, frame = source.read()
if not ok:
return []
now = now_fn()
persons = pose.infer(frame)
tracks = tracker.update(persons, now) # RAW keypoints; smoothing happens per-track below
lost = tracker.lost()
for tr in tracks:
f = tr.latest
f.keypoints = pool.smoother_for(tr.track_id).smooth(f.keypoints, now)
ring.push(now, frame)
events = engine.process(tracks, lost, now)
for tr in lost:
pool.forget(tr.track_id)
if events:
snaps = snapshotter.capture([frame], [_head_boxes(persons)])
for e in events:
sink.try_send(e, snaps)
return events
def build_pipeline(config: Config, source, pose) -> dict:
return {
"source": source,
"pose": pose,
"tracker": IouTracker(),
"pool": TrackSmootherPool(),
"engine": EventEngine(
FallDetector(config.fall),
ImmobilityWatchdog(config.immobility),
GhostIncidentDetector(config.ghost),
ZoneIntrusionDetector([], config.zone),
config.engine,
),
"ring": RingBuffer(),
"snapshotter": Snapshotter(FaceBlurrer()),
}