feat: 新增接口22生发控制与测试页
This commit is contained in:
@@ -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
@@ -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 仅为调试页附带旧规则叠图。
|
||||
|
||||
Reference in New Issue
Block a user