Three keypoint-trust hardening fixes that reuse the codebase's existing "visible" confidence convention (no new tuning knobs): - P2-9 zone.py: _foot averaged both ankles unconditionally, so a hallucinated ankle (conf~0, often pinned to (0,0)) dragged the foot point into or out of a zone. Now averages only ankles with conf >= 0.2 and falls back to the bbox bottom-centre when neither is trustworthy. - P2-11 geometry.py: head_below_hips used the NOSE regardless of confidence. Now returns False when the nose is below the visibility threshold, so an unconfident/hallucinated nose can't fire the head-below-hips fall cue. The confident-nose path is unchanged (test_fall.py stays green). - P2-10 movenet_pose.py: the CPU MoveNet fallback is single-person, so at a multi-worker site every worker but one is silently invisible to fall/zone/ghost detection. Construction now logs a loud startup WARNING pointing to the Hailo backend. Tests: test_zone (bbox fallback), test_geometry (low-conf nose), and test_movenet_pose (caplog warning). Full python suite: 157 passed, 2 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
43 lines
2 KiB
Python
43 lines
2 KiB
Python
import logging
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from edge.models import Person, BBox, Keypoint
|
|
|
|
|
|
class MoveNetPoseAdapter:
|
|
"""MoveNet SinglePose Lightning (COCO-17) via onnxruntime. CPU dev/pilot PosePort.
|
|
출력 [1,1,17,3] = (y, x, score) normalized 0..1."""
|
|
|
|
def __init__(self, onnx_path: str, input_size: int = 192, session=None,
|
|
min_person_score: float = 0.2):
|
|
self.input_size = input_size
|
|
self.min_person_score = min_person_score
|
|
if session is None:
|
|
import onnxruntime # gated: bundled on build machine
|
|
session = onnxruntime.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
|
|
self.session = session
|
|
self._input_name = session.get_inputs()[0].name
|
|
# MoveNet SinglePose returns ONE person per frame. At a multi-worker site every worker
|
|
# but one is invisible to fall/zone/ghost detection with no other symptom — so say it
|
|
# loudly at startup. The Hailo backend (yolov8s_pose) is multi-person.
|
|
logging.getLogger("edge.pose").warning(
|
|
"MoveNet CPU fallback is SINGLE-PERSON: only one worker is tracked per frame; any "
|
|
"additional workers are invisible to fall/zone/ghost detection. Use the Hailo "
|
|
"backend (POSE_BACKEND=hailo) at multi-person sites.")
|
|
|
|
def infer(self, frame) -> list:
|
|
h, w = frame.shape[:2]
|
|
img = cv2.resize(frame, (self.input_size, self.input_size))
|
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.int32)[None, ...]
|
|
out = self.session.run(None, {self._input_name: img})[0] # [1,1,17,3]
|
|
kp = out[0, 0] # [17,3] (y,x,score)
|
|
keypoints = [Keypoint(float(kp[j, 1] * w), float(kp[j, 0] * h), float(kp[j, 2]))
|
|
for j in range(17)]
|
|
vis = [k for k in keypoints if k.conf >= self.min_person_score]
|
|
if not vis:
|
|
return []
|
|
xs = [k.x for k in vis]; ys = [k.y for k in vis]
|
|
bbox = BBox(min(xs), min(ys), max(xs), max(ys))
|
|
return [Person(bbox, keypoints)]
|