修改发型为最终发型
This commit is contained in:
@@ -655,6 +655,11 @@ async def face_measure_v2(
|
||||
- 发际线类型 `hairline_type`(英文 key)
|
||||
- 顺序 `order`(本期固定 `1..N`,不排序)
|
||||
|
||||
> **female 走「换发型」模式**:生发图 `grown_image_base64` 由换发型(change_hair)
|
||||
> + Flux-2 整帧重绘(= 接口12 final 管线,整帧美颜+整帧重绘)生成,其余参数用固化默认值。
|
||||
> **male 仍走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。
|
||||
> female 依赖 change_hair 与 ComfyUI(:8188) 均在跑。
|
||||
|
||||
{_image_fields_desc}
|
||||
|
||||
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
|
||||
@@ -734,10 +739,17 @@ async def hair_grow(
|
||||
|
||||
try:
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from hairline.service import generate_grow_results
|
||||
|
||||
# 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环
|
||||
items = await run_in_threadpool(generate_grow_results, image, gender, use_mask, prompt, hair_styles)
|
||||
# 预览 + 生发/换发型 都是阻塞且较慢,放线程池避免卡住事件循环。
|
||||
# female:换发型 + Flux-2 整帧重绘(= 接口12 final 管线);male:仍走原生发管线。
|
||||
if gender == "female":
|
||||
from hairline.service import generate_grow_results_swap
|
||||
items = await run_in_threadpool(
|
||||
generate_grow_results_swap, image, hair_styles, _V2_FINAL_DEFAULTS)
|
||||
else:
|
||||
from hairline.service import generate_grow_results
|
||||
items = await run_in_threadpool(
|
||||
generate_grow_results, image, gender, use_mask, prompt, hair_styles)
|
||||
if items is None:
|
||||
return err(1001, "无法识别人像")
|
||||
|
||||
|
||||
@@ -20,11 +20,22 @@ from .render import load_ext_mesh, load_texture_rgba, render_hairline_overlay, b
|
||||
from .mask import build_inpaint_mask, compose_comfy_rgba, mask_from_curve
|
||||
from .marker_detect import detect_marker_hairline, path_to_curve_mask
|
||||
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("hair.worker")
|
||||
|
||||
# 接口2 女性发型 key → change_hair hair_id(chang_*)映射:换发型+Flux-2 整帧重绘用。
|
||||
# 与接口12 final 的 5 型一一对应。
|
||||
_FEMALE_KEY_TO_CHANG = {
|
||||
"ellipse": "chang_tuoyuan", # 椭圆
|
||||
"flower": "chang_huaban", # 花瓣
|
||||
"heart": "chang_xinxing", # 心形
|
||||
"straight": "chang_zhixian", # 直线
|
||||
"wave": "chang_bolang", # 波浪
|
||||
}
|
||||
|
||||
_REPO = os.path.dirname(os.path.dirname(__file__))
|
||||
_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture")
|
||||
_BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
||||
@@ -202,6 +213,60 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
|
||||
return results
|
||||
|
||||
|
||||
def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | None,
|
||||
redraw_defaults: dict):
|
||||
"""接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 换发型+Flux-2 整帧重绘图。
|
||||
|
||||
与 generate_grow_results 的差异仅在 grown 图的来源:这里对每个选中发型把 female key 映射到
|
||||
change_hair 的 chang_* hair_id,调 face_analysis.hairline_grow.generate_hairline_redraw
|
||||
(= 接口12 final 管线,参数用 redraw_defaults),取 `redraw_full`(整帧重绘)作为生发图。
|
||||
|
||||
overlay 仍是发际线曲线透明层(与 generate_grow_results 完全一致)。
|
||||
Returns: list[dict] {"hairline_type","order","overlay","grown_png"(jpg bytes 或 None)};
|
||||
无人脸返回 None。单个发型换发型/重绘失败时 grown_png=None,不抛异常。
|
||||
"""
|
||||
from face_analysis.hairline_grow import generate_hairline_redraw, NoFaceError
|
||||
|
||||
ctx = extract_context(image_bgr)
|
||||
if ctx is None:
|
||||
return None
|
||||
uv, ext_faces = load_ext_mesh()
|
||||
|
||||
textures = get_texture_map()["female"] # [(key, path), ...] 已排序
|
||||
if hair_styles is not None:
|
||||
items = [(s, textures[s - 1]) for s in hair_styles]
|
||||
else:
|
||||
items = list(enumerate(textures, start=1))
|
||||
|
||||
results = []
|
||||
h, w = image_bgr.shape[:2]
|
||||
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)
|
||||
|
||||
grown_png = None
|
||||
chang_id = _FEMALE_KEY_TO_CHANG.get(key)
|
||||
if chang_id is None:
|
||||
logger.warning("接口2 换发型:female key=%s 无对应 chang_id,跳过生发图", key)
|
||||
else:
|
||||
try:
|
||||
data = generate_hairline_redraw(image_bgr, chang_id, **redraw_defaults)
|
||||
b64 = (data.get("steps") or {}).get("redraw_full_base64") or ""
|
||||
if b64.startswith("data:"):
|
||||
b64 = b64.split(",", 1)[1]
|
||||
grown_png = base64.b64decode(b64) if b64 else None
|
||||
if grown_png is None:
|
||||
logger.warning("接口2 换发型:type=%s 整帧重绘为空(可能 ComfyUI 未生效)", key)
|
||||
except NoFaceError:
|
||||
logger.warning("接口2 换发型:type=%s 未检出人脸", key)
|
||||
except Exception as e: # noqa: BLE001 单张失败不拖垮整请求
|
||||
logger.warning("接口2 换发型图失败 type=%s:%s", key, e)
|
||||
|
||||
results.append({"hairline_type": key, "order": order,
|
||||
"overlay": overlay, "grown_png": grown_png})
|
||||
return results
|
||||
|
||||
|
||||
def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
|
||||
use_mask: bool, prompt: str | None):
|
||||
"""对单个发际线做生发(ComfyUI)。黑模板固定取 hairline_texture_black/(middle),
|
||||
|
||||
@@ -156,7 +156,7 @@ async function submitTest() {
|
||||
fd.append('prompt', $('promptInput').value);
|
||||
const _reqStart = performance.now();
|
||||
try {
|
||||
const r = await fetch(API_BASE + '/api/v1/hair/grow', { method:'POST', body:fd });
|
||||
const r = await fetch(API_BASE + '/api/v1/hair/grow', { method:'POST', headers:{ 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
|
||||
const json = await r.json();
|
||||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||||
$('jsonContent').textContent = JSON.stringify(json, null, 2);
|
||||
@@ -179,6 +179,9 @@ function renderSchemes(results) {
|
||||
let html = '';
|
||||
results.forEach((r, i) => {
|
||||
const lb = TYPE_LABELS[r.hairline_type] || r.hairline_type;
|
||||
// 直连 worker 时后端返回 base64(网关才会改写为 *_url),URL 缺失则回退 data URI
|
||||
const overlaySrc = r.image_url || (r.image_base64 ? 'data:image/png;base64,' + r.image_base64 : '');
|
||||
const grownSrc = r.grown_image_url || (r.grown_image_base64 ? 'data:image/jpeg;base64,' + r.grown_image_base64 : '');
|
||||
|
||||
// 原图
|
||||
const origSlot = '<div class="img-slot">'+
|
||||
@@ -190,15 +193,15 @@ function renderSchemes(results) {
|
||||
'<div class="label"><span class="dot preview"></span>原图+发际线叠加</div>'+
|
||||
'<div class="thumb"><div class="img-stack">'+
|
||||
'<img class="layer-base" src="'+_origUrl+'" alt="原图">'+
|
||||
'<img class="layer-anno" src="'+r.image_url+'" alt="叠加">'+
|
||||
'<img class="layer-anno" src="'+overlaySrc+'" alt="叠加">'+
|
||||
'</div></div></div>';
|
||||
|
||||
// 生发效果
|
||||
let grownSlot;
|
||||
if (r.grown_image_url) {
|
||||
if (grownSrc) {
|
||||
grownSlot = '<div class="img-slot">'+
|
||||
'<div class="label"><span class="dot grown"></span>生发效果</div>'+
|
||||
'<div class="thumb"><img src="'+r.grown_image_url+'" alt="生发"></div></div>';
|
||||
'<div class="thumb"><img src="'+grownSrc+'" alt="生发"></div></div>';
|
||||
} else {
|
||||
grownSlot = '<div class="img-slot">'+
|
||||
'<div class="label"><span class="dot grown"></span>生发效果</div>'+
|
||||
|
||||
Reference in New Issue
Block a user