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:
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
@@ -103,6 +103,39 @@ def test_grow_female_returns_5(client, monkeypatch):
|
||||
assert "image_url" not in results[0]
|
||||
|
||||
|
||||
GROWB = "/api/v1/hair/grow-b"
|
||||
|
||||
|
||||
def test_growb_missing_original_1007(client):
|
||||
files = {"marked_image_file": ("m.jpg", open(fixture("marked_hairline.jpg"), "rb"), "application/octet-stream")}
|
||||
r = client.post(GROWB, headers=H, files=files)
|
||||
assert r.json()["code"] == 1007
|
||||
|
||||
|
||||
def test_growb_no_line_1001(client):
|
||||
f = lambda: open(fixture("frontal.jpg"), "rb")
|
||||
files = {"marked_image_file": ("m.jpg", f(), "application/octet-stream"),
|
||||
"original_image_file": ("o.jpg", f(), "application/octet-stream")}
|
||||
r = client.post(GROWB, headers=H, files=files)
|
||||
assert r.json()["code"] == 1001
|
||||
|
||||
|
||||
def test_growb_success(client, monkeypatch):
|
||||
import hairline.comfyui as comfy
|
||||
monkeypatch.setattr(comfy, "run", lambda *a, **k: _PNG_1x1)
|
||||
fm = open(fixture("marked_hairline.jpg"), "rb")
|
||||
fo = open(fixture("marked_hairline.jpg"), "rb")
|
||||
files = {"marked_image_file": ("m.jpg", fm, "application/octet-stream"),
|
||||
"original_image_file": ("o.jpg", fo, "application/octet-stream")}
|
||||
body = client.post(GROWB, headers=H, files=files).json()
|
||||
assert body["code"] == 0, body
|
||||
d = body["data"]
|
||||
assert d["hairline_type"] == "custom"
|
||||
assert base64.b64decode(d["hair_growth_image_base64"])[:8] == b"\x89PNG\r\n\x1a\n"
|
||||
assert d["best_hairline_image_base64"] # 原图原样(非空)
|
||||
assert "best_hairline_image_url" not in d
|
||||
|
||||
|
||||
def test_success_structure(client):
|
||||
r = _post(client, "frontal.jpg")
|
||||
body = r.json()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""接口3 马克笔检测测试:辅助函数(纯numpy) + 真实样本检测/拒识。"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
from conftest import fixture
|
||||
|
||||
from hairline.marker_detect import (
|
||||
detect_marker_hairline, path_to_curve_mask, _snap_anchor,
|
||||
)
|
||||
from hairline.service import get_landmarker, get_parser
|
||||
|
||||
|
||||
def test_path_to_curve_mask():
|
||||
path = np.array([[10, 5], [10, 50], [10, 90]]) # 水平线 row=10
|
||||
m = path_to_curve_mask(path, 100, 100, thickness=3)
|
||||
assert m[10, 50] == 255
|
||||
assert m[90, 50] == 0
|
||||
|
||||
|
||||
def test_snap_anchor_moves_to_peak():
|
||||
bh = np.zeros((100, 100), np.float32)
|
||||
bh[40, 30] = 99.0 # 峰值在 (40,30)
|
||||
r, c = _snap_anchor(bh, x=33, y=42, w=1000) # 起点附近 → 应吸到峰值
|
||||
assert (r, c) == (40, 30)
|
||||
|
||||
|
||||
def _ctx(path_img):
|
||||
img = cv2.imread(path_img)
|
||||
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
lm = get_landmarker().detect(rgb)
|
||||
pm = get_parser().parse(rgb)
|
||||
return img, lm, pm
|
||||
|
||||
|
||||
def test_detect_real_marked():
|
||||
img, lm, pm = _ctx(fixture("marked_hairline.jpg"))
|
||||
path = detect_marker_hairline(img, lm, pm)
|
||||
assert path is not None and len(path) > 50 # 检测到画线
|
||||
|
||||
|
||||
def test_reject_clean_photo():
|
||||
img, lm, pm = _ctx(fixture("frontal.jpg"))
|
||||
assert detect_marker_hairline(img, lm, pm) is None # 无画线 → 拒识
|
||||
Reference in New Issue
Block a user