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
+38 -1
View File
@@ -17,7 +17,8 @@ from .face_parsing import FaceParser
from .hairline_2d import sample_hairline, smooth_hairline
from .lift_3d import lift_hairline_to_3d, build_middle_row, assemble_full
from .render import load_ext_mesh, load_texture_rgba, render_hairline_overlay
from .mask import build_inpaint_mask, compose_comfy_rgba
from .mask import build_inpaint_mask, compose_comfy_rgba, mask_from_curve
from .marker_detect import detect_marker_hairline, path_to_curve_mask
import io
import logging
@@ -160,6 +161,42 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str):
return results
def generate_grow_b(marked_bgr: np.ndarray, original_bgr: np.ndarray):
"""接口3:检测医生手绘发际线 → 遮罩 → 原图重画干净线 → ComfyUI 生发。
Returns: {"grown_png": bytes 或 None, "status": "ok"|"no_face"|"no_line"}。
"""
rgb = cv2.cvtColor(marked_bgr, cv2.COLOR_BGR2RGB)
landmarks = get_landmarker().detect(rgb)
if landmarks is None:
return {"grown_png": None, "status": "no_face"}
parse_map = get_parser().parse(rgb)
path = detect_marker_hairline(marked_bgr, landmarks, parse_map)
if path is None:
return {"grown_png": None, "status": "no_line"}
h, w = marked_bgr.shape[:2]
# 原图对齐到 marked 坐标系(同一张照片的原始版/划线版,尺寸应一致)
orig = original_bgr
if orig.shape[:2] != (h, w):
orig = cv2.resize(orig, (w, h), interpolation=cv2.INTER_AREA)
# 在原图上重画干净黑线(膨胀成笔迹宽度),替代医生手绘的毛刺
line_w = max(2, int(w * 0.006))
marked_clean = orig.copy()
cv2.polylines(marked_clean, [path[:, ::-1].reshape(-1, 1, 2)], False,
(0, 0, 0), line_w, lineType=cv2.LINE_AA)
# 遮罩:检测路径曲线 + ROI 闭合
curve_mask = path_to_curve_mask(path, h, w, thickness=max(3, line_w))
mask = mask_from_curve(curve_mask, landmarks, parse_map)
buf = io.BytesIO()
compose_comfy_rgba(marked_clean, mask).save(buf, format="PNG")
grown_png = comfyui.run(buf.getvalue())
return {"grown_png": grown_png, "status": "ok"}
if __name__ == "__main__":
import sys
g = sys.argv[2] if len(sys.argv) > 2 else "female"