feat: 新增42号人脸特征与72号B端关键点接口

This commit is contained in:
xsl
2026-11-07 20:35:55 +08:00
parent ccb9c142c4
commit c9a47a76c9
16 changed files with 983 additions and 25 deletions
+94 -1
View File
@@ -527,6 +527,12 @@ async def hair_grow_b(request: Request):
return await _proxy(request, "/api/v1/hair/grow-b")
@app.post("/api/v1/face/b-end-points", summary="72号接口:B 端五个关键点", tags=["人脸分析"])
async def face_b_end_points(request: Request):
"""返回扶正裁切后的图片和透明标注层。"""
return await _proxy(request, "/api/v1/face/b-end-points")
@app.post("/api/v1/face/features", tags=["人脸分析"])
async def face_features(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG)"),
@@ -534,6 +540,42 @@ async def face_features(
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带前缀)"),
):
"""接口4:用户特征分析 — 本机直接调豆包视觉模型,不经过 worker。"""
return await _face_features_response(image_file, image_url, image_base64)
@app.post(
"/api/v1/face/features-42",
summary="42号接口-加两个特征点",
tags=["人脸分析"],
)
async def face_features_42(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG)"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带前缀)"),
):
"""沿用接口 4。图片参数 `image_file`、`image_url`、`image_base64` 必须且只能传一个。
先检查原图人脸完整性和俯拍角度,再按接口 52 扶正裁切。俯拍角度超过服务端
`FRONTAL_PITCH_THR`(默认 30°)时拦截。
| 业务码 | 情况 |
|---|---|
| 1001 | 未检测到人脸 |
| 1003 | 人脸不完整、俯拍角度过大或无法扶正裁切 |
| 1008 | 图片 URL 下载失败、Base64 解码失败或图片无法读取 |
| 1007 | 图片参数错误、预处理内部异常或视觉模型调用失败 |
业务失败使用 HTTP 200,返回 `code`、`message`、`request_id` 和 `data: null`。
同一业务码下的具体原因见 `message`,响应不含独立的质检原因或角度字段。
成功时 `data.features` 为 JSON 字符串,在接口 4 的字段上新增
`zygoma_expansion`(颧弓外扩)和 `temple_hollowing`(太阳穴凹陷)。
Ft ÷ Zy 小于 0.8 时两项均为 true,否则均为 false,无法测量时均为 null。
"""
return await _face_features_response(image_file, image_url, image_base64, include_ratios=True)
async def _face_features_response(image_file, image_url, image_base64, *, include_ratios=False):
import uuid as _uuid
# 三选一校验
@@ -562,6 +604,25 @@ async def face_features(
from fastapi.concurrency import run_in_threadpool
from face_features import analyze_features
ratio_features = {}
if include_ratios:
from face_analysis.face_ratios import PortraitQualityError
try:
img_bytes, ratio_features = await _prepare_features_42(img_bytes, image_url)
except PortraitQualityError as ex:
return JSONResponse(status_code=200, content={
"code": ex.code, "message": str(ex),
"request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None,
})
except Exception:
logger.exception("接口42 图片预处理失败")
return JSONResponse(status_code=200, content={
"code": 1007, "message": "图片预处理失败",
"request_id": f"gw-{_uuid.uuid4().hex[:8]}", "data": None,
})
image_url = None
try:
feats = await run_in_threadpool(analyze_features, img_bytes, image_url)
except Exception as ex:
@@ -581,10 +642,42 @@ async def face_features(
"code": 0,
"message": "success",
"request_id": f"gw-{_uuid.uuid4().hex[:8]}",
"data": {"features": json.dumps(feats, ensure_ascii=False)},
"data": {"features": json.dumps({**feats, **ratio_features}, ensure_ascii=False)},
})
async def _prepare_features_42(image_bytes, image_url):
from fastapi.concurrency import run_in_threadpool
if image_url:
import httpx
try:
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
response = await client.get(image_url)
response.raise_for_status()
image_bytes = response.content
except httpx.HTTPError as exc:
from face_analysis.face_ratios import PortraitQualityError
raise PortraitQualityError(1008, "图片下载失败(URL 不可达)") from exc
async with _if99_lock:
return await run_in_threadpool(_preprocess_and_measure_42, image_bytes)
def _preprocess_and_measure_42(image_bytes):
import cv2
from face_analysis.face_ratios import analyze_ratio_features, prepare_feature_portrait
image, landmarks = prepare_feature_portrait(image_bytes)
success, encoded = cv2.imencode(".png", image)
if not success:
raise RuntimeError("处理后图片编码失败")
ratios = analyze_ratio_features(image, landmarks)
return encoded.tobytes(), ratios
@app.post("/api/v1/hairline/generate", tags=["人脸分析"])
async def hairline_generate(request: Request):
"""接口5:发际线PNG生成"""