"""TDD for HailoPoseAdapter — locks the yolov8s_pose host decode (DFL box + keypoint decode + NMS + COCO-17 assembly + letterbox un-mapping) WITHOUT hardware/hailort, by injecting a fake infer_fn that returns synthetic raw output tensors. yolov8s_pose emits 9 raw tensors (no on-chip NMS): per stride (8/16/32) a box(H,W,64 DFL), score(H,W,1 already-sigmoid), and kpts(H,W,51 = 17*(x,y,score)). """ import inspect import re import numpy as np from edge.adapters.hailo_pose import HailoPoseAdapter INPUT = 640 STRIDES = (8, 16, 32) def _blank_outputs(): outs = [] for s in STRIDES: size = INPUT // s outs.append(np.zeros((1, size, size, 64), np.float32)) # box DFL outs.append(np.zeros((1, size, size, 1), np.float32)) # class score (sigmoid'd) outs.append(np.zeros((1, size, size, 51), np.float32)) # keypoints return outs def _one_person_outputs(): outs = _blank_outputs() # activate a single cell on the stride-8 map (size 80), grid cell (40, 40) box, score, kpts = outs[0], outs[1], outs[2] r = c = 40 score[0, r, c, 0] = 0.9 # person prob (already sigmoid on-chip) # DFL: one-hot each of the 4 sides at bin index 2 -> distance = 2 cells * stride8 = 16 px b = box[0, r, c].reshape(4, 16) b[:, 2] = 50.0 box[0, r, c] = b.reshape(64) # keypoints: raw x=y=0.5, raw score=3.0 for all 17 joints k = kpts[0, r, c].reshape(17, 3) k[:, 0] = 0.5 k[:, 1] = 0.5 k[:, 2] = 3.0 kpts[0, r, c] = k.reshape(51) return outs def test_infer_maps_yolov8_pose_to_coco17_person(): frame = np.zeros((640, 640, 3), np.uint8) # already 640x640 -> scale 1, no padding outs = _one_person_outputs() a = HailoPoseAdapter("unused.hef", infer_fn=lambda f: outs) persons = a.infer(frame) assert len(persons) == 1 p = persons[0] assert len(p.keypoints) == 17 # grid center = (40 + 0.5) * 8 = 324 ; box +/- 16 -> [308, 340] assert abs(p.bbox.x1 - 308) < 2 and abs(p.bbox.y1 - 308) < 2 assert abs(p.bbox.x2 - 340) < 2 and abs(p.bbox.y2 - 340) < 2 # keypoint decode: stride*(2*0.5 - 0.5) + 324 = 8*0.5 + 324 = 328 assert abs(p.keypoints[0].x - 328) < 2 and abs(p.keypoints[0].y - 328) < 2 # joint score sigmoided: sigmoid(3.0) ~= 0.9526 assert abs(p.keypoints[0].conf - 0.9526) < 0.01 def test_no_detection_returns_empty(): a = HailoPoseAdapter("unused.hef", infer_fn=lambda f: _blank_outputs()) assert a.infer(np.zeros((640, 640, 3), np.uint8)) == [] def test_hailort_run_is_called_positionally_not_with_timeout_ms(): """HailoRT's ConfiguredInferModel.run() takes `timeout`, not `timeout_ms`. Verified verbatim against upstream pyhailort.py at v4.19.0, v4.20.0, v4.23.0 and master — all four are `def run(self, bindings, timeout):`. (The neighbouring `wait_for_async_ready(self, timeout_ms=1000, ...)` DOES use `timeout_ms`, which is the trap.) So `run(..., timeout_ms=...)` is a TypeError on EVERY frame. That failure is invisible without this test: `_build_hailo_infer` is device-only (`# pragma: no cover`), so nothing executes it here, and on the Pi `run_loop` catches Exception per frame and only bumps an error counter — the unit stays green and emits zero events, forever. Hence a source-level assertion rather than a behavioural one. """ src = inspect.getsource(HailoPoseAdapter._build_hailo_infer) # Inspect the CALL, not the whole source: the code comment beside it necessarily spells # out the wrong form, and a naive `"timeout_ms=" not in src` matches that comment. call = re.search(r"configured\.run\((.*?)\)\n", src) assert call, "configured.run(...) not found in _build_hailo_infer" args = call.group(1).strip() assert "timeout_ms" not in args, \ f"ConfiguredInferModel.run() has no timeout_ms kwarg; pass it positionally. Got: run({args})" assert re.fullmatch(r"\[bindings\],\s*\d+", args), \ f"expected run([bindings], ) — positional timeout. Got: run({args})"