feat(接口5): 发际线PNG生成(真实实现,替换Mock)
复用接口2 预览管线:gender 必填 → 该性别全部发际线叠加图(同接口2预览) +
最佳(order=1)发际线曲线的面部中间点坐标。无生发(不调 ComfyUI)。
- hairline/service.py: generate_hairline_pngs——N张发际线叠图 +
best_center(面部中轴眉心x × order1曲线在该处的y)
- app.py: /hairline/generate 真实实现,新增 gender 必填(非法→1004),
返回 hairline_images[].image_base64 + best_hairline_center_point;
无人脸→1001;重活线程池
- 接口文档/OpenAPI: 接口5 新增 gender 入参 + 输出改 base64(网关改url)
- 测试: test_api 接口5(gender必填/N张/center点),44全绿
实测(5090): female 5张叠图 + center{x,y},~2.5s(无Flux)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -721,10 +721,15 @@ async def face_features(
|
||||
|
||||
---
|
||||
|
||||
**入参**:新增必填 `gender`(`male`/`female`),决定返回的发际线集合(female 5 / male 4)。
|
||||
|
||||
**返回说明**:
|
||||
|
||||
- `hairline_images`:发际线 PNG 列表,数量 N 不固定,已按合适度**从高到低排序**(`order=1` 最合适)
|
||||
- `best_hairline_center_point`:最合适发际线的面部中间点坐标,以**原图像素**为基准(左上角为原点,x 向右,y 向下)
|
||||
- `hairline_images`:发际线叠加图列表(发际线曲线叠加在用户照片上,同接口2预览),
|
||||
数量 = 该性别的发际线类型数,本期按贴图顺序 `order=1..N`(暂不计算合适度)。
|
||||
worker 返回 `image_base64`,网关落盘后改写为 `image_url`。
|
||||
- `best_hairline_center_point`:最佳(`order=1`)发际线曲线的**面部中间点**坐标,
|
||||
以**原图像素**为基准(左上角为原点,x 向右,y 向下)。
|
||||
""",
|
||||
responses={
|
||||
200: {
|
||||
@@ -737,8 +742,8 @@ async def face_features(
|
||||
"request_id": "mock-request-id",
|
||||
"data": {
|
||||
"hairline_images": [
|
||||
{"image_url": SAMPLE_IMAGE_URL, "order": 1},
|
||||
{"image_url": SAMPLE_IMAGE_URL, "order": 2},
|
||||
{"image_base64": "iVBORw0KGgo...", "order": 1},
|
||||
{"image_base64": "iVBORw0KGgo...", "order": 2},
|
||||
],
|
||||
"best_hairline_center_point": {"x": 540, "y": 430},
|
||||
},
|
||||
@@ -760,15 +765,50 @@ async def hairline_generate(
|
||||
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, 前缀)"),
|
||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
||||
):
|
||||
data = {
|
||||
"hairline_images": [
|
||||
{"image_url": SAMPLE_IMAGE_URL, "order": 1},
|
||||
{"image_url": SAMPLE_IMAGE_URL, "order": 2},
|
||||
],
|
||||
"best_hairline_center_point": {"x": 540, "y": 430},
|
||||
}
|
||||
return ok(data)
|
||||
if gender not in ("male", "female"):
|
||||
return err(1004, "gender 必填且只能为 male / female")
|
||||
|
||||
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
|
||||
if e is not None:
|
||||
return e
|
||||
if len(raw) > MAX_FILE_BYTES:
|
||||
return err(1006, "文件超出 1 MB 限制")
|
||||
|
||||
image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
|
||||
if image is None:
|
||||
return err(1008, "图片格式不支持(仅 JPG / PNG)")
|
||||
|
||||
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, "人像分辨率过低")
|
||||
|
||||
try:
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from hairline.service import generate_hairline_pngs
|
||||
|
||||
res = await run_in_threadpool(generate_hairline_pngs, image, gender)
|
||||
if res is None:
|
||||
return err(1001, "无法识别人像")
|
||||
|
||||
hairline_images = []
|
||||
for it in res["images"]:
|
||||
_ok, png = cv2.imencode(".png", it["image_bgr"])
|
||||
hairline_images.append({
|
||||
"image_base64": base64.b64encode(png.tobytes()).decode(),
|
||||
"order": it["order"],
|
||||
})
|
||||
c = res["best_center"]
|
||||
data = {
|
||||
"hairline_images": hairline_images,
|
||||
"best_hairline_center_point": ({"x": c[0], "y": c[1]} if c else None),
|
||||
}
|
||||
return ok(data)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
logger.exception("接口5 处理异常")
|
||||
return err(1007, f"处理失败:{ex}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user