Files
hair/tests/test_api.py
T
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

157 lines
5.6 KiB
Python
Raw 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.
"""接口集成测试(FastAPI TestClient):错误码 + 鉴权 + 正常用例结构。"""
import base64
import pytest
from fastapi.testclient import TestClient
from conftest import fixture
import app as app_module
# 测试自带固定密码,避免依赖 worker_config.json 的实际值(中间件运行期读模块全局)
app_module.ACCEPT_PASSWORDS = ["testpass"]
URL = "/api/v1/face/measure"
H = {"X-Internal-Token": "testpass"}
@pytest.fixture(scope="module")
def client():
# with 触发 lifespan:加载模型单例(detector + 尽力加载 segmenter
with TestClient(app_module.app) as c:
yield c
def _post(client, fixture_name=None, headers=H, data=None, extra_files=None):
files = {}
if fixture_name:
files["image_file"] = (fixture_name, open(fixture(fixture_name), "rb"), "application/octet-stream")
if extra_files:
files.update(extra_files)
return client.post(URL, headers=headers, files=files or None, data=data)
def test_auth_missing_token_401(client):
r = _post(client, "frontal.jpg", headers={})
assert r.status_code == 401
def test_health_no_token_200(client):
r = client.get("/health")
assert r.status_code == 200
assert r.json()["status"] == "ok"
def test_param_none_provided_1007(client):
r = client.post(URL, headers=H)
assert r.json()["code"] == 1007
def test_param_multiple_provided_1007(client):
r = _post(client, "frontal.jpg", data={"image_url": "http://example.com/x.jpg"})
assert r.json()["code"] == 1007
def test_oversize_1006(client, oversize_file):
files = {"image_file": ("oversize.bin", open(oversize_file, "rb"), "application/octet-stream")}
r = client.post(URL, headers=H, files=files)
assert r.json()["code"] == 1006
def test_lowres_1002(client):
r = _post(client, "lowres.png")
assert r.json()["code"] == 1002
def test_no_face_1001(client):
r = _post(client, "landscape.jpg")
assert r.json()["code"] == 1001
def test_corrupt_1008(client):
r = _post(client, "corrupt.bin")
assert r.json()["code"] == 1008
GROW = "/api/v1/hair/grow"
def test_grow_missing_gender_1004(client):
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
r = client.post(GROW, headers=H, files=files)
assert r.json()["code"] == 1004
_PNG_1x1 = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
)
def test_grow_female_returns_5(client, monkeypatch):
# mock ComfyUI:不依赖 8182、不跑 Flux,只验证管线接线 + grown 字段
import hairline.comfyui as comfy
monkeypatch.setattr(comfy, "run", lambda *a, **k: _PNG_1x1)
files = {"image_file": ("frontal.jpg", open(fixture("frontal.jpg"), "rb"), "application/octet-stream")}
r = client.post(GROW, headers=H, files=files, data={"gender": "female"})
body = r.json()
assert body["code"] == 0, body
results = body["data"]["results"]
assert [x["hairline_type"] for x in results] == ["ellipse", "flower", "heart", "straight", "wave"]
assert [x["order"] for x in results] == [1, 2, 3, 4, 5]
assert base64.b64decode(results[0]["image_base64"])[:8] == b"\x89PNG\r\n\x1a\n"
assert base64.b64decode(results[0]["grown_image_base64"])[:8] == b"\x89PNG\r\n\x1a\n"
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()
assert body["code"] == 0, body
data = body["data"]
# 业务字段对齐文档
assert set(["face_total_height_cm", "four_courts", "seven_eyes",
"landmarks", "hairline_source", "head_pose",
"annotated_image_base64"]).issubset(data.keys())
assert set(["top_court_cm", "upper_court_cm", "middle_court_cm",
"lower_court_cm", "ratios"]).issubset(data["four_courts"].keys())
assert set(["eye_width_cm", "face_width_cm", "inter_eye_distance_cm",
"ratios"]).issubset(data["seven_eyes"].keys())
assert data["hairline_source"] in ("segmentation", "estimated")
# base64 解码为合法 PNG(非 URL
assert "annotated_image_url" not in data
png = base64.b64decode(data["annotated_image_base64"])
assert png[:8] == b"\x89PNG\r\n\x1a\n"