feat: 新增42号人脸特征与72号B端关键点接口
This commit is contained in:
+94
-1
@@ -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生成"""
|
||||
|
||||
Reference in New Issue
Block a user