feat: 接口2/5 发际线叠图改为透明 PNG(仅曲线),生发图不变

接口2(/api/v1/hair/grow)的 image_url 和接口5(/api/v1/hairline/generate)
的 image_middle/high/low_url 从「原图+白线合成 JPG」改为「透明底 PNG(仅含
发际线曲线)」,前端需叠加原图显示。grown_image_url 生发图保持不变(ComfyUI
完整人像照片)。

实现:
- app.py 新增 _rgba_png_b64() 编码 RGBA 透明层为 PNG base64(保留 alpha)
- hairline/service.py 接口2/5 改用 build_overlay_layer(返回 RGBA 透明层)
  替代 render_hairline_overlay(合成到原图)
- 网关零改动:rewrite_base64_to_url 已按 \x89PNG 魔数嗅探落盘为 .png

测试页:test_interface2/5.html 改为「原图打底 + 透明PNG 绝对定位叠加」显示
(复用 .img-stack 结构,固定叠加无开关)。

文档:接口文档.md / integration.html 更新接口2/5 图片字段说明。
测试:43 passed,接口2 image_base64 改断言为 PNG 魔数,接口5 三档叠图同。
This commit is contained in:
xsl
2026-07-13 23:38:55 +08:00
parent 28255ef7c2
commit 95c6a2d929
7 changed files with 81 additions and 40 deletions
+19 -4
View File
@@ -14,6 +14,7 @@ from typing import Any, List, Optional
import cv2
import numpy as np
from PIL import Image
from fastapi import FastAPI, File, Form, Request, UploadFile
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
@@ -229,6 +230,20 @@ def _jpg_b64(bgr) -> str:
return base64.b64encode(buf.tobytes()).decode()
def _rgba_png_b64(rgba) -> str:
"""(H,W,4) float32/uint8 RGBA 透明层 → PNG base64(保留 alpha 通道)。
用于发际线叠图/预览图:只含发际线曲线像素、背景透明,前端叠加到原图上显示。
"""
arr = np.asarray(rgba)
if arr.dtype != np.uint8:
arr = np.clip(arr, 0, 255).astype(np.uint8)
img = Image.fromarray(arr, mode="RGBA")
buf = BytesIO()
img.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode()
def _png_to_jpg_b64(png_bytes) -> str:
"""ComfyUI 返回的 PNG 字节 → 重编码为 JPG base64;无法解码则原样透传。"""
img = cv2.imdecode(np.frombuffer(png_bytes, np.uint8), cv2.IMREAD_COLOR)
@@ -723,7 +738,7 @@ async def hair_grow(
results = []
for p in items:
results.append({
"image_base64": _jpg_b64(p["image_bgr"]), # 预览图 JPG
"image_base64": _rgba_png_b64(p["overlay"]), # 发际线曲线透明 PNG
"grown_image_base64": (_png_to_jpg_b64(p["grown_png"]) # 生发图 JPG
if p["grown_png"] else None),
"hairline_type": p["hairline_type"],
@@ -1133,9 +1148,9 @@ async def hairline_generate(
ov = it["overlays"]
hairline_images.append({
"hairline_type": it["hairline_type"],
"image_middle_base64": _jpg_b64(ov["middle"]), # 发际线叠图 middle 档 JPG
"image_high_base64": _jpg_b64(ov["high"]), # 发际线叠图 high 档 JPG
"image_low_base64": _jpg_b64(ov["low"]), # 发际线叠图 low 档 JPG
"image_middle_base64": _rgba_png_b64(ov["middle"]), # 发际线曲线透明 PNG middle 档
"image_high_base64": _rgba_png_b64(ov["high"]), # 发际线曲线透明 PNG high 档
"image_low_base64": _rgba_png_b64(ov["low"]), # 发际线曲线透明 PNG low 档
"grown_image_base64": (_png_to_jpg_b64(it["grown_png"]) # 生发图 JPG(失败为 null
if it.get("grown_png") else None),
"order": it["order"],