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 在合成剪影上置信度低(域差异), 体型断言按文档允许走真值定高路径,关键点路径仅统计参考一致率。
191 lines
7.3 KiB
Python
191 lines
7.3 KiB
Python
"""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())
|