spike(vision): rtmpose latency, birefnet matting proxies, body-shape prototype

T-P0-16/12/10 可行性验证:CPU 基准 + 代理质检指标 + 合成图体型剖面法
原型 + REPORT.md 三问三答(Sprint 1 门槛的决策依据)。

偏差说明:模型齐全未触发降级(rtmpose-s/m + birefnet_lite fp32 全部首选
候选成功,hf-mirror 实测 4~12MB/s);huggingface_hub 不在 spike extra 且
禁新增依赖,改用 stdlib urllib 直连 resolve 端点(同 HTTP 语义);PINTO
model zoo 无法脚本化定位小模型、dhkim0115 仓不存在,姿态模型改用
bukuroo/RTMPose-ONNX;BiRefNet 官方仓无 ONNX,用 onnx-community 导出;
GPU 档未测(CPU 版 ORT 无 CUDA EP,按文档不折腾);BiRefNet 单张>3s 正式
次数自适应降为 10 并在输出注明;RTMPose 在合成剪影上置信度低(域差异),
体型断言按文档允许走真值定高路径,关键点路径仅统计参考一致率。
This commit is contained in:
xsl
2026-08-14 20:21:39 +08:00
parent d5f366fa4c
commit 446d7a9391
7 changed files with 1250 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
"""T9 SPIKE · RTMPose ONNX CPU 单张耗时基准(T-P0-16 验收口径)。
- 输入:scripts/spikes/assets/synthetic_person.png(synthetic.py 生成的合成人形,
不涉及真实人脸/人体数据);
- 口径:预热 3 + 正式 20,报「纯模型 session.run」与「预处理+推理+解码」两条链路
的均值 / P95 / 单次最大(time.perf_counter);
- GPU 档:先探测 nvidia-smi 与 ORT providers,CUDA EP 不可用则只报 CPU(不折腾);
- 降级:models/ 下无 RTMPose ONNX 时,用 cv2.HOG 人体检测作关键点占位并在输出声明
(HOG 无关键点,仅给检测框 + 按人体比例推的占位点,报告需注明);
- 输出:out/rtmpose_bench.json(耗时)与 out/rtmpose_keypoints.json(关键点)。
用法:.venv/bin/python scripts/spikes/rtmpose_benchmark.py
"""
from __future__ import annotations
import json
import sys
from typing import Any
import cv2
import numpy as np
from synthetic import ASSETS_DIR, ensure_assets
from vision_common import (
COCO17_NAMES,
IDX_HIP,
IDX_SHOULDER,
MODELS_DIR,
OUT_DIR,
POSE_CHAIN,
RTMPoseRunner,
bench,
dump_json,
gpu_probe,
load_session,
)
# GT 校验容差:合成图人形位置已知,肩/臀线估计偏差在该范围内视为「关键点可用」
LINE_TOLERANCE_PX = 15
CONF_USABLE = 0.30 # 关键点平均置信度低于该值视为「定位不可信」(软指标,仅提示)
def _print_gpu_verdict(gpu: dict[str, Any]) -> None:
print("== GPU 探测 ==")
print(f" nvidia-smi: {gpu['nvidia_smi'] or '不可用'}")
print(f" ORT providers: {gpu['ort_providers']}")
if gpu["cuda_ep_available"]:
print(" 结论:CUDA EP 可用(但本 spike 约定只装 CPU 版 ORT,仅报 CPU 档)")
else:
print(" 结论:CUDA EP 不可用 → 只测 CPU 档(任务文档:探测失败不折腾)")
def _print_keypoints(name: str, kps: np.ndarray) -> None:
print(f"\n== {name} 关键点(画布坐标 {kps.shape[0]} 点)==")
print(f" {'idx':>3} {'name':<14} {'x':>6} {'y':>6} {'conf':>6}")
for i, (x, y, c) in enumerate(kps):
kp_name = COCO17_NAMES[i] if i < len(COCO17_NAMES) else f"kp{i}"
print(f" {i:>3} {kp_name:<14} {x:>6.1f} {y:>6.1f} {c:>6.3f}")
def _check_against_gt(name: str, kps: np.ndarray, gt_std: dict[str, Any]) -> dict[str, Any]:
"""合成图真值校验:肩线/臀线高度与置信度(RTMPose 是否真的「看懂」了人形)。"""
gt_lines: dict[str, int] = gt_std["line_y"]
y_sh = float(np.mean(kps[list(IDX_SHOULDER), 1]))
y_hip = float(np.mean(kps[list(IDX_HIP), 1]))
d_sh = y_sh - gt_lines["shoulder"]
d_hip = y_hip - gt_lines["hip"]
conf_mean = float(np.mean(kps[:, 2]))
usable = (
abs(d_sh) <= LINE_TOLERANCE_PX
and abs(d_hip) <= LINE_TOLERANCE_PX
and conf_mean >= CONF_USABLE
)
print(
f" [GT 校验] 肩线偏差 {d_sh:+.1f}px(真值 {gt_lines['shoulder']}),"
f"臀线偏差 {d_hip:+.1f}px(真值 {gt_lines['hip']}),平均置信度 {conf_mean:.3f}"
f" → {'可用' if usable else '定位不可信(降级口径见 REPORT)'}"
)
return {
"shoulder_line_delta_px": round(d_sh, 1),
"hip_line_delta_px": round(d_hip, 1),
"conf_mean": round(conf_mean, 3),
"keypoints_usable": usable,
}
def _bench_model(runner: RTMPoseRunner, tensor: np.ndarray, img: np.ndarray) -> dict[str, Any]:
def fn_model() -> None:
runner.session.run(None, {runner.input_name: tensor})
def fn_full() -> None:
runner.infer(img)
return {
"model_only": bench(fn_model, warmup=3, runs=20),
"full_pipeline": bench(fn_full, warmup=3, runs=20),
}
def _hog_fallback(img: np.ndarray) -> int:
"""无 RTMPose 时的降级路径:HOG 人体检测作关键点占位(报告必须注明此口径)。"""
print("\n== 降级路径:cv2.HOG 人体检测(关键点占位,非 RTMPose)==")
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
def fn() -> None:
hog.detectMultiScale(img, winStride=(8, 8), padding=(4, 4), scale=1.05)
stats = bench(fn, warmup=2, runs=10)
found, _weights = hog.detectMultiScale(img, winStride=(8, 8), padding=(4, 4), scale=1.05)
if len(found):
x, y, w, h = found[0]
print(f" 检测到人体框: x={x} y={y} w={w} h={h};耗时 {stats}")
# 占位关键点:按人体比例从检测框推(HOG 不输出关键点,仅维持管线可跑通)
placeholder = {
"left_shoulder": [x + 0.2 * w, y + 0.25 * h],
"right_shoulder": [x + 0.8 * w, y + 0.25 * h],
"left_hip": [x + 0.3 * w, y + 0.55 * h],
"right_hip": [x + 0.7 * w, y + 0.55 * h],
}
else:
print(" 未检测到人体:占位关键点不可用")
placeholder = {}
dump_json(
OUT_DIR / "rtmpose_keypoints.json",
{
"source": "hog-placeholder",
"detections": [list(map(int, b)) for b in found],
"placeholder": placeholder,
},
)
dump_json(OUT_DIR / "rtmpose_bench.json", {"source": "hog-placeholder", "latency": stats})
return 0
def main() -> int:
OUT_DIR.mkdir(parents=True, exist_ok=True)
ensure_assets()
img = cv2.imread(str(ASSETS_DIR / "synthetic_person.png"))
gt = json.loads((ASSETS_DIR / "ground_truth.json").read_text(encoding="utf-8"))
gpu = gpu_probe()
_print_gpu_verdict(gpu)
available = [name for name in POSE_CHAIN if (MODELS_DIR / name).exists()]
if not available:
print("[降级] models/ 下无 RTMPose ONNX,改用 cv2.HOG 作关键点占位(见 REPORT)")
return _hog_fallback(img)
results: dict[str, Any] = {
"gpu": gpu,
"input": "synthetic_person.png (384x512 合成人形)",
"models": {},
}
keypoints_dump: dict[str, Any] = {}
for name in available:
path = MODELS_DIR / name
print(f"\n==== {name}({path.stat().st_size / 1048576:.1f} MB,CPUExecutionProvider)====")
runner = RTMPoseRunner(load_session(path))
print(f" 输入: {runner.input_name} {runner.input_hw}(H,W)")
kps = runner.infer(img)
_print_keypoints(name, kps)
gt_check = _check_against_gt(name, kps, gt["variants"]["STD"])
stats = _bench_model(runner, runner.preprocess(img), img)
for lane, s in stats.items():
print(
f" [{lane}] runs={s['runs']} 均值={s['mean_s'] * 1000:.1f}ms "
f"P95={s['p95_s'] * 1000:.1f}ms 最大={s['max_s'] * 1000:.1f}ms"
)
results["models"][name] = {
"input_hw": list(runner.input_hw),
"num_keypoints": int(kps.shape[0]),
"gt_check": gt_check,
"latency": stats,
}
keypoints_dump[name] = {
"names": list(COCO17_NAMES)[: kps.shape[0]],
"keypoints": [
[round(float(x), 1), round(float(y), 1), round(float(c), 3)] for x, y, c in kps
],
}
dump_json(OUT_DIR / "rtmpose_bench.json", results)
dump_json(OUT_DIR / "rtmpose_keypoints.json", keypoints_dump)
print(f"\n结果已写入 {OUT_DIR / 'rtmpose_bench.json'} 与 {OUT_DIR / 'rtmpose_keypoints.json'}")
return 0
if __name__ == "__main__":
sys.exit(main())