修改接口3 加入是否用遮罩接口
This commit is contained in:
+10
-10
@@ -63,7 +63,7 @@
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"noise_seed": 690815303279000
|
||||
"noise_seed": 217742615722421
|
||||
},
|
||||
"class_type": "RandomNoise",
|
||||
"_meta": {
|
||||
@@ -238,7 +238,7 @@
|
||||
},
|
||||
"26": {
|
||||
"inputs": {
|
||||
"image": "clipspace/clipspace-painted-masked-1780841075435.png [input]"
|
||||
"image": "clipspace/clipspace-painted-masked-1781655058499.png [input]"
|
||||
},
|
||||
"class_type": "LoadImage",
|
||||
"_meta": {
|
||||
@@ -297,7 +297,7 @@
|
||||
},
|
||||
"class_type": "Mask Fill Holes",
|
||||
"_meta": {
|
||||
"title": "遮罩填充漏洞"
|
||||
"title": "Mask Fill Holes"
|
||||
}
|
||||
},
|
||||
"36": {
|
||||
@@ -309,7 +309,7 @@
|
||||
},
|
||||
"class_type": "Convert Masks to Images",
|
||||
"_meta": {
|
||||
"title": "遮罩到图像"
|
||||
"title": "Convert Masks to Images"
|
||||
}
|
||||
},
|
||||
"37": {
|
||||
@@ -322,7 +322,7 @@
|
||||
},
|
||||
"class_type": "Image To Mask",
|
||||
"_meta": {
|
||||
"title": "图像到遮罩"
|
||||
"title": "Image To Mask"
|
||||
}
|
||||
},
|
||||
"39": {
|
||||
@@ -363,7 +363,7 @@
|
||||
},
|
||||
"class_type": "ImageAndMaskPreview",
|
||||
"_meta": {
|
||||
"title": "图像与遮罩预览"
|
||||
"title": "ImageAndMaskPreview"
|
||||
}
|
||||
},
|
||||
"45": {
|
||||
@@ -385,12 +385,12 @@
|
||||
{
|
||||
"name": "A",
|
||||
"selected": true,
|
||||
"url": "/api/view?filename=rgthree.compare._temp_nljpo_00001_.png&type=temp&subfolder=&rand=0.7003773747423834"
|
||||
"url": "/api/view?filename=rgthree.compare._temp_zuixa_00119_.png&type=temp&subfolder=&rand=0.7926013627811991"
|
||||
},
|
||||
{
|
||||
"name": "B",
|
||||
"selected": true,
|
||||
"url": "/api/view?filename=rgthree.compare._temp_nljpo_00002_.png&type=temp&subfolder=&rand=0.10574778041280719"
|
||||
"url": "/api/view?filename=rgthree.compare._temp_zuixa_00120_.png&type=temp&subfolder=&rand=0.43692946883795813"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -405,12 +405,12 @@
|
||||
},
|
||||
"class_type": "Image Comparer (rgthree)",
|
||||
"_meta": {
|
||||
"title": "图像对比"
|
||||
"title": "Image Comparer (rgthree)"
|
||||
}
|
||||
},
|
||||
"60": {
|
||||
"inputs": {
|
||||
"text": "严格保留原图人物五官、面部肤色、神态、服饰、背景、原图光影与整体色调,画面除划线区域外全部细节保持原样不变;先清除画面内所有黑色标注划线,仅在原划线划定范围内生成植发术后 3 个月长发效果,新生头发生长边界刚好止于原划线位置,新发沿原划线轮廓边缘和外部原生发丝交错融合、自然无缝衔接,无生硬分界线;发色和自身原生黑发保持统一,发丝细腻写实、带有真实毛发纹理与自然光泽,发量均匀,符合植发 3 个月刚长出的轻微稀疏生长状态,毛发走向遵从头部原生头发生长规律,头发光影明暗和原图环境统一,整体写实自然,无贴片假发质感。"
|
||||
"text": "补充遮罩区补充遮罩区域内的头发,头发填满遮罩区域。发际线往下挡住额头"
|
||||
},
|
||||
"class_type": "JjkText",
|
||||
"_meta": {
|
||||
|
||||
@@ -594,6 +594,7 @@ async def hair_grow_b(
|
||||
marked_image_file: Optional[UploadFile] = File(default=None, description="划线图片文件(JPG/PNG,≤ 1 MB)"),
|
||||
marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"),
|
||||
marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"),
|
||||
use_mask: bool = Form(default=True, description="是否启用自动检测遮罩(测试对比用)。false 时跳过检测、直接送划线图(空遮罩)"),
|
||||
):
|
||||
# 划线图三选一取图(只需这一张)
|
||||
marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64)
|
||||
@@ -615,7 +616,7 @@ async def hair_grow_b(
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from hairline.service import generate_grow_b
|
||||
|
||||
res = await run_in_threadpool(generate_grow_b, marked)
|
||||
res = await run_in_threadpool(generate_grow_b, marked, use_mask)
|
||||
if res["status"] == "no_face":
|
||||
return err(1001, "无法识别人像")
|
||||
if res["status"] == "no_line":
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| marked_image_* | file / string | 是 | 已用马克笔标注发际线的图片,三选一 |
|
||||
| use_mask | bool | 否 | 是否启用自动检测遮罩,默认 `true`。`false` 时跳过划线检测、直接送划线图(空遮罩),模型仅凭手绘黑线生发,供测试对比 |
|
||||
|
||||
### 输出(data)
|
||||
|
||||
|
||||
+68
-3
@@ -214,7 +214,8 @@ def _proxy(request: Request, path: str):
|
||||
return proxy_request(request, path)
|
||||
|
||||
|
||||
# 声明各接口的 form 参数用于 OpenAPI schema(实际转发直接读 Request)
|
||||
# 声明各接口的 form 参数 → 注入 OpenAPI schema(实际转发直接读 Request body,不在此解析)。
|
||||
# 每项字段:type(file/string/boolean)、description,可选 required(默认否)/default。
|
||||
_MEASURE_FORMS = {
|
||||
"image_file": {"type": "file", "description": "上传图片文件(JPG/PNG,≤ 1 MB)"},
|
||||
"image_url": {"type": "string", "description": "图片 URL"},
|
||||
@@ -223,13 +224,31 @@ _MEASURE_FORMS = {
|
||||
|
||||
_GROW_FORMS = {
|
||||
**_MEASURE_FORMS,
|
||||
"beauty_enabled": {"type": "boolean", "description": "是否开启美颜效果"},
|
||||
"gender": {"type": "string", "description": "性别 male/female(必填)", "required": True},
|
||||
"beauty_enabled": {"type": "boolean", "description": "是否开启美颜(本期不生效)", "default": False},
|
||||
}
|
||||
|
||||
_GROW_B_FORMS = {
|
||||
"marked_image_file": {"type": "file", "description": "划线图片文件"},
|
||||
"marked_image_file": {"type": "file", "description": "划线图片文件(JPG/PNG,≤ 1 MB)"},
|
||||
"marked_image_url": {"type": "string", "description": "划线图片 URL"},
|
||||
"marked_image_base64": {"type": "string", "description": "划线图片 base64"},
|
||||
"use_mask": {"type": "boolean",
|
||||
"description": "是否启用自动检测遮罩,默认 true;false 跳过检测直接送图(空遮罩),测试对比用",
|
||||
"default": True},
|
||||
}
|
||||
|
||||
_HAIRLINE_FORMS = {
|
||||
**_MEASURE_FORMS,
|
||||
"gender": {"type": "string", "description": "性别 male/female(必填)", "required": True},
|
||||
}
|
||||
|
||||
# 路由(path, method) → form 参数表,供 custom_openapi 注入 requestBody。
|
||||
# 接口4(/api/v1/face/features) 在网关本地用 Form() 声明,FastAPI 自动生成 schema,不在此列。
|
||||
_ROUTE_FORMS = {
|
||||
("/api/v1/face/measure", "post"): _MEASURE_FORMS,
|
||||
("/api/v1/hair/grow", "post"): _GROW_FORMS,
|
||||
("/api/v1/hair/grow-b", "post"): _GROW_B_FORMS,
|
||||
("/api/v1/hairline/generate", "post"): _HAIRLINE_FORMS,
|
||||
}
|
||||
|
||||
|
||||
@@ -320,3 +339,49 @@ async def face_features(
|
||||
async def hairline_generate(request: Request):
|
||||
"""接口5:发际线PNG生成"""
|
||||
return await _proxy(request, "/api/v1/hairline/generate")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 自定义 OpenAPI:代理路由用 Request 透传、不声明 Form,FastAPI 默认生成不出入参。
|
||||
# 这里把 _ROUTE_FORMS 注入各路由的 multipart/form-data requestBody,使 /docs 入参完整。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _form_schema(forms: dict) -> dict:
|
||||
"""form 参数表 → multipart/form-data 的 JSON Schema。"""
|
||||
props, required = {}, []
|
||||
for name, spec in forms.items():
|
||||
prop = ({"type": "string", "format": "binary"} if spec["type"] == "file"
|
||||
else {"type": spec["type"]})
|
||||
if spec.get("description"):
|
||||
prop["description"] = spec["description"]
|
||||
if "default" in spec:
|
||||
prop["default"] = spec["default"]
|
||||
props[name] = prop
|
||||
if spec.get("required"):
|
||||
required.append(name)
|
||||
schema = {"type": "object", "properties": props}
|
||||
if required:
|
||||
schema["required"] = required
|
||||
return schema
|
||||
|
||||
|
||||
def custom_openapi():
|
||||
if app.openapi_schema:
|
||||
return app.openapi_schema
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
|
||||
schema = get_openapi(title=app.title, version=app.version,
|
||||
description=app.description, routes=app.routes)
|
||||
for (path, method), forms in _ROUTE_FORMS.items():
|
||||
op = schema.get("paths", {}).get(path, {}).get(method)
|
||||
if op is not None:
|
||||
op["requestBody"] = {
|
||||
"required": True,
|
||||
"content": {"multipart/form-data": {"schema": _form_schema(forms)}},
|
||||
}
|
||||
app.openapi_schema = schema
|
||||
return schema
|
||||
|
||||
|
||||
app.openapi = custom_openapi
|
||||
|
||||
+20
-13
@@ -193,26 +193,33 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str):
|
||||
return {"images": images, "best_center": best_center}
|
||||
|
||||
|
||||
def generate_grow_b(marked_bgr: np.ndarray):
|
||||
def generate_grow_b(marked_bgr: np.ndarray, use_mask: bool = True):
|
||||
"""接口3:检测医生手绘发际线 → 遮罩 → 送 ComfyUI 生发(仅需划线图一张)。
|
||||
|
||||
检测路径只用来**建遮罩**;ComfyUI 输入图用 **marked 原图**(含医生手绘线,
|
||||
工作流提示词会清除黑线再生发)。
|
||||
|
||||
use_mask(默认 True):是否启用自动检测的遮罩,用于测试对比。
|
||||
- True:检测手绘线 → 建遮罩 → alpha=255−mask(透明区=重绘区,节点44 画黄色参考区)。
|
||||
- False:跳过检测,直接送划线图,alpha 全 255(空遮罩,节点26 mask 为空),
|
||||
模型仅凭医生黑线参考生发。无需改工作流,唯一变量是遮罩。
|
||||
Returns: {"grown_png": bytes 或 None, "status": "ok"|"no_face"|"no_line"}。
|
||||
"""
|
||||
rgb = cv2.cvtColor(marked_bgr, cv2.COLOR_BGR2RGB)
|
||||
landmarks = get_landmarker().detect(rgb)
|
||||
if landmarks is None:
|
||||
return {"grown_png": None, "status": "no_face"}
|
||||
parse_map = get_parser().parse(rgb)
|
||||
path = detect_marker_hairline(marked_bgr, landmarks, parse_map)
|
||||
if path is None:
|
||||
return {"grown_png": None, "status": "no_line"}
|
||||
|
||||
h, w = marked_bgr.shape[:2]
|
||||
line_w = max(2, int(w * 0.006))
|
||||
curve_mask = path_to_curve_mask(path, h, w, thickness=max(3, line_w))
|
||||
mask = mask_from_curve(curve_mask, landmarks, parse_map)
|
||||
if use_mask:
|
||||
rgb = cv2.cvtColor(marked_bgr, cv2.COLOR_BGR2RGB)
|
||||
landmarks = get_landmarker().detect(rgb)
|
||||
if landmarks is None:
|
||||
return {"grown_png": None, "status": "no_face"}
|
||||
parse_map = get_parser().parse(rgb)
|
||||
path = detect_marker_hairline(marked_bgr, landmarks, parse_map)
|
||||
if path is None:
|
||||
return {"grown_png": None, "status": "no_line"}
|
||||
line_w = max(2, int(w * 0.006))
|
||||
curve_mask = path_to_curve_mask(path, h, w, thickness=max(3, line_w))
|
||||
mask = mask_from_curve(curve_mask, landmarks, parse_map)
|
||||
else:
|
||||
mask = np.zeros((h, w), np.uint8) # 空遮罩:alpha 全 255,跳过检测
|
||||
|
||||
buf = io.BytesIO()
|
||||
compose_comfy_rgba(marked_bgr, mask).save(buf, format="PNG") # marked 原图 + 遮罩
|
||||
|
||||
Reference in New Issue
Block a user