feat(接口6): 复刻接口1,新增 /api/v1/face/measure-v2
- 抽取 _face_measure_impl() 共用实现,接口1/6 零逻辑差异 - 接口6 路径 POST /api/v1/face/measure-v2 - 入参/出参与接口1 完全一致 - 文档同步更新(接口文档、实现说明、网关待改动) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -316,9 +316,71 @@ class ImageJsonBody(BaseModel):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 接口 1:四庭七眼测量标注
|
||||
# 接口 1/6 共用实现
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _face_measure_impl(image_file, image_url, image_base64):
|
||||
"""接口1/6 共用实现:四庭七眼测量 + 标注图生成。返回 (ok_dict, err_dict)。"""
|
||||
# 1. 三选一取图
|
||||
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
|
||||
if e is not None:
|
||||
return None, e
|
||||
|
||||
# 2. 大小校验(≤ 1MB)
|
||||
if len(raw) > MAX_FILE_BYTES:
|
||||
return None, err(1006, "文件超出 1 MB 限制")
|
||||
|
||||
# 3. 解码
|
||||
image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
|
||||
if image is None:
|
||||
return None, err(1008, "图片格式不支持(仅 JPG / PNG)")
|
||||
|
||||
# 4. 分辨率(短边/长边,方向无关,可配置门槛)
|
||||
h, w = image.shape[:2]
|
||||
short_side, long_side = min(w, h), max(w, h)
|
||||
if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE:
|
||||
return None, err(1002, "人像分辨率过低")
|
||||
|
||||
try:
|
||||
from face_analysis.detector import detector
|
||||
from face_analysis.pose import estimate_head_pose, check_frontal_face
|
||||
from face_analysis.measure import measure_face
|
||||
from face_analysis.annotation import create_annotated_image
|
||||
|
||||
# 5. 人脸检测
|
||||
landmarks = detector.detect(image)
|
||||
if landmarks is None:
|
||||
return None, err(1001, "无法识别人像")
|
||||
|
||||
# 6. 姿态校验
|
||||
if not check_frontal_face(landmarks, w, h):
|
||||
return None, err(1003, "角度问题,请上传正面照")
|
||||
head_pose = estimate_head_pose(landmarks, w, h)
|
||||
|
||||
# 7. 头发分割(方案 B),失败传 None 由 measure 内部回退方案 A
|
||||
hair_mask = None
|
||||
try:
|
||||
from face_analysis.hair_segmenter import get_segmenter
|
||||
hair_mask = get_segmenter().segment_hair(image)
|
||||
except Exception as seg_e: # noqa: BLE001
|
||||
logger.warning("头发分割失败,回退方案A:%s", seg_e)
|
||||
|
||||
# 8. 测量 + 标注图
|
||||
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
||||
annotated = create_annotated_image(image, result)
|
||||
buf = BytesIO()
|
||||
annotated.save(buf, format="PNG")
|
||||
|
||||
# 9. 拆分架构:返回 base64,不落盘不拼 URL(落盘改 URL 由网关完成)
|
||||
data = result.to_response()
|
||||
data["annotated_image_base64"] = base64.b64encode(buf.getvalue()).decode()
|
||||
return ok(data), None
|
||||
except Exception as ex: # noqa: BLE001
|
||||
logger.exception("接口1/6 处理异常")
|
||||
return None, err(1007, f"处理失败:{ex}")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/api/v1/face/measure",
|
||||
summary="接口1 四庭七眼测量标注",
|
||||
@@ -405,63 +467,106 @@ async def face_measure(
|
||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||
):
|
||||
# 1. 三选一取图
|
||||
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
|
||||
if e is not None:
|
||||
return e
|
||||
"""接口1:四庭七眼测量标注"""
|
||||
ok_data, err_data = await _face_measure_impl(image_file, image_url, image_base64)
|
||||
return ok_data if ok_data is not None else err_data
|
||||
|
||||
# 2. 大小校验(≤ 1MB)
|
||||
if len(raw) > MAX_FILE_BYTES:
|
||||
return err(1006, "文件超出 1 MB 限制")
|
||||
|
||||
# 3. 解码
|
||||
image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
|
||||
if image is None:
|
||||
return err(1008, "图片格式不支持(仅 JPG / PNG)")
|
||||
# ---------------------------------------------------------------------------
|
||||
# 接口 6:四庭七眼测量标注 v2(复刻接口1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 4. 分辨率(短边/长边,方向无关,可配置门槛)
|
||||
h, w = image.shape[:2]
|
||||
short_side, long_side = min(w, h), max(w, h)
|
||||
if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE:
|
||||
return err(1002, "人像分辨率过低")
|
||||
@app.post(
|
||||
"/api/v1/face/measure-v2",
|
||||
summary="接口6 四庭七眼测量标注 v2",
|
||||
tags=["人脸分析"],
|
||||
description=f"""
|
||||
输入用户正面照,返回:
|
||||
- 标注好四庭七眼数据的 **PNG 图片**(仅标注图层,不含人物)
|
||||
- 四庭(顶庭/上庭/中庭/下庭)各段**厘米数值及占比**
|
||||
- 七眼(眼宽/脸宽/两眼间距)**厘米数值及占比**
|
||||
- 五个关键分界点的**原图像素坐标**(头顶/发际线/眉心/鼻翼下缘/下巴尖)
|
||||
|
||||
try:
|
||||
from face_analysis.detector import detector
|
||||
from face_analysis.pose import estimate_head_pose, check_frontal_face
|
||||
from face_analysis.measure import measure_face
|
||||
from face_analysis.annotation import create_annotated_image
|
||||
功能与接口1 完全一致,复刻实现。
|
||||
|
||||
# 5. 人脸检测
|
||||
landmarks = detector.detect(image)
|
||||
if landmarks is None:
|
||||
return err(1001, "无法识别人像")
|
||||
{_image_fields_desc}
|
||||
|
||||
# 6. 姿态校验
|
||||
if not check_frontal_face(landmarks, w, h):
|
||||
return err(1003, "角度问题,请上传正面照")
|
||||
head_pose = estimate_head_pose(landmarks, w, h)
|
||||
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`),或 JSON Body 传 `image_url` / `image_base64`。
|
||||
|
||||
# 7. 头发分割(方案 B),失败传 None 由 measure 内部回退方案 A
|
||||
hair_mask = None
|
||||
try:
|
||||
from face_analysis.hair_segmenter import get_segmenter
|
||||
hair_mask = get_segmenter().segment_hair(image)
|
||||
except Exception as seg_e: # noqa: BLE001
|
||||
logger.warning("头发分割失败,回退方案A:%s", seg_e)
|
||||
---
|
||||
|
||||
# 8. 测量 + 标注图
|
||||
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
||||
annotated = create_annotated_image(image, result)
|
||||
buf = BytesIO()
|
||||
annotated.save(buf, format="PNG")
|
||||
**坐标说明**:所有坐标以原图像素为基准,原点为图片左上角,x 向右,y 向下。
|
||||
|
||||
# 9. 拆分架构:返回 base64,不落盘不拼 URL(落盘改 URL 由网关完成)
|
||||
data = result.to_response()
|
||||
data["annotated_image_base64"] = base64.b64encode(buf.getvalue()).decode()
|
||||
return ok(data)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
logger.exception("接口1 处理异常")
|
||||
return err(1007, f"处理失败:{ex}")
|
||||
**标注图片 UI 规范**(真实版本生效):
|
||||
- 字体/线条颜色:`#FFFFFF 100%`
|
||||
- 四庭数值在图片**左侧**呈现,七眼间距**上下穿插**展示
|
||||
- 字体:PingFangSC-Regular 10pt,线宽 1pt
|
||||
- 横线/竖线渐变消失,虚线两侧带箭头
|
||||
""",
|
||||
responses={
|
||||
200: {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"request_id": "mock-request-id",
|
||||
"data": {
|
||||
"annotated_image_url": SAMPLE_IMAGE_URL,
|
||||
"face_total_height_cm": 13.76,
|
||||
"four_courts": {
|
||||
"top_court_cm": 3.44,
|
||||
"upper_court_cm": 3.44,
|
||||
"middle_court_cm": 3.44,
|
||||
"lower_court_cm": 3.44,
|
||||
"ratios": {
|
||||
"top_court": 0.25,
|
||||
"upper_court": 0.25,
|
||||
"middle_court": 0.25,
|
||||
"lower_court": 0.25,
|
||||
},
|
||||
},
|
||||
"seven_eyes": {
|
||||
"eye_width_cm": 3.44,
|
||||
"face_width_cm": 24.08,
|
||||
"inter_eye_distance_cm": 3.44,
|
||||
"ratios": {"eye_width": 0.143, "inter_eye_distance": 0.143},
|
||||
},
|
||||
"landmarks": {
|
||||
"hair_top": {"x": 540, "y": 120},
|
||||
"hairline": {"x": 540, "y": 430},
|
||||
"brow_center": {"x": 540, "y": 740},
|
||||
"nose_bottom": {"x": 540, "y": 1050},
|
||||
"chin_tip": {"x": 540, "y": 1360},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
400: {
|
||||
"description": "参数错误 / 图片识别失败",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"图片参数错误": {"value": {"code": 1007, "message": "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个", "request_id": "x", "data": None}},
|
||||
"无法识别人像": {"value": {"code": 1001, "message": "无法识别人像", "request_id": "x", "data": None}},
|
||||
"多张人脸": {"value": {"code": 1005, "message": "检测到多张人脸,仅支持单人照片", "request_id": "x", "data": None}},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
async def face_measure_v2(
|
||||
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"),
|
||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||
):
|
||||
"""接口6:四庭七眼测量标注 v2(复刻接口1)"""
|
||||
ok_data, err_data = await _face_measure_impl(image_file, image_url, image_base64)
|
||||
return ok_data if ok_data is not None else err_data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user