feat: 新增mask区域重绘功能 + 羽化贴回优化 + 发丝增强
新增功能: - inpaint_mask.py: mask区域重绘服务(enhance/pure_inpaint两种模式) 严格只在mask区做SD inpainting,发丝自然化 - /inpaint 页面 + /api/inpaint 接口:画板手绘mask + 提示词编辑 + denoising可调 - enhance_hair.webui_img2img: 新增denoising_strength参数(原硬编码0.35) 羽化贴回优化(hair_swap_manual.py 步骤⑥): - feather_px(羽化范围)+ feather_alpha(羽化强度)拆分为两个独立参数 - 改为只羽化边缘(mask内部保持硬切,仅边缘带渐变) - 新增黑底羽化对比图(纯黑背景凸显边缘过渡) 其他改动: - OSS/COS密钥脱敏:改为可选import,未设环境变量时不崩溃 - manual页面:发型列表改为本次训练的8个(含hair_flow) - enhance二次增强:重绘区改为「原图头发∪手绘mask」并外扩10%+减发际线带3cm - 发型清单文档更新:179→180个可用发型(+hair_flow) 涉及文件: - 新增: inpaint_mask.py, inpaint.html - 修改: app.py, manual.html, hair_swap_manual.py, enhance_hair.py - 修改: oss_module.py, upload_oss.py, cos_module.py(密钥脱敏) - 文档: HAIRSTYLES_AVAILABLE.md, hairstyles_available.csv
This commit is contained in:
@@ -87,6 +87,12 @@ def manual_page():
|
||||
return send_from_directory(os.path.join(BASE_DIR, "static"), "manual.html")
|
||||
|
||||
|
||||
@app.route("/inpaint")
|
||||
def inpaint_page():
|
||||
"""mask区域重绘测试页(纯发丝增强/自然重绘)"""
|
||||
return send_from_directory(os.path.join(BASE_DIR, "static"), "inpaint.html")
|
||||
|
||||
|
||||
@app.route("/api/swap", methods=["POST"])
|
||||
def api_swap_proxy():
|
||||
"""代理换发型请求到 8801(避免前端跨域问题)"""
|
||||
@@ -340,6 +346,7 @@ def api_swap_manual():
|
||||
blend_dilate=(int(bd[0]), int(bd[1])),
|
||||
seamless_dilate=(int(sd[0]), int(sd[1])),
|
||||
feather_px=int(g("feather_px", 0)),
|
||||
feather_alpha=float(g("feather_alpha", 1.0)),
|
||||
enhance=bool(g("enhance", False)),
|
||||
enhance_denoising=float(g("enhance_denoising", 0.35)),
|
||||
)
|
||||
@@ -402,14 +409,25 @@ def preview_img(hair_id):
|
||||
|
||||
@app.route("/train_src/<hair_id>")
|
||||
def train_src_img(hair_id):
|
||||
"""返回发型的训练原图(hair_type_images/<hair_id>.jpg/.png)。
|
||||
用于测试页展示发型真实样子,而非套在标准脸上的效果图。
|
||||
"""返回发型的训练原图,用于测试页展示发型真实样子。
|
||||
依次查找:hair_type_images/ → images/ → train_material/<id>/images 输入 → 预览图(兜底)。
|
||||
"""
|
||||
src_dir = "/home/xsl/change_hair/hair_type_images"
|
||||
for ext in (".jpg", ".jpeg", ".png"):
|
||||
path = os.path.join(src_dir, hair_id + ext)
|
||||
if os.path.exists(path):
|
||||
return send_from_directory(src_dir, hair_id + ext)
|
||||
# 候选源目录(按优先级)
|
||||
candidate_dirs = [
|
||||
"/home/xsl/change_hair/hair_type_images",
|
||||
"/home/xsl/change_hair/images",
|
||||
"/home/xsl/change_hair/data/batch_train_inputs/" + hair_id,
|
||||
]
|
||||
for src_dir in candidate_dirs:
|
||||
for ext in (".jpg", ".jpeg", ".png"):
|
||||
path = os.path.join(src_dir, hair_id + ext)
|
||||
if os.path.exists(path):
|
||||
return send_from_directory(src_dir, hair_id + ext)
|
||||
# 兜底:返回套脸预览图(previews/<hair_id>.jpg)
|
||||
preview_dir = os.path.join(BASE_DIR, "static", "previews")
|
||||
preview_path = os.path.join(preview_dir, hair_id + ".jpg")
|
||||
if os.path.exists(preview_path):
|
||||
return send_from_directory(preview_dir, hair_id + ".jpg")
|
||||
return ("", 404)
|
||||
|
||||
|
||||
@@ -507,6 +525,59 @@ def api_grow():
|
||||
return jsonify({"state": -1, "msg": f"生发失败: {e}"}), 500
|
||||
|
||||
|
||||
@app.route("/api/inpaint", methods=["POST"])
|
||||
def api_inpaint():
|
||||
"""mask区域重绘接口(严格只在mask区做SD inpainting)。
|
||||
|
||||
入参 JSON:
|
||||
img: 原图 base64
|
||||
mask: 手绘mask base64(白色=重绘区)
|
||||
prompt: 提示词(空则用模式默认值)
|
||||
denoising_strength: 重绘强度 0~1(默认0.5)
|
||||
mode: "enhance" 或 "pure_inpaint"(默认enhance)
|
||||
返回: {state, result, mask_used, info}
|
||||
"""
|
||||
try:
|
||||
d = request.json
|
||||
img_b64 = d.get("img", "")
|
||||
mask_b64 = d.get("mask", "")
|
||||
if not img_b64 or not mask_b64:
|
||||
return jsonify({"state": -1, "msg": "img 和 mask 不能为空(请先手绘重绘区域)"}), 400
|
||||
|
||||
img = _b64_to_ndarray(img_b64, color=True)
|
||||
mask = _b64_to_ndarray(mask_b64, color=False)
|
||||
if img is None or mask is None:
|
||||
return jsonify({"state": -1, "msg": "img 或 mask 解析失败"}), 400
|
||||
|
||||
prompt = d.get("prompt", "")
|
||||
denoising = float(d.get("denoising_strength", 0.5))
|
||||
mode = d.get("mode", "enhance")
|
||||
if mode not in ("enhance", "pure_inpaint"):
|
||||
mode = "enhance"
|
||||
|
||||
print(f"[inpaint] mode={mode}, denoising={denoising}, prompt={prompt[:40]}, img={img.shape}")
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, "/home/xsl/change_hair/project/hair_service_sd")
|
||||
from inpaint_mask import inpaint_mask as do_inpaint
|
||||
result, mask_used, info = do_inpaint(
|
||||
img, mask, prompt=prompt,
|
||||
denoising_strength=denoising, mode=mode
|
||||
)
|
||||
|
||||
_, buf = cv2.imencode(".jpg", result, [cv2.IMWRITE_JPEG_QUALITY, 95])
|
||||
result_b64 = base64.b64encode(buf).decode("utf-8")
|
||||
_, mbuf = cv2.imencode(".png", mask_used)
|
||||
mask_used_b64 = "data:image/png;base64," + base64.b64encode(mbuf).decode("utf-8")
|
||||
|
||||
return jsonify({"state": 0, "result": result_b64,
|
||||
"mask_used": mask_used_b64, "info": info})
|
||||
except Exception as e:
|
||||
print(f"[inpaint] 失败: {e}")
|
||||
traceback.print_exc()
|
||||
return jsonify({"state": -1, "msg": f"mask重绘失败: {e}"}), 500
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"生发服务启动,端口 {PORT}")
|
||||
print(f"测试页面: http://0.0.0.0:{PORT}")
|
||||
|
||||
Reference in New Issue
Block a user