feat: 新增52号发际线接口并限制GPU worker来源IP
保留5号历史三档实现,新增中下庭高度定位、扶正裁切、矩形叠加与30案例对比页。 worker仅允许本机和配置白名单访问;网关不增加IP限制。空图片URL字段改写限定为52号,保持旧接口兼容。同步接口、裁切与部署文档。 验证:113项测试通过;1项历史数值基线失败,在未修改的a3fddbb版本复现。浏览器验证5/52及30案例对比页通过。
This commit is contained in:
+77
-2
@@ -22,6 +22,7 @@ from .lift_3d import lift_hairline_to_3d, build_middle_row, assemble_full
|
||||
from .render import load_ext_mesh, load_texture_rgba, render_hairline_overlay, build_overlay_layer
|
||||
from .mask import build_inpaint_mask, compose_comfy_rgba, mask_from_curve
|
||||
from .marker_detect import detect_marker_hairline, path_to_curve_mask
|
||||
from .height_rule import height_target, clean_curve, place_curve, inpaint_from_curve
|
||||
|
||||
import base64
|
||||
import io
|
||||
@@ -178,7 +179,7 @@ def extract_502(image_bgr: np.ndarray):
|
||||
return ctx["points"], ctx["valid"]
|
||||
|
||||
|
||||
def extract_context(image_bgr: np.ndarray):
|
||||
def extract_context(image_bgr: np.ndarray, landmarks: np.ndarray | None = None):
|
||||
"""照片(BGR) → {landmarks, parse_map, points, valid}。无人脸返回 None。
|
||||
|
||||
发际线几何使用射线检测 + 人脸对应点对齐 + 头部轮廓钳制:
|
||||
@@ -189,7 +190,8 @@ def extract_context(image_bgr: np.ndarray):
|
||||
fallback 点不会再跑出头部轮廓。
|
||||
"""
|
||||
rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
|
||||
landmarks = get_landmarker().detect(rgb)
|
||||
if landmarks is None:
|
||||
landmarks = get_landmarker().detect(rgb)
|
||||
if landmarks is None:
|
||||
return None
|
||||
parse_map = get_parser().parse(rgb)
|
||||
@@ -538,6 +540,79 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
||||
return {"images": images, "best_centers": best_centers}
|
||||
|
||||
|
||||
def generate_hairline_pngs_52(image_bgr: np.ndarray, gender: str,
|
||||
hair_styles: list[int], use_mask: bool = True,
|
||||
prompt: str | None = None,
|
||||
generate_grow_image: bool = True,
|
||||
height_reference: dict | None = None,
|
||||
compare_rules: bool = False,
|
||||
precomputed_landmarks: np.ndarray | None = None):
|
||||
"""接口52:每种 middle 曲线中点对齐到眉心上方 (中庭+下庭)/2。
|
||||
|
||||
只读取 middle 贴图,high/low 保留空值。生发直接复用移动后的预览曲线,
|
||||
不再使用位置不同的黑模板。compare_rules 仅为调试页附带旧规则叠图。
|
||||
接口2仍使用原有管线。
|
||||
"""
|
||||
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, landmarks=precomputed_landmarks)
|
||||
if ctx is None:
|
||||
return None
|
||||
h, w = image_bgr.shape[:2]
|
||||
uv, ext_faces = load_ext_mesh()
|
||||
rule = height_target(ctx["landmarks"], w, h, height_reference)
|
||||
textures = get_texture_map()[gender]
|
||||
|
||||
# use_mask=False:干净原图+空遮罩与贴图无关,只跑一次 ComfyUI,选中项复用
|
||||
# generate_grow_image=False:完全跳过生发(最耗时),grown_png 恒为 None
|
||||
shared_grown = None
|
||||
if generate_grow_image and not use_mask:
|
||||
shared_grown = _grow_from_texture(image_bgr, ctx, None, use_mask=False, prompt=prompt)
|
||||
|
||||
images, best_centers = [], None
|
||||
for s in hair_styles: # s = 1-indexed 发型序号
|
||||
key, mid_path = textures[s - 1]
|
||||
old = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, load_texture_rgba(mid_path))
|
||||
overlay, placement = place_curve(old, rule["target_center"], ctx["landmarks"])
|
||||
overlays = {"middle": overlay, "high": None, "low": None}
|
||||
if not generate_grow_image:
|
||||
grown_png = None
|
||||
elif not use_mask:
|
||||
grown_png = shared_grown
|
||||
else:
|
||||
grown_png = None
|
||||
try:
|
||||
marked, mask = inpaint_from_curve(
|
||||
image_bgr, ctx["landmarks"], ctx["parse_map"], overlay)
|
||||
buf = io.BytesIO()
|
||||
compose_comfy_rgba(marked, mask).save(buf, format="PNG", compress_level=1)
|
||||
grown_png = comfyui.run(buf.getvalue(), prompt=prompt)
|
||||
except Exception as ex: # 单张生发失败不丢弃设计曲线
|
||||
logger.warning("接口52 生发失败 type=%s:%s", key, ex)
|
||||
# Flux may round dimensions to a multiple of its latent stride.
|
||||
# Restore the input size so the preview and generated photo align.
|
||||
if grown_png:
|
||||
grown_png = _upscale_png_to(grown_png, w, h)
|
||||
item = {"hairline_type": key, "order": s, "overlays": overlays,
|
||||
"grown_png": grown_png, "height_placement": placement}
|
||||
if compare_rules:
|
||||
item["legacy_overlay"] = clean_curve(old)
|
||||
images.append(item)
|
||||
if best_centers is None:
|
||||
center = placement["new_center"]
|
||||
best_centers = {"middle": (round(center["x"]), round(center["y"])),
|
||||
"high": None, "low": None}
|
||||
guide_landmarks = {
|
||||
str(index): {"x": float(ctx["landmarks"][index, 0] * w),
|
||||
"y": float(ctx["landmarks"][index, 1] * h)}
|
||||
for index in (9, 150, 152, 162, 251, 389)
|
||||
}
|
||||
return {"images": images, "best_centers": best_centers, "height_rule": rule,
|
||||
"guide_landmarks": guide_landmarks}
|
||||
|
||||
|
||||
# 接口3 送 ComfyUI 前限边,降低峰值显存,避免与接口2 切换时把 Flux 挤出。
|
||||
# 统一 prompt 后 Flux 不再被 CLIP 挤出,接口3 可用较高分辨率。可用 GROW_B_MAX_SIDE 覆盖。
|
||||
_GROW_B_MAX_SIDE = int(os.getenv("GROW_B_MAX_SIDE", "1024"))
|
||||
|
||||
Reference in New Issue
Block a user