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>
86 lines
3.1 KiB
Python
86 lines
3.1 KiB
Python
"""尺度校准:像素 → 厘米(虹膜直径法,眼宽降级)。
|
|
|
|
人类虹膜直径高度稳定(成人平均 11.7mm),作为天然标尺把像素距离换算成厘米。
|
|
虹膜点(索引 469/471、474/476)需 refine_landmarks=True 才输出;缺失时降级用
|
|
眼宽(外→内眼角,均值约 2.85cm)。详见技术方案 §3。
|
|
"""
|
|
from face_analysis.face_mesh_landmarks import (
|
|
IRIS_LEFT_LEFT, IRIS_LEFT_RIGHT, IRIS_RIGHT_LEFT, IRIS_RIGHT_RIGHT,
|
|
LEFT_EYE_OUTER, LEFT_EYE_INNER, RIGHT_EYE_INNER, RIGHT_EYE_OUTER,
|
|
)
|
|
|
|
AVG_IRIS_DIAMETER_CM = 1.17 # 虹膜平均直径 11.7mm
|
|
AVG_EYE_WIDTH_CM = 2.85 # 眼裂平均宽度约 28.5mm(降级标尺)
|
|
|
|
|
|
def _lm_list(landmarks):
|
|
"""兼容 NormalizedLandmarkList(有 .landmark)与裸 list 两种入参。"""
|
|
return landmarks.landmark if hasattr(landmarks, "landmark") else landmarks
|
|
|
|
|
|
def normalized_to_pixel(landmark, image_width, image_height):
|
|
"""归一化坐标 → 像素坐标。"""
|
|
return landmark.x * image_width, landmark.y * image_height
|
|
|
|
|
|
def pixel_distance(p1, p2):
|
|
"""两点像素欧氏距离。"""
|
|
return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5
|
|
|
|
|
|
def _iris_diameter_px(lm, w, h):
|
|
"""左右虹膜直径像素均值;任一边缘点缺失/为 0 返回 None。"""
|
|
try:
|
|
ll = normalized_to_pixel(lm[IRIS_LEFT_LEFT], w, h)
|
|
lr = normalized_to_pixel(lm[IRIS_LEFT_RIGHT], w, h)
|
|
rl = normalized_to_pixel(lm[IRIS_RIGHT_LEFT], w, h)
|
|
rr = normalized_to_pixel(lm[IRIS_RIGHT_RIGHT], w, h)
|
|
except (IndexError, KeyError):
|
|
return None
|
|
left_d = pixel_distance(ll, lr)
|
|
right_d = pixel_distance(rl, rr)
|
|
if left_d <= 0 or right_d <= 0:
|
|
return None
|
|
return (left_d + right_d) / 2
|
|
|
|
|
|
def _eye_width_px(lm, w, h):
|
|
"""左右眼宽(外→内眼角)像素均值,作为虹膜降级标尺。"""
|
|
l = pixel_distance(normalized_to_pixel(lm[LEFT_EYE_OUTER], w, h),
|
|
normalized_to_pixel(lm[LEFT_EYE_INNER], w, h))
|
|
r = pixel_distance(normalized_to_pixel(lm[RIGHT_EYE_OUTER], w, h),
|
|
normalized_to_pixel(lm[RIGHT_EYE_INNER], w, h))
|
|
return (l + r) / 2
|
|
|
|
|
|
def estimate_scale_factor(landmarks, image_width, image_height):
|
|
"""估算 px_per_cm(每厘米对应像素数)。
|
|
|
|
优先用虹膜直径法;虹膜点不可用时降级用眼宽。返回正浮点数。
|
|
"""
|
|
lm = _lm_list(landmarks)
|
|
iris_px = _iris_diameter_px(lm, image_width, image_height)
|
|
if iris_px is not None:
|
|
return iris_px / AVG_IRIS_DIAMETER_CM
|
|
# 降级:眼宽法
|
|
eye_px = _eye_width_px(lm, image_width, image_height)
|
|
return eye_px / AVG_EYE_WIDTH_CM
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
import cv2
|
|
from face_analysis.detector import detector
|
|
|
|
path = sys.argv[1] if len(sys.argv) > 1 else "tests/fixtures/frontal.jpg"
|
|
img = cv2.imread(path)
|
|
if img is None:
|
|
print(f"无法读取图片: {path}")
|
|
sys.exit(1)
|
|
h, w = img.shape[:2]
|
|
lms = detector.detect(img)
|
|
if lms is None:
|
|
print("未检出人脸")
|
|
sys.exit(1)
|
|
print(f"px_per_cm: {estimate_scale_factor(lms, w, h):.4f}")
|