feat: 新增接口22生发控制与测试页

This commit is contained in:
xsl
2026-11-09 21:19:39 +08:00
parent c800822d54
commit b84a506b9b
21 changed files with 770 additions and 175 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
"""接口52设计高度:上庭 = (中庭 + 下庭) / 2;不改变接口2/5的几何。"""
"""接口52设计高度:目标上庭为眉心到下巴垂直距离的 45%。"""
from __future__ import annotations
import cv2
@@ -27,15 +27,15 @@ def height_target(landmarks, width, height, reference=None):
if not np.isfinite([brow, nose, chin]).all() or not brow[1] < nose[1] < chin[1]:
raise ValueError("眉心、鼻翼下缘与下巴坐标异常,无法计算目标上庭高度")
middle, lower = float(nose[1] - brow[1]), float(chin[1] - nose[1])
upper = (middle + lower) / 2.0
upper = (middle + lower) * 0.45
target_y = float(brow[1] - upper)
if not (0 <= brow[0] < width and 1 <= target_y < height - 1):
raise ValueError("按中下庭均值计算的目标发际线超出图片范围,请使用完整正面照")
raise ValueError("按中下庭高度计算的目标发际线超出图片范围,请使用完整正面照")
def pt(p):
return {"x": float(p[0]), "y": float(p[1])}
return {
"name": "court_average",
"formula": "上庭 = (中庭 + 下庭) / 2",
"formula": "上庭 = (中庭 + 下庭) × 0.45",
"measurement_source": source,
"brow_center": pt(brow), "nose_bottom": pt(nose), "chin_tip": pt(chin),
"middle_height_px": middle, "lower_height_px": lower,
+45 -14
View File
@@ -221,10 +221,26 @@ def generate_previews(image_bgr: np.ndarray, gender: str):
return results
def _hairline_shift_px(landmarks: np.ndarray, height: int, adjustment: float | None) -> int:
if not adjustment:
return 0
brow_to_chin = abs(float(landmarks[152, 1] - landmarks[9, 1])) * height
return round(adjustment * 0.10 * brow_to_chin)
def _shift_overlay(overlay: np.ndarray, dy: int) -> np.ndarray:
if dy == 0:
return overlay
h, w = overlay.shape[:2]
transform = np.float32([[1, 0, 0], [0, 1, dy]])
return cv2.warpAffine(overlay, transform, (w, h))
def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = True,
prompt: str = None, hair_styles: list[int] | None = None,
workflow_path: str | None = None,
unet_name: str | None = None):
unet_name: str | None = None,
hairline_height: float | None = None):
"""指定发际线类型:发际线透明叠图(白线 RGBA) + 生发图(ComfyUI)。
hair_styles(1-indexed 列表):指定生成哪几张发际线(按贴图排序)。female: 1..7,male: 1..7。
@@ -269,18 +285,23 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
results = []
h, w = image_bgr.shape[:2]
dy = _hairline_shift_px(ctx["landmarks"], h, hairline_height)
for order, (key, white_path) in items:
white = load_texture_rgba(white_path)
overlay = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white)
overlay = _shift_overlay(build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white), dy)
if not use_mask:
grown_png = shared_grown
else:
grown_png = None
try:
black = load_texture_rgba(_black_texture_path(white_path))
marked, mask = build_inpaint_mask(
image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black)
if hairline_height is not None:
marked, mask = inpaint_from_curve(
image_bgr, ctx["landmarks"], ctx["parse_map"], clean_curve(overlay))
else:
black = load_texture_rgba(_black_texture_path(white_path))
marked, mask = build_inpaint_mask(
image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black)
m_s, msk_s, gsc = _prep_comfy_input(marked, mask)
buf = io.BytesIO()
compose_comfy_rgba(m_s, msk_s).save(buf, format="PNG", compress_level=1)
@@ -302,7 +323,8 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
redraw_defaults: dict,
redraw_max_side: int | None = None,
unet_name: str | None = None,
prompt: str | None = None):
prompt: str | None = None,
hairline_height: float | None = None):
"""接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 生发图。
生发图来源按发型分两路:
@@ -340,6 +362,7 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
results = []
h, w = image_bgr.shape[:2]
dy = _hairline_shift_px(ctx["landmarks"], h, hairline_height)
# 重绘管线(swapHair + ComfyUI)统一降分辨率:真实照片 swap(SD WebUI)~5s、blend、ComfyUI
# 均随分辨率线性下降。overlay 预览仍用全分辨率;grown_png 最后放大回原尺寸。
@@ -355,9 +378,10 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
logger.info("接口2女 管线降分辨率: %dx%d → %dx%d (max_side=%d)",
w, h, _nw, _nh, eff_side)
paste_dy = round(dy * redraw_img.shape[0] / h)
for order, (key, white_path) in items:
white = load_texture_rgba(white_path)
overlay = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white)
overlay = _shift_overlay(build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white), dy)
grown_png = None
chang_id = _FEMALE_KEY_TO_CHANG.get(key)
@@ -365,12 +389,14 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
# 无对应 change_hair LoRA(如 bigflower/clasicalflower)→ 走与男性一致的原生生发
logger.info("接口2 女 key=%s 无 chang_id,走原生生发(ComfyUI add_hair)", key)
grown_png = _grow_native_one(image_bgr, ctx, white_path,
prompt=prompt, unet_name=unet_name)
prompt=prompt, unet_name=unet_name,
design_overlay=overlay if hairline_height is not None else None)
else:
try:
import time as _t
_ts0 = _t.perf_counter()
data = generate_hairline_redraw(redraw_img, chang_id, hair_mask=hair_mask_redraw, **redraw_defaults)
data = generate_hairline_redraw(redraw_img, chang_id, hair_mask=hair_mask_redraw,
paste_dy=paste_dy, **redraw_defaults)
_ts1 = _t.perf_counter()
steps = data.get("steps") or {}
# ④ final(接缝融合基底)+ ⑤-② 纯红遮罩 PNG
@@ -438,7 +464,8 @@ def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
def _grow_native_one(image_bgr: np.ndarray, ctx: dict, white_path: str,
prompt: str | None = None, unet_name: str | None = None):
prompt: str | None = None, unet_name: str | None = None,
design_overlay: np.ndarray | None = None):
"""对单个发际线做原生生发(ComfyUI add_hair),与男性 generate_grow_results 一致。
接口2 女性新发型(bigflower/clasicalflower 等)无对应 change_hair LoRA,改走此路径:
@@ -447,9 +474,13 @@ def _grow_native_one(image_bgr: np.ndarray, ctx: dict, white_path: str,
"""
try:
h, w = image_bgr.shape[:2]
black = load_texture_rgba(_black_texture_path(white_path))
marked, mask = build_inpaint_mask(
image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black)
if design_overlay is not None:
marked, mask = inpaint_from_curve(
image_bgr, ctx["landmarks"], ctx["parse_map"], clean_curve(design_overlay))
else:
black = load_texture_rgba(_black_texture_path(white_path))
marked, mask = build_inpaint_mask(
image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black)
m_s, msk_s, gsc = _prep_comfy_input(marked, mask)
buf = io.BytesIO()
compose_comfy_rgba(m_s, msk_s).save(buf, format="PNG", compress_level=1)
@@ -539,7 +570,7 @@ def generate_hairline_pngs_52(image_bgr: np.ndarray, gender: str,
height_reference: dict | None = None,
compare_rules: bool = False,
precomputed_landmarks: np.ndarray | None = None):
"""接口52:每种 middle 曲线中点对齐到眉心上方 (中庭+下庭)/2。
"""接口52:每种 middle 曲线中点对齐到眉心上方 (中庭+下庭) × 0.45。
只读取 middle 贴图,high/low 保留空值。生发直接复用移动后的预览曲线,
不再使用位置不同的黑模板。compare_rules 仅为调试页附带旧规则叠图。