Files
hair/hairline/marker_detect.py
xslandClaude Opus 4.8 ce95a508c1 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>
2026-06-15 00:08:05 +08:00

103 lines
4.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""接口3:马克笔手绘发际线检测(黑帽响应图 + 端点锚定 Dijkstra 最小路径)。
源自 /home/xsl/headmark 调研结论:全局灰度阈值不可用(笔迹平均灰度反高于阈值、
与皮肤阴影分布重叠);黑帽变换响应"比局部邻域暗的细结构",叠加 ROI + 两鬓角锚点间
最小代价路径,对抬头纹/眉毛/发丝鲁棒。复用接口2 的 ROI(额头上部 ∩ 头部分割)。
"""
from __future__ import annotations
import cv2
import numpy as np
from skimage.graph import route_through_array
from .mask import forehead_upper_region, head_silhouette
# 鬓角锚点(MediaPipe canonical 索引):21 左、251 右
ANCHOR_LEFT = 21
ANCHOR_RIGHT = 251
# 拒识阈值:路径平均黑帽响应低于此值 → 判"未检测到画线"(待真实图标定)
MIN_MEAN_RESPONSE = 8.0
def _blackhat(gray: np.ndarray, w: int) -> np.ndarray:
k = max(15, int(w * 0.025) | 1)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k))
return cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, kernel).astype(np.float32)
def _snap_anchor(bh_roi: np.ndarray, x: int, y: int, w: int):
"""在 (x,y) 周围窗口内吸附到黑帽响应最大处,返回 (row, col)。"""
win = max(8, int(w * 0.03))
h, ww = bh_roi.shape
x0, x1 = max(0, x - win), min(ww, x + win)
y0, y1 = max(0, y - win), min(h, y + win)
sub = bh_roi[y0:y1, x0:x1]
if sub.size == 0 or sub.max() <= 0:
return (int(np.clip(y, 0, h - 1)), int(np.clip(x, 0, ww - 1)))
dy, dx = np.unravel_index(int(np.argmax(sub)), sub.shape)
return (y0 + dy, x0 + dx)
def detect_marker_hairline(marked_bgr: np.ndarray, landmarks_mp: np.ndarray,
parse_map: np.ndarray, min_mean_response: float = MIN_MEAN_RESPONSE):
"""检测手绘发际线,返回路径 (N,2) row,col;未检出/被拒识返回 None。"""
h, w = marked_bgr.shape[:2]
roi = cv2.bitwise_and(forehead_upper_region(landmarks_mp, w, h),
head_silhouette(parse_map)) > 0
if roi.sum() == 0:
return None
gray = cv2.cvtColor(marked_bgr, cv2.COLOR_BGR2GRAY)
bh = _blackhat(gray, w)
bh_roi = bh * roi
al = _snap_anchor(bh_roi, int(landmarks_mp[ANCHOR_LEFT, 0] * w),
int(landmarks_mp[ANCHOR_LEFT, 1] * h), w)
ar = _snap_anchor(bh_roi, int(landmarks_mp[ANCHOR_RIGHT, 0] * w),
int(landmarks_mp[ANCHOR_RIGHT, 1] * h), w)
cost = (bh.max() - bh) + 1.0
cost[~roi] = 1e6 # 禁止路径走出 ROI
path, _ = route_through_array(cost, al, ar, fully_connected=True, geometric=True)
path = np.asarray(path)
# 拒识:路径平均黑帽响应过低 → 没画线(强行找出的伪路径)
if float(bh[path[:, 0], path[:, 1]].mean()) < min_mean_response:
return None
return path
def path_to_curve_mask(path: np.ndarray, h: int, w: int, thickness: int = 3) -> np.ndarray:
"""把路径画成曲线 maskuint8 0/255),用作遮罩下边界 / 重画干净线。"""
m = np.zeros((h, w), np.uint8)
pts = path[:, ::-1].reshape(-1, 1, 2) # (row,col)→(x,y)
cv2.polylines(m, [pts], False, 255, thickness, lineType=cv2.LINE_AA)
return m
if __name__ == "__main__":
import sys
from .service import get_landmarker, get_parser
path_img = sys.argv[1] if len(sys.argv) > 1 else "/home/xsl/headmark/test_image/input1.png"
img = cv2.imread(path_img)
if img is None:
print(f"无法读取 {path_img}"); sys.exit(1)
h, w = img.shape[:2]
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
lm = get_landmarker().detect(rgb)
if lm is None:
print("未检出人脸"); sys.exit(1)
pm = get_parser().parse(rgb)
p = detect_marker_hairline(img, lm, pm)
if p is None:
print("未检测到发际线划线(拒识)"); sys.exit(0)
print(f"检测到画线:{len(p)} 点")
vis = img.copy()
cv2.polylines(vis, [p[:, ::-1].reshape(-1, 1, 2)], False, (0, 0, 255), 2)
import os
os.makedirs("tests/output", exist_ok=True)
name = os.path.splitext(os.path.basename(path_img))[0]
cv2.imwrite(f"tests/output/marker_{name}.png", vis)
print(f"saved tests/output/marker_{name}.png")