Three intertwined bodies of work developed and tested together this session. Full test suite green: python (edge) + phpunit (cloud). ## Edge/Pi install path - setup_online.sh: apt-lock retry (apt-daily contention), Type=exec unit + StartLimitIntervalSec in [Unit], journalctl-on-failure, crash-loop detection. - onnxruntime is now best-effort (no sdist / no 32-bit-ARM wheel); [6/8] probes the real backend and hard-fails if none works. - Hailo HEF auto-fetch: edge/tools/fetch_hailo_hef.py (pinned sha256, refuses to guess 8 vs 8L, HAILO_ARCH override for serial-less boards); hailo_pose.py run() timeout fix. - install_offline.sh: mirrors the backend gate; onnxruntime optional. ## Cafe24 cloud package (cloud/deploy/) - build_cafe24_package.sh emits move_detect_app/ (private, one folder) + www/move_detect/ (docroot subdir); no-dev classmap autoloader; preflight.php; hardened .htaccess. - Subdirectory mount support: Request::baseFrom/pathFor strip the mount prefix; index.php walks up to find the app root; views/JS prefix every URL with APP_BASE; session cookie scoped to the mount. ## Delivery audit — silent-failure & correctness (docs/delivery-audit.md) All 4 blockers + all Phase-1 silent-failure + Phase-2 HIGH correctness: - Edge cloud clients send a custom User-Agent (cafe24 WAF blocks python-requests) and allow_redirects=False. - Daemon fails loud on blank CAFE24_URL/DEVICE_SECRET; run_loop logs swallowed exceptions; heartbeat carries real fps + per-camera health; UvcSource reopens a dead camera; offline queue is size-capped and drops poison rows; TARGET_FPS/QUEUE_DB validated. - Zone coordinates normalized 0..1 on both sides (were canvas-px vs camera-px — every zone misplaced); pinch zones emit PINCH/HIGH (were downgraded to WARNING). - Fall detection uses bbox cues when keypoints are low-confidence (occluded fallen person); ghost incident not cancelled by a bystander; displacement normalized per-second (FPS-invariant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
76 lines
2.7 KiB
Python
76 lines
2.7 KiB
Python
import math
|
|
|
|
from edge.models import (
|
|
Keypoint, BBox, NOSE, L_SHOULDER, R_SHOULDER, L_HIP, R_HIP, CORE_JOINTS,
|
|
)
|
|
|
|
|
|
def _mean(points: list[tuple[float, float]]) -> tuple[float, float]:
|
|
n = len(points)
|
|
return (sum(p[0] for p in points) / n, sum(p[1] for p in points) / n)
|
|
|
|
|
|
def com(keypoints: list[Keypoint]) -> tuple[float, float]:
|
|
pts = [(keypoints[i].x, keypoints[i].y) for i in CORE_JOINTS]
|
|
return _mean(pts)
|
|
|
|
|
|
def _shoulder_center(kps: list[Keypoint]) -> tuple[float, float]:
|
|
return _mean([(kps[L_SHOULDER].x, kps[L_SHOULDER].y),
|
|
(kps[R_SHOULDER].x, kps[R_SHOULDER].y)])
|
|
|
|
|
|
def _hip_center(kps: list[Keypoint]) -> tuple[float, float]:
|
|
return _mean([(kps[L_HIP].x, kps[L_HIP].y),
|
|
(kps[R_HIP].x, kps[R_HIP].y)])
|
|
|
|
|
|
def torso_angle_deg(keypoints: list[Keypoint]) -> float:
|
|
"""몸통(어깨중심→엉덩이중심) 벡터가 수직축과 이루는 각. 0=수직, 90=수평."""
|
|
sx, sy = _shoulder_center(keypoints)
|
|
hx, hy = _hip_center(keypoints)
|
|
vx, vy = hx - sx, hy - sy
|
|
# 수직축(0,1) 대비 편차. 위/아래 무관하게 |성분| 사용.
|
|
return math.degrees(math.atan2(abs(vx), abs(vy))) if (vx or vy) else 0.0
|
|
|
|
|
|
def head_below_hips(keypoints: list[Keypoint]) -> bool:
|
|
_, hy = _hip_center(keypoints)
|
|
return keypoints[NOSE].y > hy
|
|
|
|
|
|
def visible_count(keypoints: list[Keypoint], thr: float = 0.2) -> int:
|
|
return sum(1 for k in keypoints if k.conf >= thr)
|
|
|
|
|
|
def core_visible(keypoints: list[Keypoint], thr: float = 0.5) -> bool:
|
|
return all(keypoints[i].conf >= thr for i in CORE_JOINTS)
|
|
|
|
|
|
def height_of(bbox: BBox) -> float:
|
|
return bbox.height
|
|
|
|
|
|
def normalize(value: float, bbox_height: float) -> float:
|
|
return value / bbox_height if bbox_height else 0.0
|
|
|
|
|
|
def track_displacement(track) -> float:
|
|
"""Mean per-keypoint SPEED between the last two frames: displacement normalized by bbox
|
|
height AND by elapsed time (per second). Without the time term the value scales with the
|
|
frame rate, so the same physical stillness reads as 'moving' at low fps and 'still' at
|
|
high fps — flipping immobility/no_motion with the daemon's actual (load-dependent) fps.
|
|
Returns 1.0 (moving/unknown) with <2 frames or a non-positive dt.
|
|
|
|
NOTE: motion_thr in FallConfig/ImmobilityConfig is now a per-SECOND threshold; its default
|
|
predates this change and should be re-tuned on device."""
|
|
if len(track.frames) < 2:
|
|
return 1.0
|
|
a, b = track.frames[-2], track.frames[-1]
|
|
dt = b.t - a.t
|
|
if dt <= 0:
|
|
return 1.0
|
|
h = b.bbox.height or 1.0
|
|
d = sum(((p.x - q.x) ** 2 + (p.y - q.y) ** 2) ** 0.5
|
|
for p, q in zip(a.keypoints, b.keypoints)) / len(b.keypoints)
|
|
return normalize(d, h) / dt
|