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
73 lines
3.2 KiB
Python
73 lines
3.2 KiB
Python
import math
|
|
from collections import deque
|
|
from edge.models import Keypoint, BBox
|
|
from edge.tracking import Track, TrackFrame
|
|
from edge.geometry import (
|
|
com, torso_angle_deg, head_below_hips, visible_count,
|
|
core_visible, normalize, track_displacement,
|
|
)
|
|
|
|
def _kps(overrides):
|
|
kps = [Keypoint(0, 0, 0.0) for _ in range(17)]
|
|
for i, (x, y, c) in overrides.items():
|
|
kps[i] = Keypoint(x, y, c)
|
|
return kps
|
|
|
|
def test_com_is_mean_of_shoulders_and_hips():
|
|
kps = _kps({5: (0, 0, 1), 6: (10, 0, 1), 11: (0, 10, 1), 12: (10, 10, 1)})
|
|
assert com(kps) == (5.0, 5.0)
|
|
|
|
def test_torso_angle_upright_near_zero():
|
|
# shoulders above hips (smaller y), vertical torso
|
|
kps = _kps({5: (0, 0, 1), 6: (10, 0, 1), 11: (0, 100, 1), 12: (10, 100, 1)})
|
|
assert torso_angle_deg(kps) < 10
|
|
|
|
def test_torso_angle_lying_near_ninety():
|
|
# shoulders and hips at same height, horizontal torso
|
|
kps = _kps({5: (0, 50, 1), 6: (0, 55, 1), 11: (100, 50, 1), 12: (100, 55, 1)})
|
|
assert torso_angle_deg(kps) > 80
|
|
|
|
def test_head_below_hips_true_when_nose_lower_on_screen():
|
|
kps = _kps({0: (5, 120, 1), 11: (0, 100, 1), 12: (10, 100, 1)})
|
|
assert head_below_hips(kps) is True
|
|
|
|
def test_visible_count_and_core_visible():
|
|
kps = _kps({5: (0, 0, 0.9), 6: (1, 0, 0.9), 11: (0, 1, 0.9), 12: (1, 1, 0.9)})
|
|
assert visible_count(kps, thr=0.2) == 4
|
|
assert core_visible(kps, thr=0.5) is True
|
|
|
|
def test_normalize_divides_by_bbox_height():
|
|
assert normalize(30, 60) == 0.5
|
|
assert normalize(30, 0) == 0.0
|
|
|
|
def test_track_displacement_defaults_to_one_with_fewer_than_two_frames():
|
|
tr = Track(1, deque(maxlen=90))
|
|
tr.frames.append(TrackFrame(0.0, BBox(0, 0, 20, 60),
|
|
[Keypoint(0, 0, 0.9) for _ in range(17)]))
|
|
assert track_displacement(tr) == 1.0
|
|
|
|
def test_track_displacement_normalizes_mean_delta_by_bbox_height_and_time():
|
|
tr = Track(1, deque(maxlen=90))
|
|
tr.frames.append(TrackFrame(0.0, BBox(0, 0, 20, 60),
|
|
[Keypoint(0, 0, 0.9) for _ in range(17)]))
|
|
tr.frames.append(TrackFrame(0.1, BBox(0, 0, 20, 60),
|
|
[Keypoint(6, 8, 0.9) for _ in range(17)]))
|
|
# per-point delta = sqrt(6^2+8^2) = 10; /bbox height 60; /dt 0.1 -> per-second, per-height
|
|
assert math.isclose(track_displacement(tr), (10 / 60) / 0.1)
|
|
|
|
|
|
def test_displacement_is_fps_invariant():
|
|
# The stillness measure must reflect real SPEED, not per-frame delta: the same physical
|
|
# motion sampled at 10fps vs 1fps must read the same, or immobility/no_motion flips with
|
|
# the daemon's actual frame rate (which varies with Hailo/CPU load and camera count).
|
|
def moving_track(dt, speed_px_s):
|
|
move = speed_px_s * dt
|
|
tr = Track(1, deque(maxlen=90))
|
|
tr.frames.append(TrackFrame(0.0, BBox(0, 0, 10, 100),
|
|
[Keypoint(0, 0, 0.9) for _ in range(17)]))
|
|
tr.frames.append(TrackFrame(dt, BBox(0, 0, 10, 100),
|
|
[Keypoint(0, move, 0.9) for _ in range(17)]))
|
|
return tr
|
|
slow = track_displacement(moving_track(1.0, 5.0)) # sampled once per second
|
|
fast = track_displacement(moving_track(0.1, 5.0)) # sampled ten times per second
|
|
assert math.isclose(slow, fast)
|