Files
hair/hairline/render.py
xslandClaude Opus 4.8 94ad95850e feat(接口2): 新增生发后图片(ComfyUI/Flux inpaint)
在发际线预览基础上,每种发际线再出一张「植发3个月」生发图:

- hairline/mask.py: headmark 5步法遮罩(额头上部区域∩SegFormer头部=ROI,
  取发际线曲线以上闭合区域);用 hairline_texture_black 渲染黑线替代手绘检测;
  compose_comfy_rgba 合成 RGBA(alpha=255-mask, 透明=重绘区, 对齐 ComfyUI mask=1-alpha)
- hairline/comfyui.py: ComfyUI 客户端(默认8182),/upload/image+/prompt(改节点26+随机seed)
  +轮询/history+/view 取回生发图
- hairline/render.py: 抽出 build_overlay_layer 供遮罩取曲线像素
- hairline/service.py: extract_context 一次出 landmarks/parse_map/502点;
  generate_grow_results 每种=预览+生发图(同步串行N张,单张ComfyUI失败则grown置空不拖垮整请求)
- app.py: /hair/grow 返回 results[].grown_image_base64;重活放线程池避免卡事件循环
- add_hair.json 工作流 + hairline_texture_black/ 黑贴图入库
- 测试: test_mask.py(遮罩几何) + test_api mock ComfyUI 验 grown 字段,35 全绿

实测(5090): female 5张生发图同步约18s;预览/生发图人物五官服饰背景保持、黑线已清除。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:42:30 +08:00

114 lines
4.7 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.
"""接口2 渲染器:把发际线类型贴图按 502 点 mesh 贴到照片上(OpenCV 逐三角仿射 warp)。
原理(技术方案 §4):face_ext.obj 的 502 顶点里,[468..485) 中间行 + [485..502) 发际线行
与 17 个 MP 顶部锚点连成 ~64 个 ribbon 三角形,其 UV 落在贴图顶部条带(发际线曲线所在)。
只 warp 这些扩展三角形,即可把贴图里的发际线曲线贴到额头/发际线区域,且天然只画在 ribbon 区。
"""
from __future__ import annotations
import os
import cv2
import numpy as np
from PIL import Image
from .obj_io import read_obj
from ._index_map_data import INDEX_MAP_468
_MESH_PATH = os.path.join(os.path.dirname(__file__), "mesh", "face_ext.obj")
_N_MP = 468
_mesh_cache = None
_INDEX_MAP = np.asarray(INDEX_MAP_468, dtype=np.int64) # OBJ顶点i → MP点索引
def mp_order_to_obj_order(points502_mp: np.ndarray) -> np.ndarray:
"""把 MP 顺序的 502 点重排成 face_ext.obj 的顶点顺序。
extract_hairline 输出为 MP 顺序:[0..468) MP / [468..485) middle / [485..502) hairline。
而 face_ext.obj 的前 468 顶点经 INDEX_MAP_468 重排(obj_i → mp_i);扩展顶点
[468..502) 两侧同序,直接对应。
"""
out = np.empty_like(points502_mp)
out[:_N_MP] = points502_mp[_INDEX_MAP] # obj[0..468) = mp[INDEX_MAP]
out[_N_MP:] = points502_mp[_N_MP:] # 扩展行同序
return out
def load_ext_mesh(obj_path: str = _MESH_PATH):
"""解析 face_ext.obj,返回 (uv502, ext_faces)。结果缓存。
- uv502: (502, 2) float32,每个顶点的 UVV_raw,V=1 对应贴图顶部)。
- ext_faces: list[(i,j,k)],仅保留顶点索引含 ≥468 的扩展三角形(ribbon)。
obj 中 v 与 vt 一一对应(face 用相同索引),故按位置索引取 UV。
"""
global _mesh_cache
if _mesh_cache is not None:
return _mesh_cache
mesh = read_obj(obj_path)
n_v = len(mesh.positions)
uv = np.zeros((n_v, 2), dtype=np.float32)
for face in mesh.faces:
for pi, ti, _ni in face:
if ti >= 0 and pi >= 0:
uv[pi] = mesh.texcoords[ti]
ext_faces = []
for face in mesh.faces:
idx = [pi for (pi, _t, _n) in face]
if max(idx) >= _N_MP: # 含扩展顶点 → ribbon 三角形
ext_faces.append(tuple(idx))
_mesh_cache = (uv, ext_faces)
return _mesh_cache
def load_texture_rgba(path: str) -> np.ndarray:
"""读发际线贴图为 (H, W, 4) uint8 RGBA。"""
return np.array(Image.open(path).convert("RGBA"))
def render_hairline_overlay(photo_bgr: np.ndarray,
points502_norm: np.ndarray,
ext_faces,
uv502: np.ndarray,
texture_rgba: np.ndarray) -> np.ndarray:
"""把 texture_rgba 的发际线曲线渲染到 photo_bgr 上,返回 BGR 预览图。
points502_norm: (502, 3) 归一化坐标(x,y ∈ [0,1]),**MP 顺序**extract_hairline 输出)。
"""
H, W = photo_bgr.shape[:2]
overlay = build_overlay_layer(H, W, points502_norm, ext_faces, uv502, texture_rgba)
# alpha 合成(RGBA→BGR:贴图 RGB 顺序需反成 BGR)
a = overlay[:, :, 3:4] / 255.0
rgb = overlay[:, :, :3][..., ::-1] # RGB→BGR
out = photo_bgr.astype(np.float32) * (1.0 - a) + rgb * a
return np.clip(out, 0, 255).astype(np.uint8)
def build_overlay_layer(H, W, points502_norm, ext_faces, uv502, texture_rgba) -> np.ndarray:
"""渲染发际线曲线层,返回 (H, W, 4) float32 RGBA(未合成到照片)。
供渲染合成(render_hairline_overlay)与遮罩(mask.py 取 alpha=曲线像素)共用。
"""
TH, TW = texture_rgba.shape[:2]
pts_obj = mp_order_to_obj_order(points502_norm)
img_xy = pts_obj[:, :2] * np.array([W, H], dtype=np.float32)
overlay = np.zeros((H, W, 4), np.float32)
tex = texture_rgba.astype(np.float32)
for (i, j, k) in ext_faces:
dst = img_xy[[i, j, k]].astype(np.float32)
# UV → 贴图像素;flipY:贴图 y = (1 - v_raw) * TH(与 head3d Three.js flipY=true 一致)
src = np.array([[uv502[v][0] * TW, (1.0 - uv502[v][1]) * TH] for v in (i, j, k)],
dtype=np.float32)
if cv2.contourArea(dst.astype(np.int32)) < 1.0: # 退化三角形跳过
continue
M = cv2.getAffineTransform(src, dst)
warped = cv2.warpAffine(tex, M, (W, H), flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT, borderValue=(0, 0, 0, 0))
tri_mask = np.zeros((H, W), np.uint8)
cv2.fillConvexPoly(tri_mask, dst.astype(np.int32), 255)
sel = tri_mask > 0
overlay[sel] = warped[sel]
return overlay