接口5:改为多选发型 + 每发型返回 middle/high/low 三档叠图(去掉 hairline_level 入参)

- 去掉 hairline_level 入参;middle/high/low 三档都返回
- 入参改为同接口2:gender + hair_style(逗号分隔多选,必填),缺失/越界返回 1007
- 每个选中发型返回 image_{middle,high,low}_base64 三档叠图 + grown_image_base64 生发图,
  按发型分组;order = 发型序号,含 hairline_type
- 生发黑模板仍固定 middle(hairline_texture_black/),每发型 1 张生发图
- best_hairline_center_point 取首个选中发型的 middle 档
- 同步更新测试页(三档并排展示)、integration.html、stub_worker、接口文档/实现说明、test_api

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-07-10 00:19:08 +08:00
co-authored by Claude Opus 4.8
parent 864d7f969a
commit f8e30ad32e
8 changed files with 161 additions and 120 deletions
+27 -21
View File
@@ -223,20 +223,22 @@ def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
return None
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str, hairline_level: str = "middle",
hair_styles: list[int] | None = None, use_mask: bool = True,
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
hair_styles: list[int], use_mask: bool = True,
prompt: str | None = None):
"""接口5该性别全部发际线叠图(同接口2预览) + 最佳(order1)发际线曲线的面部中间点,
并对 hair_styles 选中的类型生发(同接口2)。
"""接口5对选中发型返回 middle/high/low 三档发际线叠图 + 生发图(同接口2)。
hairline_levelmiddle(默认)/high/low,选用不同档位的发际线贴图(仅影响叠图)。
hair_styles1-indexed 列表,按贴图排序):指定对哪些类型生发;None/[] 时不生发。
生发黑模板固定取自 hairline_texture_black/middle),与 hairline_level 无关
入参同接口2:先选 gender,再多选 hair_styles(必填,1-indexed 按贴图排序)。
每个选中发型返回三档叠图(middle/high/low)与一张生发图;三档贴图同名,
生发黑模板固定取自 hairline_texture_black/middle),故生发目标固定 middle 档
use_mask/prompt:同接口2 的生发参数。
Returns: {"images":[{hairline_type,order,image_bgr,grown_png}], "best_center":(x,y)};无人脸 None。
Returns: {"images":[{hairline_type,order,overlays:{middle,high,low}(BGR),grown_png}],
"best_center":(x,y)};无人脸 None。best_center 取首个选中发型的 middle 档。
"""
if gender not in ("male", "female"):
raise ValueError(f"gender 必须是 male/female,收到 {gender!r}")
if not hair_styles:
raise ValueError("hair_styles 必填且不能为空")
ctx = extract_context(image_bgr)
if ctx is None:
return None
@@ -246,25 +248,29 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str, hairline_level: s
# 面部中轴 x = 眉心(9/151 中点)
face_cx = float((lm[9, 0] + lm[151, 0]) / 2 * w)
textures = get_texture_map(hairline_level)[gender]
grow_set = set(hair_styles or [])
# 三档贴图表(同性别、同 key 顺序,因三个文件夹同名)
tex_by_level = {lv: get_texture_map(lv)[gender] for lv in _TEXTURE_DIRS}
# use_mask=False:干净原图+空遮罩与贴图无关,只跑一次 ComfyUI,选中项复用
shared_grown = None
if grow_set and not use_mask:
if not use_mask:
shared_grown = _grow_from_texture(image_bgr, ctx, None, use_mask=False, prompt=prompt)
images, best_center = [], None
for order, (key, path) in enumerate(textures, start=1):
white = load_texture_rgba(path)
preview = render_hairline_overlay(image_bgr, ctx["points"], ext_faces, uv, white)
grown_png = None
if order in grow_set:
grown_png = shared_grown if not use_mask else \
_grow_from_texture(image_bgr, ctx, path, use_mask=True, prompt=prompt)
images.append({"hairline_type": key, "order": order,
"image_bgr": preview, "grown_png": grown_png})
if order == 1: # 最佳发际线曲线的中点(面部中轴处的发际线 y)
for s in hair_styles: # s = 1-indexed 发型序号
key, mid_path = tex_by_level["middle"][s - 1]
overlays = {}
for lv in _TEXTURE_DIRS:
white = load_texture_rgba(tex_by_level[lv][s - 1][1])
overlays[lv] = render_hairline_overlay(image_bgr, ctx["points"], ext_faces, uv, white)
# 生发:固定 middle 黑模板
grown_png = shared_grown if not use_mask else \
_grow_from_texture(image_bgr, ctx, mid_path, use_mask=True, prompt=prompt)
images.append({"hairline_type": key, "order": s,
"overlays": overlays, "grown_png": grown_png})
# best_center:首个选中发型的 middle 档发际线中点(面部中轴处的发际线 y)
if best_center is None:
white = load_texture_rgba(mid_path)
overlay = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white)
ys, xs = np.where(overlay[:, :, 3] > 40)
if xs.size: