Files
hair/tests/test_pipeline.py
T
xslandClaude Opus 4.8 8d3b145111 feat(worker): 接口1 四庭七眼测量真实实现(替换 Mock)
worker 侧从 Mock 替换为真实算法:
- face_analysis 包:detector(MediaPipe 478点) / pose(solvePnP 姿态) /
  calibration(虹膜直径法) / hair_segmenter+bisenet_model(方案B 头发分割) /
  measure(方案A兜底+B/A决策+七眼+换算) / annotation(numpy渐变线+中文标注)
- app.py:/api/v1/face/measure 接真实实现,返回 annotated_image_base64
  (不落盘不拼URL,落盘由网关做);加 X-Internal-Token 鉴权、/health 就绪态、
  可配置分辨率门槛、异常兜底
- 部署:start.sh/run_worker.sh/hair-worker.service 监听 8187;worker_config 示例
- 测试 tests/:Tier1合成真值<1e-6 + Tier2缩放不变 + Tier3叠加 + 错误码集成 +
  数值回归,pytest 24 项全绿
- 文档补实测基线表 + RTX5090/torch 说明

注:worker 为 RTX 5090(sm_120),pinned torch 2.2.2(cu121) 只到 sm_90,
BiSeNet 已自动回退 CPU(方案B 正常);要用 GPU 需换 torch cu128(≥2.7)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:07:28 +08:00

79 lines
3.1 KiB
Python

"""Tier 2 缩放不变性 + Tier 3 落点可视化 + 数值回归(均走方案 A,torch 无关、确定性)。"""
import os
import cv2
import numpy as np
from conftest import fixture, OUTPUT
from face_analysis.detector import detector
from face_analysis.measure import measure_face
def _run(img):
h, w = img.shape[:2]
lms = detector.detect(img)
assert lms is not None
# 固定走方案 A(mask=None),保证确定性与 torch 无关
return measure_face(lms, None, w, h)
def test_scale_invariance():
"""等比放大 2×:占比几乎不变(±0.5%),cm 近似不变(±2%)。"""
img = cv2.imread(fixture("frontal.jpg"))
big = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
r1, r2 = _run(img), _run(big)
cm1 = {"top": r1.top_cm, "upper": r1.upper_cm, "middle": r1.middle_cm, "lower": r1.lower_cm}
cm2 = {"top": r2.top_cm, "upper": r2.upper_cm, "middle": r2.middle_cm, "lower": r2.lower_cm}
d1, d2 = r1.to_response(), r2.to_response()
for k in ["top_court", "upper_court", "middle_court", "lower_court"]:
a = d1["four_courts"]["ratios"][k]
b = d2["four_courts"]["ratios"][k]
assert abs(a - b) < 0.005, f"ratio {k} 漂移过大: {a} vs {b}"
for k in ["top", "upper", "middle", "lower"]:
assert abs(cm1[k] - cm2[k]) / cm1[k] < 0.02, f"cm {k} 漂移过大: {cm1[k]} vs {cm2[k]}"
def test_landmark_overlay():
"""生成 5 纵向点叠加图供人工核验,并断言坐标在图内且自上而下有序。"""
img = cv2.imread(fixture("frontal.jpg"))
h, w = img.shape[:2]
r = _run(img)
pts = r.to_response()["landmarks"]
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
ys = [pts[n]["y"] for n in order]
# 坐标在图像范围内
for n in order:
assert 0 <= pts[n]["x"] <= w
assert 0 <= pts[n]["y"] <= h
# 自上而下严格递增
assert ys == sorted(ys), f"纵向点未自上而下有序: {ys}"
# dump 叠加图
canvas = img.copy()
for n in order:
cv2.circle(canvas, (pts[n]["x"], pts[n]["y"]), 4, (0, 0, 255), -1)
cv2.line(canvas, (0, pts[n]["y"]), (w, pts[n]["y"]), (0, 255, 0), 1)
cv2.imwrite(os.path.join(OUTPUT, "frontal_landmarks.png"), canvas)
# 数值回归基线:frontal.jpg 方案A 首次实测值,防重构回归(容差 1%)。
_BASELINE = {
"face_total_height_cm": 24.01,
"top_court_cm": 5.06, "upper_court_cm": 6.07,
"middle_court_cm": 7.23, "lower_court_cm": 5.64,
"eye_width_cm": 2.52, "face_width_cm": 12.49, "inter_eye_distance_cm": 3.24,
}
def test_numeric_regression():
img = cv2.imread(fixture("frontal.jpg"))
d = _run(img).to_response()
got = {
"face_total_height_cm": d["face_total_height_cm"],
**{k: d["four_courts"][k] for k in
["top_court_cm", "upper_court_cm", "middle_court_cm", "lower_court_cm"]},
**{k: d["seven_eyes"][k] for k in
["eye_width_cm", "face_width_cm", "inter_eye_distance_cm"]},
}
for k, base in _BASELINE.items():
assert abs(got[k] - base) / base < 0.01, f"{k} 回归: 基线 {base}, 实测 {got[k]}"