接口11/12:新增发际线外推遮罩(pushed)模式 + multiband融合修复 + 调试日志 + 对比报告脚本
发际线生发遮罩算法(mask_type=pushed): - _extract_hairline:提取头发/皮肤交界线(逐列头发下沿),用 baseline 水平 y 线截断(无竖线) - _pushed_mask:以眉心(151点)为圆心逐点径向外推 push_cm,与 baseline 组闭合区域 - 径向归并锯齿用插值填补,避免遮罩碎裂 - pushed 模式过程可视化(①-f 交界线 / ①-g 外推+遮罩),eroded/closed 不展示无关步骤 multiband 金字塔融合修复(hairline_grow.py): - mb_levels 按层数膨胀外缘 keep 区,让过渡带随层数变宽(旧硬二值钳回导致 mb_levels 形同虚设) 接口12 grow_v2(固定参数精简版): - 固定 multiband/mb_levels=5/erode_cm=0.6,仅返回 final_base64 - 支持 mask_type=pushed + hairline_push_cm/hairline_edge 调试支持: - 调试页 test_interface11_debug.html(前后端日志面板 + 下载日志按钮) - hairline_grow.log 全链路日志(按 rid 关联),/api/v1/debug/hairline_log 下载接口 - 遮罩计算过程可视化(baseline/upper/头发分割/交界线/外推/最终遮罩) 文档与脚本: - docs/发际线生发遮罩算法_pushed模式.md 算法说明 - scripts/batch_grow_v2.py 批量调用、gen_report_hairline_v2.py 对比报告生成
This commit is contained in:
@@ -137,7 +137,7 @@ app = FastAPI(
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
# 不校验鉴权的路径前缀(供网关探测 / 文档 / 静态)
|
||||
_AUTH_EXEMPT = ("/health", "/docs", "/openapi.json", "/redoc", "/static")
|
||||
_AUTH_EXEMPT = ("/health", "/docs", "/openapi.json", "/redoc", "/static", "/api/v1/debug")
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
@@ -1277,7 +1277,10 @@ async def head_band(
|
||||
(忠于算法文档);`swap_mode=as_is`:不改换发型,贴回时再裁到接口9 遮罩。
|
||||
3. 严格按接口9 遮罩把生成图贴回原图(遮罩外=原图不动)。
|
||||
4. 接缝融合:`blend_method` 选 feather(高斯羽化)/alpha_gradient(距离变换内渐变)/
|
||||
seamless(泊松无缝克隆);`feather_px`、`edge_erode_px` 控制过渡细节。
|
||||
seamless(泊松无缝克隆)/multiband(多频段金字塔融合);`feather_px`、`edge_erode_px` 控制过渡细节
|
||||
(feather_px 仅 feather/alpha_gradient 用;multiband 用 `mb_levels` 控制金字塔层数 2~6)。
|
||||
`color_match=true` 时先在遮罩区做 Reinhard 颜色统计迁移,消除生成图与原图的整体色差
|
||||
(对 feather/alpha_gradient/multiband 有效;seamless 自带色彩调和,自动跳过)。
|
||||
|
||||
{_image_fields_desc}
|
||||
|
||||
@@ -1294,13 +1297,17 @@ async def hairline_grow(
|
||||
hairgrow_strength: float = Form(default=0.75, description="区域生发强度(仅 hairgrow 后端),默认 0.75"),
|
||||
is_hr: bool = Form(default=False, description="高清模式(换发型输出 1152×1536,否则 576×768)"),
|
||||
seg_model: str = Form(default="segformer", description="头发分割模型:bisenet | segformer(默认 segformer)"),
|
||||
mask_type: str = Form(default="eroded", description="遮罩类型:eroded(内缩) | closed(闭合区域)(默认 eroded)"),
|
||||
mask_type: str = Form(default="eroded", description="遮罩类型:eroded(内缩) | closed(闭合区域) | pushed(发际线外推)(默认 eroded)"),
|
||||
erode_cm: float = Form(default=1.2, description="遮罩外缘朝中心151内缩距离(厘米,同接口9),默认 1.2"),
|
||||
hairline_push_cm: float = Form(default=1.0, description="发际线外推距离(厘米,往头发方向推进;仅 mask_type=pushed 生效),默认 1.0"),
|
||||
hairline_edge: str = Form(default="column", description="发际线提取方式:column(逐列最低点) | contour(形态学轮廓)(仅 pushed 生效),默认 column"),
|
||||
swap_mode: str = Form(default="ext_mask", description="换发型取图模式:ext_mask(改造换发型用接口9遮罩) | as_is(不改换发型,贴回再裁)(默认 ext_mask)"),
|
||||
blend_method: str = Form(default="feather", description="接缝融合:feather(高斯羽化) | alpha_gradient(距离渐变) | seamless(泊松无缝)(默认 feather)"),
|
||||
blend_method: str = Form(default="feather", description="接缝融合:feather(高斯羽化) | alpha_gradient(距离渐变) | seamless(泊松无缝) | multiband(多频段金字塔)(默认 feather)"),
|
||||
feather_px: int = Form(default=15, description="羽化/渐变过渡宽度(像素),默认 15"),
|
||||
edge_erode_px: int = Form(default=3, description="贴图前遮罩内缩像素(防边缘露皮/光晕),默认 3"),
|
||||
denoising_strength: float = Form(default=0.6, description="换发型 webui 重绘强度(越大生发越激进),默认 0.6"),
|
||||
color_match: bool = Form(default=False, description="融合前对生成图做 Reinhard 颜色校正(消除整体色差,seamless 下自动跳过),默认 false"),
|
||||
mb_levels: int = Form(default=5, description="multiband 多频段金字塔层数(2~6,越大低频色差抹得越宽,仅 multiband 生效),默认 5"),
|
||||
):
|
||||
"""接口11:发际线生发 + 分步可视化"""
|
||||
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
|
||||
@@ -1314,21 +1321,124 @@ async def hairline_grow(
|
||||
try:
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from face_analysis.hairline_grow import generate_hairline_grow, NoFaceError, SwapError
|
||||
from uuid import uuid4 as _uuid4
|
||||
rid = _uuid4().hex[:8]
|
||||
logger.info("[%s] 接口11 收到请求: mask_type=%s hairline_push_cm=%s hairline_edge=%s",
|
||||
rid, mask_type, hairline_push_cm, hairline_edge)
|
||||
try:
|
||||
data = await run_in_threadpool(
|
||||
generate_hairline_grow, image, hairline_id, is_hr, seg_model,
|
||||
mask_type, erode_cm, swap_mode, blend_method, feather_px, edge_erode_px,
|
||||
denoising_strength, gen_backend, hairgrow_strength)
|
||||
denoising_strength, gen_backend, hairgrow_strength, color_match, mb_levels,
|
||||
hairline_push_cm=hairline_push_cm, hairline_edge=hairline_edge, rid=rid)
|
||||
except NoFaceError:
|
||||
return err(1001, "无法识别人像")
|
||||
except SwapError as se:
|
||||
return err(1007, f"换发型失败:{se}")
|
||||
logger.info("[%s] 接口11 成功返回", rid)
|
||||
return ok(data)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
logger.exception("接口11 处理异常")
|
||||
return err(1007, f"处理失败:{ex}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 接口 12:发际线生发(接口11 固定参数精简版)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.post(
|
||||
"/api/v1/hairline/grow_v2",
|
||||
summary="接口12 发际线生发(固定金字塔融合,仅返回最终图)",
|
||||
tags=["生发"],
|
||||
description=f"""
|
||||
接口11 的固定参数精简版,适合生产直调。与接口11 共用同一管线,区别仅在于:
|
||||
|
||||
- **固定** `blend_method=multiband`(多频段金字塔融合)、`mb_levels=5`、`erode_cm=0.6`
|
||||
(外缘朝中心151 内缩 0.6cm)。这三项不可调,故本接口不暴露。
|
||||
- **返回值精简**:只返回 `final_base64`(最终合成图),不再附带接口11 的分步可视化。
|
||||
|
||||
其余参数(hairline_id、seg_model、gen_backend、is_hr、denoising_strength、color_match、
|
||||
edge_erode_px 等)仍保留为可选 Form,调用方可按需覆盖,未传则用接口11 同款默认值。
|
||||
|
||||
{_image_fields_desc}
|
||||
""",
|
||||
)
|
||||
async def hairline_grow_v2(
|
||||
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG)"),
|
||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||
hairline_id: str = Form(..., description="发际线类型 ID(= change_hair hair_id,如 chang_tuoyuan)"),
|
||||
gen_backend: str = Form(default="swaphair", description="生成后端:swaphair(换发型LoRA) | hairgrow(区域生发inpaint,压低发际线)(默认 swaphair)"),
|
||||
hairgrow_strength: float = Form(default=0.75, description="区域生发强度(仅 hairgrow 后端),默认 0.75"),
|
||||
is_hr: bool = Form(default=False, description="高清模式(换发型输出 1152×1536,否则 576×768)"),
|
||||
seg_model: str = Form(default="segformer", description="头发分割模型:bisenet | segformer(默认 segformer)"),
|
||||
mask_type: str = Form(default="eroded", description="遮罩类型:eroded(内缩) | closed(闭合区域) | pushed(发际线外推)(默认 eroded)"),
|
||||
swap_mode: str = Form(default="ext_mask", description="换发型取图模式:ext_mask(改造换发型用接口9遮罩) | as_is(不改换发型,贴回再裁)(默认 ext_mask)"),
|
||||
feather_px: int = Form(default=15, description="羽化/渐变过渡宽度(像素,本接口固定 multiband 故不生效,仅留作兼容)"),
|
||||
edge_erode_px: int = Form(default=3, description="贴图前遮罩内缩像素(防边缘露皮/光晕),默认 3"),
|
||||
denoising_strength: float = Form(default=0.6, description="换发型 webui 重绘强度(越大生发越激进),默认 0.6"),
|
||||
color_match: bool = Form(default=False, description="融合前对生成图做 Reinhard 颜色校正(消除整体色差),默认 false"),
|
||||
hairline_push_cm: float = Form(default=1.0, description="发际线外推距离(厘米,仅 mask_type=pushed 生效),默认 1.0"),
|
||||
hairline_edge: str = Form(default="column", description="发际线提取方式:column | contour(仅 pushed 生效),默认 column"),
|
||||
):
|
||||
"""接口12:发际线生发(固定 multiband/mb_levels=5/erode_cm=0.6,仅返回最终图)。"""
|
||||
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
|
||||
if e is not None:
|
||||
return e
|
||||
|
||||
image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
|
||||
if image is None:
|
||||
return err(1008, "图片格式不支持(仅 JPG / PNG)")
|
||||
|
||||
try:
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from face_analysis.hairline_grow import generate_hairline_grow, NoFaceError, SwapError
|
||||
try:
|
||||
# 固定三项:blend_method=multiband、mb_levels=5、erode_cm=0.6
|
||||
data = await run_in_threadpool(
|
||||
generate_hairline_grow, image, hairline_id, is_hr, seg_model,
|
||||
mask_type, 0.6, swap_mode, "multiband", feather_px, edge_erode_px,
|
||||
denoising_strength, gen_backend, hairgrow_strength, color_match, 5,
|
||||
hairline_push_cm=hairline_push_cm, hairline_edge=hairline_edge)
|
||||
except NoFaceError:
|
||||
return err(1001, "无法识别人像")
|
||||
except SwapError as se:
|
||||
return err(1007, f"换发型失败:{se}")
|
||||
# 精简返回:只取最终合成图,丢掉接口11 的全部分步可视化
|
||||
return ok({
|
||||
"hairline_id": data["hairline_id"],
|
||||
"image_size": data["image_size"],
|
||||
"final_base64": data["steps"]["final_base64"],
|
||||
})
|
||||
except Exception as ex: # noqa: BLE001
|
||||
logger.exception("接口12 处理异常")
|
||||
return err(1007, f"处理失败:{ex}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 调试:下载后端日志(接口11 遮罩计算全过程)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.get("/api/v1/debug/hairline_log", include_in_schema=False)
|
||||
async def download_hairline_log(rid: Optional[str] = None, tail: int = 500):
|
||||
"""返回 /home/xsl/hair/log/hairline_grow.log 的内容。
|
||||
|
||||
rid 非空时只返回该 request id 相关的行;tail 限制返回最后 N 行(默认 500)。
|
||||
供调试页"下载日志"按钮调用。
|
||||
"""
|
||||
from fastapi.responses import PlainTextResponse
|
||||
log_path = "/home/xsl/hair/log/hairline_grow.log"
|
||||
try:
|
||||
with open(log_path, encoding="utf-8") as fh:
|
||||
lines = fh.readlines()
|
||||
except FileNotFoundError:
|
||||
return PlainTextResponse("(日志文件不存在,可能服务还没处理过请求)", media_type="text/plain")
|
||||
if rid:
|
||||
lines = [l for l in lines if f"[{rid}]" in l]
|
||||
lines = lines[-tail:] if tail > 0 else lines
|
||||
return PlainTextResponse("".join(lines), media_type="text/plain; charset=utf-8")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 健康检查
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user