修改发型为最终发型
This commit is contained in:
@@ -655,6 +655,11 @@ async def face_measure_v2(
|
|||||||
- 发际线类型 `hairline_type`(英文 key)
|
- 发际线类型 `hairline_type`(英文 key)
|
||||||
- 顺序 `order`(本期固定 `1..N`,不排序)
|
- 顺序 `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}
|
{_image_fields_desc}
|
||||||
|
|
||||||
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
|
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
|
||||||
@@ -734,10 +739,17 @@ async def hair_grow(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from fastapi.concurrency import run_in_threadpool
|
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:
|
if items is None:
|
||||||
return err(1001, "无法识别人像")
|
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 .mask import build_inpaint_mask, compose_comfy_rgba, mask_from_curve
|
||||||
from .marker_detect import detect_marker_hairline, path_to_curve_mask
|
from .marker_detect import detect_marker_hairline, path_to_curve_mask
|
||||||
|
|
||||||
|
import base64
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger("hair.worker")
|
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__))
|
_REPO = os.path.dirname(os.path.dirname(__file__))
|
||||||
_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture")
|
_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture")
|
||||||
_BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
_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
|
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,
|
def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
|
||||||
use_mask: bool, prompt: str | None):
|
use_mask: bool, prompt: str | None):
|
||||||
"""对单个发际线做生发(ComfyUI)。黑模板固定取 hairline_texture_black/(middle),
|
"""对单个发际线做生发(ComfyUI)。黑模板固定取 hairline_texture_black/(middle),
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ async function submitTest() {
|
|||||||
fd.append('prompt', $('promptInput').value);
|
fd.append('prompt', $('promptInput').value);
|
||||||
const _reqStart = performance.now();
|
const _reqStart = performance.now();
|
||||||
try {
|
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 json = await r.json();
|
||||||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||||||
$('jsonContent').textContent = JSON.stringify(json, null, 2);
|
$('jsonContent').textContent = JSON.stringify(json, null, 2);
|
||||||
@@ -179,6 +179,9 @@ function renderSchemes(results) {
|
|||||||
let html = '';
|
let html = '';
|
||||||
results.forEach((r, i) => {
|
results.forEach((r, i) => {
|
||||||
const lb = TYPE_LABELS[r.hairline_type] || r.hairline_type;
|
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">'+
|
const origSlot = '<div class="img-slot">'+
|
||||||
@@ -190,15 +193,15 @@ function renderSchemes(results) {
|
|||||||
'<div class="label"><span class="dot preview"></span>原图+发际线叠加</div>'+
|
'<div class="label"><span class="dot preview"></span>原图+发际线叠加</div>'+
|
||||||
'<div class="thumb"><div class="img-stack">'+
|
'<div class="thumb"><div class="img-stack">'+
|
||||||
'<img class="layer-base" src="'+_origUrl+'" alt="原图">'+
|
'<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>';
|
'</div></div></div>';
|
||||||
|
|
||||||
// 生发效果
|
// 生发效果
|
||||||
let grownSlot;
|
let grownSlot;
|
||||||
if (r.grown_image_url) {
|
if (grownSrc) {
|
||||||
grownSlot = '<div class="img-slot">'+
|
grownSlot = '<div class="img-slot">'+
|
||||||
'<div class="label"><span class="dot grown"></span>生发效果</div>'+
|
'<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 {
|
} else {
|
||||||
grownSlot = '<div class="img-slot">'+
|
grownSlot = '<div class="img-slot">'+
|
||||||
'<div class="label"><span class="dot grown"></span>生发效果</div>'+
|
'<div class="label"><span class="dot grown"></span>生发效果</div>'+
|
||||||
|
|||||||
Reference in New Issue
Block a user