feat(接口3): B端生发-马克笔发际线检测+生发(替换Mock)

医生在额头用马克笔画规划发际线 → 检测该线 → 生发。检测算法源自 /home/xsl/headmark。

- hairline/marker_detect.py: 黑帽响应图(MORPH_BLACKHAT)+鬓角锚点(MediaPipe 21/251吸附)
  +skimage route_through_array 最小路径检测画线;路径平均响应阈值拒识无画线
  (headmark 调研:全局灰度阈值不可用,黑帽+Dijkstra 实测误差≤0.5px)
- hairline/mask.py: 抽出 mask_from_curve(曲线+ROI闭合),接口2/3共用
- hairline/service.py: generate_grow_b——检测→遮罩→原图重画干净线→ComfyUI生发
- app.py: /hair/grow-b 真实实现,marked+original各三选一+校验;输出
  best_hairline_image_base64(=原图)/hair_growth_image_base64/hairline_type="custom";
  无人脸或未检测到画线→1001;重活进线程池
- requirements: scikit-image==0.24.0 (⚠️锁0.24,0.25+强依赖numpy>=2会顶掉mediapipe的numpy<2)
- 文档: docs/接口3-B端生发-技术实现方案.md
- 测试: test_marker.py(检测/拒识/辅助) + test_api grow-b(mock ComfyUI),42全绿

实测(5090): grow-b ~6.4s,生发图把额头发际线补到医生画线、清除划线、人物保持。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-06-15 00:08:05 +08:00
co-authored by Claude Opus 4.8
parent 94ad95850e
commit ce95a508c1
9 changed files with 370 additions and 23 deletions
+43 -9
View File
@@ -559,9 +559,9 @@ async def hair_grow(
"message": "success",
"request_id": "mock-request-id",
"data": {
"best_hairline_image_url": SAMPLE_IMAGE_URL,
"hair_growth_image_url": SAMPLE_IMAGE_URL,
"hairline_type": "花瓣形",
"best_hairline_image_base64": "iVBORw0KGgo...(原图)",
"hair_growth_image_base64": "iVBORw0KGgo...(生发图)",
"hairline_type": "custom",
},
}
}
@@ -585,12 +585,46 @@ async def hair_grow_b(
original_image_url: Optional[str] = Form(default=None, description="原始用户照片 URL"),
original_image_base64: Optional[str] = Form(default=None, description="原始用户照片 base64"),
):
data = {
"best_hairline_image_url": SAMPLE_IMAGE_URL,
"hair_growth_image_url": SAMPLE_IMAGE_URL,
"hairline_type": "花瓣形",
}
return ok(data)
# 1. 两组图各三选一取图
marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64)
if e is not None:
return e
orig_raw, e = await resolve_image_bytes(original_image_file, original_image_url, original_image_base64)
if e is not None:
return e
if len(marked_raw) > MAX_FILE_BYTES or len(orig_raw) > MAX_FILE_BYTES:
return err(1006, "文件超出 1 MB 限制")
marked = cv2.imdecode(np.frombuffer(marked_raw, np.uint8), cv2.IMREAD_COLOR)
original = cv2.imdecode(np.frombuffer(orig_raw, np.uint8), cv2.IMREAD_COLOR)
if marked is None or original is None:
return err(1008, "图片格式不支持(仅 JPG / PNG)")
h, w = marked.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_grow_b
res = await run_in_threadpool(generate_grow_b, marked, original)
if res["status"] == "no_face":
return err(1001, "无法识别人像")
if res["status"] == "no_line":
return err(1001, "未检测到发际线划线,请确认划线图额头有清晰的手绘发际线")
grown_b64 = base64.b64encode(res["grown_png"]).decode() if res["grown_png"] else None
data = {
"best_hairline_image_base64": base64.b64encode(orig_raw).decode(), # 原图原样
"hair_growth_image_base64": grown_b64,
"hairline_type": "custom",
}
return ok(data)
except Exception as ex: # noqa: BLE001
logger.exception("接口3 处理异常")
return err(1007, f"处理失败:{ex}")
# ---------------------------------------------------------------------------