Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
718372dc07 | ||
|
|
3a7c3fa07b | ||
|
|
b61ea6f33b | ||
|
|
b354c8c627 | ||
|
|
fd9c41430c | ||
|
|
5101bb5f6b |
+13
@@ -65,3 +65,16 @@ gateway.log
|
|||||||
|
|
||||||
# 工作流备份文件(不入 git)
|
# 工作流备份文件(不入 git)
|
||||||
*.json.bak.*
|
*.json.bak.*
|
||||||
|
|
||||||
|
# 分辨率对比测试产物(out/ 下结果图+原图副本+日志,体积大,不入 git)
|
||||||
|
# 仅忽略 out/ 与运行期文件;测试脚本与 HTML 报告仍入库
|
||||||
|
image/compare_test/out/
|
||||||
|
image/res_test/out/
|
||||||
|
image/wave_test/out/
|
||||||
|
image/compare_test/progress.json
|
||||||
|
image/res_test/progress.json
|
||||||
|
image/wave_test/progress.json
|
||||||
|
image/compare_test/batch_test.log
|
||||||
|
image/res_test/batch_test.log
|
||||||
|
image/wave_test/batch_test.log
|
||||||
|
image/compare_test/http.log
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"60": {
|
"60": {
|
||||||
"class_type": "JjkText",
|
"class_type": "JjkText",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
"text": "填充遮罩区域的头发"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"22": {
|
"22": {
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -410,7 +410,7 @@
|
|||||||
},
|
},
|
||||||
"60": {
|
"60": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
"text": "填充遮罩区域的头发"
|
||||||
},
|
},
|
||||||
"class_type": "JjkText",
|
"class_type": "JjkText",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
|
|||||||
@@ -401,21 +401,36 @@ def _run_face_measure_data(image, variant="v1"):
|
|||||||
logger.warning("头发/耳朵分割失败,回退方案A:%s", seg_e)
|
logger.warning("头发/耳朵分割失败,回退方案A:%s", seg_e)
|
||||||
|
|
||||||
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
||||||
|
discarded = result.hairline_discarded
|
||||||
data = result.to_response()
|
data = result.to_response()
|
||||||
|
vd = result.vertical
|
||||||
if variant == "v6":
|
if variant == "v6":
|
||||||
vd = result.vertical
|
if discarded:
|
||||||
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
|
# 发际线弃用:接口6 的上庭也依赖发际线,一并置 null;只保留中/下庭。
|
||||||
# 接口6 是三庭:去掉顶庭相关字段(top_court_cm / ratios.top_court / landmarks.hair_top)
|
base_px = vd["middle_court_px"] + vd["lower_court_px"]
|
||||||
data["four_courts"]["ratios"] = {
|
data["four_courts"]["upper_court_cm"] = None
|
||||||
"upper_court": round(vd["upper_court_px"] / base_px, 3),
|
data["four_courts"]["ratios"] = {
|
||||||
"middle_court": round(vd["middle_court_px"] / base_px, 3),
|
"upper_court": None,
|
||||||
"lower_court": round(vd["lower_court_px"] / base_px, 3),
|
"middle_court": round(vd["middle_court_px"] / base_px, 3),
|
||||||
}
|
"lower_court": round(vd["lower_court_px"] / base_px, 3),
|
||||||
data["four_courts"].pop("top_court_cm", None)
|
}
|
||||||
data["face_total_height_cm"] = round(
|
data["four_courts"].pop("top_court_cm", None)
|
||||||
result.upper_cm + result.middle_cm + result.lower_cm, 2)
|
data["face_total_height_cm"] = round(
|
||||||
# 注:landmarks.hair_top 保留返回(供前端/下游定位头顶),但顶庭数值、
|
result.middle_cm + result.lower_cm, 2)
|
||||||
# 占比、标注图仍按三庭处理,显示效果不变。
|
data["landmarks"]["hairline"] = None
|
||||||
|
else:
|
||||||
|
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
|
||||||
|
# 接口6 是三庭:去掉顶庭相关字段(top_court_cm / ratios.top_court / landmarks.hair_top)
|
||||||
|
data["four_courts"]["ratios"] = {
|
||||||
|
"upper_court": round(vd["upper_court_px"] / base_px, 3),
|
||||||
|
"middle_court": round(vd["middle_court_px"] / base_px, 3),
|
||||||
|
"lower_court": round(vd["lower_court_px"] / base_px, 3),
|
||||||
|
}
|
||||||
|
data["four_courts"].pop("top_court_cm", None)
|
||||||
|
data["face_total_height_cm"] = round(
|
||||||
|
result.upper_cm + result.middle_cm + result.lower_cm, 2)
|
||||||
|
# 注:landmarks.hair_top 保留返回(供前端/下游定位头顶),但顶庭数值、
|
||||||
|
# 占比、标注图仍按三庭处理,显示效果不变。
|
||||||
|
|
||||||
# 七眼段宽度(cm)。eye1=左耳外段 eye2=左脸颊 eye3=左眼 eye4=两眼间距 eye5=右眼 eye6=右脸颊 eye7=右耳外段。
|
# 七眼段宽度(cm)。eye1=左耳外段 eye2=左脸颊 eye3=左眼 eye4=两眼间距 eye5=右眼 eye6=右脸颊 eye7=右耳外段。
|
||||||
# eye2~eye6(5段)只用内部分点,接口1/6 共用;eye1/eye7 需耳朵分割端线,仅接口1 有。
|
# eye2~eye6(5段)只用内部分点,接口1/6 共用;eye1/eye7 需耳朵分割端线,仅接口1 有。
|
||||||
@@ -430,11 +445,14 @@ def _run_face_measure_data(image, variant="v1"):
|
|||||||
data["seven_eyes"][f"eye{i + 2}"] = (
|
data["seven_eyes"][f"eye{i + 2}"] = (
|
||||||
None if (a is None or b is None) else round((b - a) / pc, 2))
|
None if (a is None or b is None) else round((b - a) / pc, 2))
|
||||||
if variant != "v6":
|
if variant != "v6":
|
||||||
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线
|
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线。
|
||||||
|
# 竖向范围:发际线弃用时用眉心做上界(hair_top 不可靠),否则用头顶。
|
||||||
from face_analysis.annotation import _ear_edges_from_mask
|
from face_analysis.annotation import _ear_edges_from_mask
|
||||||
|
top_y = (vd["brow_center"][1] if discarded
|
||||||
|
else vd["hair_top"][1])
|
||||||
head_l, head_r = _ear_edges_from_mask(
|
head_l, head_r = _ear_edges_from_mask(
|
||||||
ear_mask, hair_mask,
|
ear_mask, hair_mask,
|
||||||
result.vertical["hair_top"][1], result.vertical["chin_tip"][1],
|
top_y, vd["chin_tip"][1],
|
||||||
lcx, rcx, (lcx + rcx) / 2)
|
lcx, rcx, (lcx + rcx) / 2)
|
||||||
data["seven_eyes"]["eye1"] = (
|
data["seven_eyes"]["eye1"] = (
|
||||||
None if (head_l is None) else round((lcx - head_l) / pc, 2))
|
None if (head_l is None) else round((lcx - head_l) / pc, 2))
|
||||||
@@ -757,7 +775,7 @@ async def hair_grow(
|
|||||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
|
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
|
||||||
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
||||||
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
|
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
||||||
flux_model: Optional[str] = Form(default=None, description="Flux 模型文件名(切换模型用)。None=工作流默认;如 flux-2-klein-9b-Q5_K_M.gguf / flux-2-klein-9b-Q4_K_M.gguf / flux2.0/flux-2-klein-9b-fp8.safetensors"),
|
flux_model: Optional[str] = Form(default=None, description="Flux 模型文件名(切换模型用)。None=工作流默认;如 flux-2-klein-9b-Q5_K_M.gguf / flux-2-klein-9b-Q4_K_M.gguf / flux2.0/flux-2-klein-9b-fp8.safetensors"),
|
||||||
redraw_max_side: Optional[int] = Form(default=None, description="重绘压图长边像素。None=默认896;0=不缩图(原图直送);其他如 768/640/1024"),
|
redraw_max_side: Optional[int] = Form(default=None, description="重绘压图长边像素。None=默认896;0=不缩图(原图直送);其他如 768/640/1024"),
|
||||||
):
|
):
|
||||||
@@ -872,7 +890,7 @@ async def hair_grow_b(
|
|||||||
marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"),
|
marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"),
|
||||||
marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"),
|
marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"),
|
||||||
use_mask: bool = Form(default=True, description="是否画发际线(测试对比用)。false 时跳过划线检测、直接送划线图"),
|
use_mask: bool = Form(default=True, description="是否画发际线(测试对比用)。false 时跳过划线检测、直接送划线图"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
||||||
):
|
):
|
||||||
# 划线图三选一取图(只需这一张)
|
# 划线图三选一取图(只需这一张)
|
||||||
marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64)
|
marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64)
|
||||||
@@ -1001,10 +1019,13 @@ async def face_features(
|
|||||||
- 必填 `gender`(`male`/`female`),决定发型集合(female 5 / male 4)。
|
- 必填 `gender`(`male`/`female`),决定发型集合(female 5 / male 4)。
|
||||||
- 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`。
|
- 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`。
|
||||||
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave;`male`:1=ellipse,2=inverse_arc,3=m,4=straight。
|
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave;`male`:1=ellipse,2=inverse_arc,3=m,4=straight。
|
||||||
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。
|
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数(仅 male 路径生效)。
|
||||||
注:生发黑模板固定取 `hairline_texture_black/`(middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle。
|
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(最耗时)。
|
||||||
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(ComfyUI 生发,全流程最耗时)。
|
|
||||||
`false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,大幅降低耗时。
|
`false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,大幅降低耗时。
|
||||||
|
- **生发机制(同接口2,按性别分流)**:
|
||||||
|
`female` 走「换发型 + Flux-2 整帧重绘」(依赖 change_hair:8801 与 ComfyUI:8188);
|
||||||
|
`male` 走 ComfyUI `add_hair` 原生 inpaint。
|
||||||
|
- 可选 `flux_model` / `redraw_max_side`:同接口2(仅 female 路径生效)。
|
||||||
|
|
||||||
**返回说明**:
|
**返回说明**:
|
||||||
|
|
||||||
@@ -1081,9 +1102,11 @@ async def hairline_generate(
|
|||||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
||||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"),
|
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"),
|
||||||
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,仅 male 路径生效)"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词(同接口2,仅 male 路径生效),会替换工作流节点60的文本"),
|
||||||
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(ComfyUI 生发,最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
|
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
|
||||||
|
flux_model: Optional[str] = Form(default=None, description="Flux 模型文件名(同接口2,切换模型用)。None=工作流默认"),
|
||||||
|
redraw_max_side: Optional[int] = Form(default=None, description="重绘压图长边像素(同接口2,仅 female 路径生效)。None=默认896;0=不缩图(原图直送);其他如 768/640/1024"),
|
||||||
):
|
):
|
||||||
if gender not in ("male", "female"):
|
if gender not in ("male", "female"):
|
||||||
return err(1004, "gender 必填且只能为 male / female")
|
return err(1004, "gender 必填且只能为 male / female")
|
||||||
@@ -1108,7 +1131,9 @@ async def hairline_generate(
|
|||||||
|
|
||||||
res = await run_in_threadpool(
|
res = await run_in_threadpool(
|
||||||
generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt,
|
generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt,
|
||||||
generate_grow_image=generate_grow_image)
|
generate_grow_image=generate_grow_image,
|
||||||
|
redraw_max_side=redraw_max_side, unet_name=flux_model,
|
||||||
|
v2_defaults=_V2_FINAL_DEFAULTS)
|
||||||
if res is None:
|
if res is None:
|
||||||
return err(1001, "无法识别人像")
|
return err(1001, "无法识别人像")
|
||||||
|
|
||||||
@@ -1476,7 +1501,7 @@ async def hairline_grow_v2(
|
|||||||
inpainting_fill: int = Form(default=1, description="change_hair服务端重绘填充:0=保留原图 | 1=噪声 | 2=纯色 | 3=潜变量。默认 1"),
|
inpainting_fill: int = Form(default=1, description="change_hair服务端重绘填充:0=保留原图 | 1=噪声 | 2=纯色 | 3=潜变量。默认 1"),
|
||||||
mask_blur: int = Form(default=11, description="change_hair服务端遮罩边缘模糊像素,默认 11"),
|
mask_blur: int = Form(default=11, description="change_hair服务端遮罩边缘模糊像素,默认 11"),
|
||||||
mask_dilate_scale: float = Form(default=1.0, description="change_hair服务端遮罩膨胀缩放,默认 1.0"),
|
mask_dilate_scale: float = Form(default=1.0, description="change_hair服务端遮罩膨胀缩放,默认 1.0"),
|
||||||
comfyui_prompt: Optional[str] = Form(default=None, description="Flux-2 重绘提示词,None 用默认「填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜」"),
|
comfyui_prompt: Optional[str] = Form(default=None, description="Flux-2 重绘提示词,None 用默认「填充遮罩区域的头发」"),
|
||||||
beauty_alpha: float = Form(default=0.6, description="redraw_band 版 band 外的全脸美颜融入强度(0=band外无美颜纯用final,1≈整帧版),默认 0.6"),
|
beauty_alpha: float = Form(default=0.6, description="redraw_band 版 band 外的全脸美颜融入强度(0=band外无美颜纯用final,1≈整帧版),默认 0.6"),
|
||||||
band_lo_mult: float = Form(default=0.5, description="重绘带外推倍率下限(相对 hairline_push_cm,内轮廓=0×、原外推线=1.0×),默认 0.5"),
|
band_lo_mult: float = Form(default=0.5, description="重绘带外推倍率下限(相对 hairline_push_cm,内轮廓=0×、原外推线=1.0×),默认 0.5"),
|
||||||
band_hi_mult: float = Form(default=1.5, description="重绘带外推倍率上限(相对 hairline_push_cm),默认 1.5"),
|
band_hi_mult: float = Form(default=1.5, description="重绘带外推倍率上限(相对 hairline_push_cm),默认 1.5"),
|
||||||
@@ -1642,7 +1667,7 @@ async def hairline_grow_v2_final_v2(
|
|||||||
async def api_redraw(
|
async def api_redraw(
|
||||||
image_file: UploadFile = File(..., description="人物图片(JPG/PNG)"),
|
image_file: UploadFile = File(..., description="人物图片(JPG/PNG)"),
|
||||||
mask_file: UploadFile = File(..., description="遮罩图片(PNG,支持红/白/alpha 格式)"),
|
mask_file: UploadFile = File(..., description="遮罩图片(PNG,支持红/白/alpha 格式)"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜",
|
prompt: str = Form(default="填充遮罩区域的头发",
|
||||||
description="ComfyUI 提示词"),
|
description="ComfyUI 提示词"),
|
||||||
):
|
):
|
||||||
image_bytes = await image_file.read()
|
image_bytes = await image_file.read()
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ def gpu_used():
|
|||||||
|
|
||||||
def call(img_path, hair_num, model_file, res_val):
|
def call(img_path, hair_num, model_file, res_val):
|
||||||
fd = {"gender": "female", "hair_style": str(hair_num), "use_mask": "true",
|
fd = {"gender": "female", "hair_style": str(hair_num), "use_mask": "true",
|
||||||
"prompt": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"}
|
"prompt": "填充遮罩区域的头发"}
|
||||||
if model_file:
|
if model_file:
|
||||||
fd["flux_model"] = model_file
|
fd["flux_model"] = model_file
|
||||||
if res_val != "":
|
if res_val != "":
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ def call(img_path, model_file, res_val):
|
|||||||
"gender": "female",
|
"gender": "female",
|
||||||
"hair_style": "2", # 花瓣形
|
"hair_style": "2", # 花瓣形
|
||||||
"use_mask": "true",
|
"use_mask": "true",
|
||||||
"prompt": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜",
|
"prompt": "填充遮罩区域的头发",
|
||||||
}
|
}
|
||||||
if model_file:
|
if model_file:
|
||||||
fd["flux_model"] = model_file
|
fd["flux_model"] = model_file
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
|
|
||||||
# --- 自适应尺寸:字号/线宽/虚线/箭头按短边缩放 ---
|
# --- 自适应尺寸:字号/线宽/虚线/箭头按短边缩放 ---
|
||||||
s = min(w, h)
|
s = min(w, h)
|
||||||
font_size = max(8, round(s * 0.017)) # 字体更小
|
font_size = max(9, round(s * 0.020)) # 字号上调一档
|
||||||
line_w = max(1, round(s * 0.0022))
|
line_w = max(1, round(s * 0.0022))
|
||||||
dash_len = max(4, round(s * 0.008))
|
dash_len = max(4, round(s * 0.008))
|
||||||
gap_len = max(2, round(dash_len * 0.7)) # 虚线更稠密(间隙<划线)
|
gap_len = max(2, round(dash_len * 0.7)) # 虚线更稠密(间隙<划线)
|
||||||
@@ -212,7 +212,11 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
|
|
||||||
buf = np.zeros((h, w, 4), dtype=np.uint8)
|
buf = np.zeros((h, w, 4), dtype=np.uint8)
|
||||||
|
|
||||||
if variant == "v6":
|
# 发际线弃用(hairline_discarded):保留头顶横线,去掉发际线横线,
|
||||||
|
# 也不标顶/上庭(缺发际线作边界,算不出)。横线 = 头顶/眉心/鼻翼下缘/下巴尖。
|
||||||
|
if getattr(measure_result, "hairline_discarded", False):
|
||||||
|
order = ["hair_top", "brow_center", "nose_bottom", "chin_tip"]
|
||||||
|
elif variant == "v6":
|
||||||
order = ["hairline", "brow_center", "nose_bottom", "chin_tip"]
|
order = ["hairline", "brow_center", "nose_bottom", "chin_tip"]
|
||||||
else:
|
else:
|
||||||
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
|
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
|
||||||
@@ -239,7 +243,7 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
face_cx = (fx0 + fx1) / 2
|
face_cx = (fx0 + fx1) / 2
|
||||||
over = max(6, round(s * 0.030)) # 线超出包围盒的长度(参考图风格)
|
over = max(6, round(s * 0.030)) # 线超出包围盒的长度(参考图风格)
|
||||||
face_half = (fx1 - fx0) / 2 + over # 横线超出最外侧竖线一点
|
face_half = (fx1 - fx0) / 2 + over # 横线超出最外侧竖线一点
|
||||||
# v6 竖线纵向范围 = 发际线→下巴尖(不超出);v1 = 头顶→下巴尖并两端超出一点
|
# 竖线纵向范围:v6 = 发际线→下巴尖(不超出);v1(含发际线弃用)= 头顶→下巴尖并两端超出一点
|
||||||
v_top = fy0 if variant == "v6" else fy0 - over
|
v_top = fy0 if variant == "v6" else fy0 - over
|
||||||
v_bot = fy1 if variant == "v6" else fy1 + over
|
v_bot = fy1 if variant == "v6" else fy1 + over
|
||||||
|
|
||||||
@@ -265,19 +269,28 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
draw.text((x, ys[i]), text, fill=LINE_COLOR, font=font, anchor="lm")
|
draw.text((x, ys[i]), text, fill=LINE_COLOR, font=font, anchor="lm")
|
||||||
|
|
||||||
# --- 3b. 左侧四庭:名 + 数值两行(无 cm)+ 竖向虚线双箭头 ---
|
# --- 3b. 左侧四庭:名 + 数值两行(无 cm)+ 竖向虚线双箭头 ---
|
||||||
if variant == "v6":
|
# court_start:庭段在 order 里的起始索引。发际线弃用时 order 首位是头顶(无下界发际线,
|
||||||
|
# 顶/上庭不标),中庭从眉心开始 → 跳过 order[0]。
|
||||||
|
if getattr(measure_result, "hairline_discarded", False):
|
||||||
|
court_cm = [measure_result.middle_cm, measure_result.lower_cm]
|
||||||
|
court_name = ["中庭", "下庭"]
|
||||||
|
n_court = 2
|
||||||
|
court_start = 1
|
||||||
|
elif variant == "v6":
|
||||||
court_cm = [measure_result.upper_cm, measure_result.middle_cm, measure_result.lower_cm]
|
court_cm = [measure_result.upper_cm, measure_result.middle_cm, measure_result.lower_cm]
|
||||||
court_name = ["上庭", "中庭", "下庭"]
|
court_name = ["上庭", "中庭", "下庭"]
|
||||||
n_court = 3
|
n_court = 3
|
||||||
|
court_start = 0
|
||||||
else:
|
else:
|
||||||
court_cm = [measure_result.top_cm, measure_result.upper_cm,
|
court_cm = [measure_result.top_cm, measure_result.upper_cm,
|
||||||
measure_result.middle_cm, measure_result.lower_cm]
|
measure_result.middle_cm, measure_result.lower_cm]
|
||||||
court_name = ["顶庭", "上庭", "中庭", "下庭"]
|
court_name = ["顶庭", "上庭", "中庭", "下庭"]
|
||||||
n_court = 4
|
n_court = 4
|
||||||
|
court_start = 0
|
||||||
arrow_x = max(arrow_size + 1, fx0 - pad) # 竖箭头所在 x(脸左侧,贴近最左竖线)
|
arrow_x = max(arrow_size + 1, fx0 - pad) # 竖箭头所在 x(脸左侧,贴近最左竖线)
|
||||||
court_total = sum(court_cm) or 1.0 # 各庭占比分母 = 四庭(v6 三庭)之和
|
court_total = sum(court_cm) or 1.0 # 各庭占比分母 = 四庭(v6 三庭)之和
|
||||||
for i in range(n_court):
|
for i in range(n_court):
|
||||||
y_a, y_b = ys[i], ys[i + 1]
|
y_a, y_b = ys[court_start + i], ys[court_start + i + 1]
|
||||||
# 竖向虚线双箭头,覆盖该庭高度(略收一点避免压到横线)
|
# 竖向虚线双箭头,覆盖该庭高度(略收一点避免压到横线)
|
||||||
inset = min(arrow_size, (y_b - y_a) * 0.12)
|
inset = min(arrow_size, (y_b - y_a) * 0.12)
|
||||||
draw_dashed_line_with_arrows(
|
draw_dashed_line_with_arrows(
|
||||||
|
|||||||
@@ -1107,7 +1107,7 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg
|
|||||||
"hairline_id": hairline_id,
|
"hairline_id": hairline_id,
|
||||||
"blend_method": blend_method,
|
"blend_method": blend_method,
|
||||||
"hairline_push_cm": round(float(hairline_push_cm), 2),
|
"hairline_push_cm": round(float(hairline_push_cm), 2),
|
||||||
"comfyui_prompt": comfyui_prompt or "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜",
|
"comfyui_prompt": comfyui_prompt or "填充遮罩区域的头发",
|
||||||
"beauty_alpha": beauty_alpha,
|
"beauty_alpha": beauty_alpha,
|
||||||
"px_per_cm": round(float(px_per_cm), 4),
|
"px_per_cm": round(float(px_per_cm), 4),
|
||||||
"mask_pixels": mask_viz["mask_pixels"],
|
"mask_pixels": mask_viz["mask_pixels"],
|
||||||
|
|||||||
+93
-44
@@ -12,7 +12,7 @@ from face_analysis.calibration import (
|
|||||||
estimate_scale_factor, normalized_to_pixel, pixel_distance, _lm_list,
|
estimate_scale_factor, normalized_to_pixel, pixel_distance, _lm_list,
|
||||||
)
|
)
|
||||||
from face_analysis.face_mesh_landmarks import (
|
from face_analysis.face_mesh_landmarks import (
|
||||||
GLABELLA_9, GLABELLA_151, NOSE_BOTTOM, CHIN_TIP,
|
GLABELLA_9, NOSE_BOTTOM, CHIN_TIP,
|
||||||
LEFT_EYE_OUTER, LEFT_EYE_INNER, RIGHT_EYE_INNER, RIGHT_EYE_OUTER,
|
LEFT_EYE_OUTER, LEFT_EYE_INNER, RIGHT_EYE_INNER, RIGHT_EYE_OUTER,
|
||||||
LEFT_CHEEK, RIGHT_CHEEK, LEFT_POSITION, RIGHT_POSITION,
|
LEFT_CHEEK, RIGHT_CHEEK, LEFT_POSITION, RIGHT_POSITION,
|
||||||
)
|
)
|
||||||
@@ -24,10 +24,8 @@ _TOP_RATIO = 0.22 / 0.28 # 顶庭 ÷ 中庭(≈ 0.786)
|
|||||||
|
|
||||||
|
|
||||||
def _brow_center(lm, w, h):
|
def _brow_center(lm, w, h):
|
||||||
"""眉心 = 索引 9 / 151 中点。"""
|
"""眉心 = 索引 9(眉间上点)。"""
|
||||||
g9 = normalized_to_pixel(lm[GLABELLA_9], w, h)
|
return normalized_to_pixel(lm[GLABELLA_9], w, h)
|
||||||
g151 = normalized_to_pixel(lm[GLABELLA_151], w, h)
|
|
||||||
return (g9[0] + g151[0]) / 2, (g9[1] + g151[1]) / 2
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_vertical_landmarks(landmarks, image_width, image_height):
|
def estimate_vertical_landmarks(landmarks, image_width, image_height):
|
||||||
@@ -144,9 +142,22 @@ def measure_seven_eyes(landmarks, image_width, image_height):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def pt_or_none(vertical, name):
|
||||||
|
"""vertical dict 的点 → {"x","y"},值为 None 时返回 None。"""
|
||||||
|
v = vertical.get(name)
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
return {"x": int(round(v[0])), "y": int(round(v[1]))}
|
||||||
|
|
||||||
|
|
||||||
class MeasureResult:
|
class MeasureResult:
|
||||||
"""测量结果,提供 to_response() 输出与接口文档同构的 data 字段。"""
|
"""测量结果,提供 to_response() 输出与接口文档同构的 data 字段。"""
|
||||||
|
|
||||||
|
# 发际线弃用阈值:发际线离头顶(顶庭)< 此值时判定分割不可靠,弃用发际线。
|
||||||
|
# hairline 与 hair_top 几乎重合(如稀疏头发中轴漏检只剩一小撮),说明发际线
|
||||||
|
# 定位无意义 → 顶/上庭置 null、标注图不画头顶/发际线。
|
||||||
|
HAIRLINE_DISCARD_TOP_CM = 0.7
|
||||||
|
|
||||||
def __init__(self, vertical, eyes, px_per_cm, hairline_source, head_pose,
|
def __init__(self, vertical, eyes, px_per_cm, hairline_source, head_pose,
|
||||||
landmarks=None, image_width=None, image_height=None):
|
landmarks=None, image_width=None, image_height=None):
|
||||||
self.vertical = vertical
|
self.vertical = vertical
|
||||||
@@ -164,7 +175,14 @@ class MeasureResult:
|
|||||||
self.upper_cm = vertical["upper_court_px"] / px_per_cm
|
self.upper_cm = vertical["upper_court_px"] / px_per_cm
|
||||||
self.middle_cm = vertical["middle_court_px"] / px_per_cm
|
self.middle_cm = vertical["middle_court_px"] / px_per_cm
|
||||||
self.lower_cm = vertical["lower_court_px"] / px_per_cm
|
self.lower_cm = vertical["lower_court_px"] / px_per_cm
|
||||||
self.face_total_cm = self.top_cm + self.upper_cm + self.middle_cm + self.lower_cm
|
# 发际线弃用判定:顶庭(头顶→发际线)过小视为发际线贴近头顶、不可靠。
|
||||||
|
# 弃用时 hairline_source 改为 "discarded",face_total 只算中庭+下庭。
|
||||||
|
self.hairline_discarded = self.top_cm < self.HAIRLINE_DISCARD_TOP_CM
|
||||||
|
if self.hairline_discarded:
|
||||||
|
self.hairline_source = "discarded"
|
||||||
|
self.face_total_cm = self.middle_cm + self.lower_cm
|
||||||
|
else:
|
||||||
|
self.face_total_cm = self.top_cm + self.upper_cm + self.middle_cm + self.lower_cm
|
||||||
|
|
||||||
# 七眼厘米
|
# 七眼厘米
|
||||||
self.eye_width_cm = eyes["eye_width_px"] / px_per_cm
|
self.eye_width_cm = eyes["eye_width_px"] / px_per_cm
|
||||||
@@ -172,46 +190,77 @@ class MeasureResult:
|
|||||||
self.inter_eye_cm = eyes["inter_eye_distance_px"] / px_per_cm
|
self.inter_eye_cm = eyes["inter_eye_distance_px"] / px_per_cm
|
||||||
|
|
||||||
def to_response(self):
|
def to_response(self):
|
||||||
total_px = (self.vertical["top_court_px"] + self.vertical["upper_court_px"]
|
# 发际线弃用:顶/上庭相关字段置 null(保留键),ratio 分母只算中下庭;
|
||||||
+ self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
# landmarks.hair_top/hairline 置 null。否则按四庭正常输出。
|
||||||
fw_px = self.eyes["face_width_px"]
|
if self.hairline_discarded:
|
||||||
|
base_px = (self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
||||||
def pt(name):
|
data = {
|
||||||
x, y = self.vertical[name]
|
"face_total_height_cm": round(self.face_total_cm, 2),
|
||||||
return {"x": int(round(x)), "y": int(round(y))}
|
"four_courts": {
|
||||||
|
"top_court_cm": None,
|
||||||
data = {
|
"upper_court_cm": None,
|
||||||
"face_total_height_cm": round(self.face_total_cm, 2),
|
"middle_court_cm": round(self.middle_cm, 2),
|
||||||
"four_courts": {
|
"lower_court_cm": round(self.lower_cm, 2),
|
||||||
"top_court_cm": round(self.top_cm, 2),
|
"ratios": {
|
||||||
"upper_court_cm": round(self.upper_cm, 2),
|
"top_court": None,
|
||||||
"middle_court_cm": round(self.middle_cm, 2),
|
"upper_court": None,
|
||||||
"lower_court_cm": round(self.lower_cm, 2),
|
"middle_court": round(self.vertical["middle_court_px"] / base_px, 3),
|
||||||
"ratios": {
|
"lower_court": round(self.vertical["lower_court_px"] / base_px, 3),
|
||||||
"top_court": round(self.vertical["top_court_px"] / total_px, 3),
|
},
|
||||||
"upper_court": round(self.vertical["upper_court_px"] / total_px, 3),
|
|
||||||
"middle_court": round(self.vertical["middle_court_px"] / total_px, 3),
|
|
||||||
"lower_court": round(self.vertical["lower_court_px"] / total_px, 3),
|
|
||||||
},
|
},
|
||||||
},
|
"seven_eyes": {
|
||||||
"seven_eyes": {
|
"eye_width_cm": round(self.eye_width_cm, 2),
|
||||||
"eye_width_cm": round(self.eye_width_cm, 2),
|
"face_width_cm": round(self.face_width_cm, 2),
|
||||||
"face_width_cm": round(self.face_width_cm, 2),
|
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
||||||
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
"ratios": {
|
||||||
"ratios": {
|
"eye_width": round(self.eyes["eye_width_px"] / self.eyes["face_width_px"], 3),
|
||||||
"eye_width": round(self.eyes["eye_width_px"] / fw_px, 3),
|
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / self.eyes["face_width_px"], 3),
|
||||||
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / fw_px, 3),
|
},
|
||||||
},
|
},
|
||||||
},
|
"landmarks": {
|
||||||
"landmarks": {
|
"hair_top": None,
|
||||||
"hair_top": pt("hair_top"),
|
"hairline": None,
|
||||||
"hairline": pt("hairline"),
|
"brow_center": pt_or_none(self.vertical, "brow_center"),
|
||||||
"brow_center": pt("brow_center"),
|
"nose_bottom": pt_or_none(self.vertical, "nose_bottom"),
|
||||||
"nose_bottom": pt("nose_bottom"),
|
"chin_tip": pt_or_none(self.vertical, "chin_tip"),
|
||||||
"chin_tip": pt("chin_tip"),
|
},
|
||||||
},
|
"hairline_source": self.hairline_source,
|
||||||
"hairline_source": self.hairline_source,
|
}
|
||||||
}
|
else:
|
||||||
|
total_px = (self.vertical["top_court_px"] + self.vertical["upper_court_px"]
|
||||||
|
+ self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
||||||
|
data = {
|
||||||
|
"face_total_height_cm": round(self.face_total_cm, 2),
|
||||||
|
"four_courts": {
|
||||||
|
"top_court_cm": round(self.top_cm, 2),
|
||||||
|
"upper_court_cm": round(self.upper_cm, 2),
|
||||||
|
"middle_court_cm": round(self.middle_cm, 2),
|
||||||
|
"lower_court_cm": round(self.lower_cm, 2),
|
||||||
|
"ratios": {
|
||||||
|
"top_court": round(self.vertical["top_court_px"] / total_px, 3),
|
||||||
|
"upper_court": round(self.vertical["upper_court_px"] / total_px, 3),
|
||||||
|
"middle_court": round(self.vertical["middle_court_px"] / total_px, 3),
|
||||||
|
"lower_court": round(self.vertical["lower_court_px"] / total_px, 3),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"seven_eyes": {
|
||||||
|
"eye_width_cm": round(self.eye_width_cm, 2),
|
||||||
|
"face_width_cm": round(self.face_width_cm, 2),
|
||||||
|
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
||||||
|
"ratios": {
|
||||||
|
"eye_width": round(self.eyes["eye_width_px"] / self.eyes["face_width_px"], 3),
|
||||||
|
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / self.eyes["face_width_px"], 3),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"landmarks": {
|
||||||
|
"hair_top": pt_or_none(self.vertical, "hair_top"),
|
||||||
|
"hairline": pt_or_none(self.vertical, "hairline"),
|
||||||
|
"brow_center": pt_or_none(self.vertical, "brow_center"),
|
||||||
|
"nose_bottom": pt_or_none(self.vertical, "nose_bottom"),
|
||||||
|
"chin_tip": pt_or_none(self.vertical, "chin_tip"),
|
||||||
|
},
|
||||||
|
"hairline_source": self.hairline_source,
|
||||||
|
}
|
||||||
# left/right_position:mediapipe 21/251 号定位点(原图像素,与 landmarks 同坐标系)。
|
# left/right_position:mediapipe 21/251 号定位点(原图像素,与 landmarks 同坐标系)。
|
||||||
# landmarks 缺省(如测试直构 MeasureResult)时不输出,保持向后兼容。
|
# landmarks 缺省(如测试直构 MeasureResult)时不输出,保持向后兼容。
|
||||||
if self.landmarks is not None and self.w and self.h:
|
if self.landmarks is not None and self.w and self.h:
|
||||||
|
|||||||
+1
-1
@@ -410,7 +410,7 @@
|
|||||||
},
|
},
|
||||||
"60": {
|
"60": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
"text": "填充遮罩区域的头发"
|
||||||
},
|
},
|
||||||
"class_type": "JjkText",
|
"class_type": "JjkText",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@ from . import comfyui
|
|||||||
|
|
||||||
logger = logging.getLogger("hair.worker")
|
logger = logging.getLogger("hair.worker")
|
||||||
|
|
||||||
_DEFAULT_PROMPT = "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
_DEFAULT_PROMPT = "填充遮罩区域的头发"
|
||||||
_REPO = os.path.dirname(os.path.dirname(__file__))
|
_REPO = os.path.dirname(os.path.dirname(__file__))
|
||||||
_REPAINT_WORKFLOW = os.path.join(_REPO, "0716add-hair-api.json")
|
_REPAINT_WORKFLOW = os.path.join(_REPO, "0716add-hair-api.json")
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
|||||||
Args:
|
Args:
|
||||||
image_bytes: 人物图片字节(JPG/PNG)
|
image_bytes: 人物图片字节(JPG/PNG)
|
||||||
mask_bytes: 遮罩图片字节(支持红/白/alpha 遮罩格式)
|
mask_bytes: 遮罩图片字节(支持红/白/alpha 遮罩格式)
|
||||||
prompt: 提示词,None 用默认 "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
prompt: 提示词,None 用默认 "填充遮罩区域的头发"
|
||||||
timeout: ComfyUI 超时秒数
|
timeout: ComfyUI 超时秒数
|
||||||
front: True 时任务插到 ComfyUI 队列最前(接口2 时延敏感路径用)
|
front: True 时任务插到 ComfyUI 队列最前(接口2 时延敏感路径用)
|
||||||
unet_name: 非 None 时切换 Flux 模型(如 flux-2-klein-9b-Q5_K_M.gguf),None 用工作流默认
|
unet_name: 非 None 时切换 Flux 模型(如 flux-2-klein-9b-Q5_K_M.gguf),None 用工作流默认
|
||||||
|
|||||||
+42
-22
@@ -46,13 +46,14 @@ _BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
|||||||
# 关键:ComfyUI 单卡显存装不下 Flux(7.7G)+qwen CLIP(3.9G) 同驻,靠缓存 CLIP 文本条件避免重载。
|
# 关键:ComfyUI 单卡显存装不下 Flux(7.7G)+qwen CLIP(3.9G) 同驻,靠缓存 CLIP 文本条件避免重载。
|
||||||
# prompt 不同会使缓存失效 → 重载 CLIP 并挤出 Flux(每次 +4s)。三接口用同一字符串即可全程命中。
|
# prompt 不同会使缓存失效 → 重载 CLIP 并挤出 Flux(每次 +4s)。三接口用同一字符串即可全程命中。
|
||||||
# 与 app.py 接口2/接口3 的默认 prompt 保持一致;可用 REDRAW_PROMPT 覆盖。
|
# 与 app.py 接口2/接口3 的默认 prompt 保持一致;可用 REDRAW_PROMPT 覆盖。
|
||||||
_REDRAW_PROMPT = os.getenv("REDRAW_PROMPT", "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
_REDRAW_PROMPT = os.getenv("REDRAW_PROMPT", "填充遮罩区域的头发")
|
||||||
|
|
||||||
# 接口2 女重绘整条管线(swapHair + ComfyUI)送模型前限边。真实照片常达 1257x1495:
|
# 接口2 女重绘整条管线(swapHair + ComfyUI)送模型前限边。真实照片常达 1257x1495:
|
||||||
# 全分辨率 ComfyUI 重绘要 13~21s 且激活显存把模型挤出。女性路径含 swapHair(SD WebUI ~5.3s
|
# 全分辨率 ComfyUI 重绘要 13~21s 且激活显存把模型挤出。
|
||||||
# 固定地板) + ComfyUI 两段串行。1024 档画质更好但部分大图会踩 12s 线,
|
# 策略:输入图长边 > REDRAW_MAX_SIDE 才等比缩到该长边;≤ 时原图分辨率直送(不放大)。
|
||||||
# 默认压到 896 兜底(ComfyUI ~4s,女性总耗时 9~11s);追画质可设 REDRAW_MAX_SIDE=1024。
|
# 默认 1024:大于 1024 的图压到 1024(画质/速度均衡),≤1024 的小图保持原分辨率重绘。
|
||||||
_REDRAW_MAX_SIDE = int(os.getenv("REDRAW_MAX_SIDE", "896"))
|
# 可用 REDRAW_MAX_SIDE 覆盖;0=永不缩图(原图直送)。
|
||||||
|
_REDRAW_MAX_SIDE = int(os.getenv("REDRAW_MAX_SIDE", "1024"))
|
||||||
|
|
||||||
def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0,
|
def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0,
|
||||||
max_side=None, unet_name=None):
|
max_side=None, unet_name=None):
|
||||||
@@ -410,14 +411,22 @@ def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
|
|||||||
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
||||||
hair_styles: list[int], use_mask: bool = True,
|
hair_styles: list[int], use_mask: bool = True,
|
||||||
prompt: str | None = None,
|
prompt: str | None = None,
|
||||||
generate_grow_image: bool = True):
|
generate_grow_image: bool = True,
|
||||||
|
redraw_max_side: int | None = None,
|
||||||
|
unet_name: str | None = None,
|
||||||
|
v2_defaults: dict | None = None):
|
||||||
"""接口5:对选中发型返回 middle/high/low 三档发际线透明叠图 + 生发图(同接口2)。
|
"""接口5:对选中发型返回 middle/high/low 三档发际线透明叠图 + 生发图(同接口2)。
|
||||||
|
|
||||||
入参同接口2:先选 gender,再多选 hair_styles(必填,1-indexed 按贴图排序)。
|
入参同接口2:先选 gender,再多选 hair_styles(必填,1-indexed 按贴图排序)。
|
||||||
每个选中发型返回三档叠图(middle/high/low,RGBA 透明层只含发际线曲线)与一张生发图;
|
每个选中发型返回三档叠图(middle/high/low,RGBA 透明层只含发际线曲线)与一张生发图;
|
||||||
三档贴图同名,生发黑模板固定取自 hairline_texture_black/(middle),故生发目标固定 middle 档。
|
|
||||||
use_mask/prompt:同接口2 的生发参数。
|
生发机制(同接口2,按性别分流):
|
||||||
generate_grow_image(默认 True):是否生成生发图(ComfyUI,最耗时)。False 时跳过生发,
|
- female:generate_grow_results_swap(swapHair + Flux-2 整帧重绘)
|
||||||
|
- male:generate_grow_results(ComfyUI add_hair inpaint)
|
||||||
|
redraw_max_side / unet_name / v2_defaults:female 路径参数,同接口2。
|
||||||
|
male 路径仅用 unet_name;redraw_max_side/v2_defaults 对 male 无意义(忽略)。
|
||||||
|
use_mask/prompt:仅 male 路径生效(同接口2 male)。
|
||||||
|
generate_grow_image(默认 True):是否生成生发图(最耗时)。False 时跳过生发,
|
||||||
各发型 grown_png 恒为 None,可大幅降低耗时(仅留三档发际线叠图与中心点)。
|
各发型 grown_png 恒为 None,可大幅降低耗时(仅留三档发际线叠图与中心点)。
|
||||||
Returns: {"images":[{hairline_type,order,overlays:{middle,high,low}((H,W,4) RGBA 透明层),grown_png}],
|
Returns: {"images":[{hairline_type,order,overlays:{middle,high,low}((H,W,4) RGBA 透明层),grown_png}],
|
||||||
"best_centers":{"middle":(x,y),"high":(x,y),"low":(x,y)}};无人脸 None。
|
"best_centers":{"middle":(x,y),"high":(x,y),"low":(x,y)}};无人脸 None。
|
||||||
@@ -439,11 +448,27 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
|||||||
# 三档贴图表(同性别、同 key 顺序,因三个文件夹同名)
|
# 三档贴图表(同性别、同 key 顺序,因三个文件夹同名)
|
||||||
tex_by_level = {lv: get_texture_map(lv)[gender] for lv in _TEXTURE_DIRS}
|
tex_by_level = {lv: get_texture_map(lv)[gender] for lv in _TEXTURE_DIRS}
|
||||||
|
|
||||||
# use_mask=False:干净原图+空遮罩与贴图无关,只跑一次 ComfyUI,选中项复用
|
# 生发图(同接口2,按性别分流):一次性算出所有选中发型的生发图,按 order 对应回叠图。
|
||||||
# generate_grow_image=False:完全跳过生发(最耗时),grown_png 恒为 None
|
# female→generate_grow_results_swap(swapHair+Flux-2 整帧重绘);
|
||||||
shared_grown = None
|
# male→generate_grow_results(ComfyUI add_hair inpaint)。
|
||||||
if generate_grow_image and not use_mask:
|
# generate_grow_image=False 时跳过,grown_by_order 为空 dict(各发型 grown_png 恒 None)。
|
||||||
shared_grown = _grow_from_texture(image_bgr, ctx, None, use_mask=False, prompt=prompt)
|
grown_by_order: dict[int, bytes | None] = {}
|
||||||
|
if generate_grow_image:
|
||||||
|
try:
|
||||||
|
if gender == "female":
|
||||||
|
items = generate_grow_results_swap(
|
||||||
|
image_bgr, hair_styles, v2_defaults or {},
|
||||||
|
redraw_max_side=redraw_max_side, unet_name=unet_name)
|
||||||
|
else:
|
||||||
|
items = generate_grow_results(
|
||||||
|
image_bgr, gender, use_mask, prompt, hair_styles,
|
||||||
|
unet_name=unet_name)
|
||||||
|
if items is None:
|
||||||
|
return None # 无人脸(同接口2 的 None 语义)
|
||||||
|
for it in items:
|
||||||
|
grown_by_order[it["order"]] = it.get("grown_png")
|
||||||
|
except Exception as e: # noqa: BLE001 整批生发失败不拖垮叠图主结果
|
||||||
|
logger.warning("接口5 生发批量失败(gender=%s):%s", gender, e)
|
||||||
|
|
||||||
def _center_of(overlay):
|
def _center_of(overlay):
|
||||||
"""从某档发际线透明叠图取面部中轴处的发际线中点 (x,y),无像素返回 None。"""
|
"""从某档发际线透明叠图取面部中轴处的发际线中点 (x,y),无像素返回 None。"""
|
||||||
@@ -456,18 +481,13 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
|||||||
|
|
||||||
images, best_centers = [], None
|
images, best_centers = [], None
|
||||||
for s in hair_styles: # s = 1-indexed 发型序号
|
for s in hair_styles: # s = 1-indexed 发型序号
|
||||||
key, mid_path = tex_by_level["middle"][s - 1]
|
key, _mid_path = tex_by_level["middle"][s - 1]
|
||||||
overlays = {}
|
overlays = {}
|
||||||
for lv in _TEXTURE_DIRS:
|
for lv in _TEXTURE_DIRS:
|
||||||
white = load_texture_rgba(tex_by_level[lv][s - 1][1])
|
white = load_texture_rgba(tex_by_level[lv][s - 1][1])
|
||||||
overlays[lv] = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white)
|
overlays[lv] = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white)
|
||||||
# 生发:固定 middle 黑模板(generate_grow_image=False 时跳过,恒 None)
|
# 生发图:从按性别算好的结果里按 order 取(generate_grow_image=False 时缺省 None)
|
||||||
if not generate_grow_image:
|
grown_png = grown_by_order.get(s)
|
||||||
grown_png = None
|
|
||||||
elif not use_mask:
|
|
||||||
grown_png = shared_grown
|
|
||||||
else:
|
|
||||||
grown_png = _grow_from_texture(image_bgr, ctx, mid_path, use_mask=True, prompt=prompt)
|
|
||||||
images.append({"hairline_type": key, "order": s,
|
images.append({"hairline_type": key, "order": s,
|
||||||
"overlays": overlays, "grown_png": grown_png})
|
"overlays": overlays, "grown_png": grown_png})
|
||||||
# best_centers:首个选中发型三档(middle/high/low)发际线中点
|
# best_centers:首个选中发型三档(middle/high/low)发际线中点
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
=== 接口2 female 批量对比测试 ===
|
||||||
|
矩阵: 4图 × 5发型 × 2档 = 40 次
|
||||||
|
已跳过 0 个已完成项
|
||||||
|
|
||||||
|
[1/40] ▶ girl2_ellipse_default896 (side=None)... ✅ 10.8s 出图 1082x1081 (204589B)
|
||||||
|
[2/40] ▶ girl2_ellipse_origin0 (side=0)... ✅ 13.3s 出图 1088x1088 (232252B)
|
||||||
|
[3/40] ▶ girl2_flower_default896 (side=None)... ✅ 11.0s 出图 1082x1081 (210875B)
|
||||||
|
[4/40] ▶ girl2_flower_origin0 (side=0)... ✅ 12.5s 出图 1088x1088 (236073B)
|
||||||
|
[5/40] ▶ girl2_heart_default896 (side=None)... ✅ 11.9s 出图 1082x1081 (198911B)
|
||||||
|
[6/40] ▶ girl2_heart_origin0 (side=0)... ✅ 12.2s 出图 1088x1088 (249893B)
|
||||||
|
[7/40] ▶ girl2_straight_default896 (side=None)... ✅ 12.1s 出图 1082x1081 (211803B)
|
||||||
|
[8/40] ▶ girl2_straight_origin0 (side=0)... ✅ 12.3s 出图 1088x1088 (235377B)
|
||||||
|
[9/40] ▶ girl2_wave_default896 (side=None)... ✅ 12.7s 出图 1082x1081 (207736B)
|
||||||
|
[10/40] ▶ girl2_wave_origin0 (side=0)... ✅ 11.9s 出图 1088x1088 (238081B)
|
||||||
|
[11/40] ▶ girl5_ellipse_default896 (side=None)... ✅ 9.6s 出图 768x752 (105867B)
|
||||||
|
[12/40] ▶ girl5_ellipse_origin0 (side=0)... ✅ 7.3s 出图 768x752 (112963B)
|
||||||
|
[13/40] ▶ girl5_flower_default896 (side=None)... ✅ 8.0s 出图 768x752 (114378B)
|
||||||
|
[14/40] ▶ girl5_flower_origin0 (side=0)... ✅ 7.2s 出图 768x752 (108495B)
|
||||||
|
[15/40] ▶ girl5_heart_default896 (side=None)... ✅ 8.1s 出图 768x752 (113874B)
|
||||||
|
[16/40] ▶ girl5_heart_origin0 (side=0)... ✅ 7.6s 出图 768x752 (103160B)
|
||||||
|
[17/40] ▶ girl5_straight_default896 (side=None)... ✅ 8.4s 出图 768x752 (104492B)
|
||||||
|
[18/40] ▶ girl5_straight_origin0 (side=0)... ✅ 7.2s 出图 768x752 (100941B)
|
||||||
|
[19/40] ▶ girl5_wave_default896 (side=None)... ✅ 8.4s 出图 768x752 (120122B)
|
||||||
|
[20/40] ▶ girl5_wave_origin0 (side=0)... ✅ 7.7s 出图 768x752 (113282B)
|
||||||
|
[21/40] ▶ qwer_ellipse_default896 (side=None)... ✅ 10.2s 出图 1288x1678 (261812B)
|
||||||
|
[22/40] ▶ qwer_ellipse_origin0 (side=0)... ✅ 30.0s 出图 1280x1680 (331013B)
|
||||||
|
[23/40] ▶ qwer_flower_default896 (side=None)... ✅ 10.2s 出图 1288x1678 (312834B)
|
||||||
|
[24/40] ▶ qwer_flower_origin0 (side=0)... ✅ 25.0s 出图 1280x1680 (328605B)
|
||||||
|
[25/40] ▶ qwer_heart_default896 (side=None)... ✅ 11.1s 出图 1288x1678 (331337B)
|
||||||
|
[26/40] ▶ qwer_heart_origin0 (side=0)... ✅ 25.0s 出图 1280x1680 (355982B)
|
||||||
|
[27/40] ▶ qwer_straight_default896 (side=None)... ✅ 10.7s 出图 1288x1678 (281665B)
|
||||||
|
[28/40] ▶ qwer_straight_origin0 (side=0)... ✅ 23.4s 出图 1280x1680 (322183B)
|
||||||
|
[29/40] ▶ qwer_wave_default896 (side=None)... ✅ 11.1s 出图 1288x1678 (289322B)
|
||||||
|
[30/40] ▶ qwer_wave_origin0 (side=0)... ✅ 25.8s 出图 1280x1680 (351719B)
|
||||||
|
[31/40] ▶ asdf_ellipse_default896 (side=None)... ✅ 11.2s 出图 1254x1666 (285362B)
|
||||||
|
[32/40] ▶ asdf_ellipse_origin0 (side=0)... ✅ 25.7s 出图 1248x1664 (286768B)
|
||||||
|
[33/40] ▶ asdf_flower_default896 (side=None)... ✅ 10.7s 出图 1254x1666 (266946B)
|
||||||
|
[34/40] ▶ asdf_flower_origin0 (side=0)... ✅ 22.9s 出图 1248x1664 (353668B)
|
||||||
|
[35/40] ▶ asdf_heart_default896 (side=None)... ✅ 10.4s 出图 1254x1666 (271975B)
|
||||||
|
[36/40] ▶ asdf_heart_origin0 (side=0)... ✅ 22.9s 出图 1248x1664 (291983B)
|
||||||
|
[37/40] ▶ asdf_straight_default896 (side=None)... ✅ 9.8s 出图 1254x1666 (258665B)
|
||||||
|
[38/40] ▶ asdf_straight_origin0 (side=0)... ✅ 24.0s 出图 1248x1664 (279883B)
|
||||||
|
[39/40] ▶ asdf_wave_default896 (side=None)... ✅ 11.9s 出图 1254x1666 (286515B)
|
||||||
|
[40/40] ▶ asdf_wave_origin0 (side=0)... ✅ 24.3s 出图 1248x1664 (295694B)
|
||||||
|
|
||||||
|
======================================================================
|
||||||
|
汇总报告
|
||||||
|
======================================================================
|
||||||
|
|
||||||
|
--- 速度对比(秒,✓=成功 ✗=失败)---
|
||||||
|
图 发型 默认896 原图0 差值
|
||||||
|
girl2 ellipse 10.8✓ 13.3✓ +2.5
|
||||||
|
girl2 flower 11.0✓ 12.5✓ +1.5
|
||||||
|
girl2 heart 11.9✓ 12.2✓ +0.3
|
||||||
|
girl2 straight 12.1✓ 12.3✓ +0.2
|
||||||
|
girl2 wave 12.7✓ 11.9✓ -0.8
|
||||||
|
girl5 ellipse 9.6✓ 7.3✓ -2.3
|
||||||
|
girl5 flower 8.0✓ 7.2✓ -0.8
|
||||||
|
girl5 heart 8.1✓ 7.6✓ -0.5
|
||||||
|
girl5 straight 8.4✓ 7.2✓ -1.2
|
||||||
|
girl5 wave 8.4✓ 7.7✓ -0.7
|
||||||
|
qwer ellipse 10.2✓ 30.0✓ +19.8
|
||||||
|
qwer flower 10.2✓ 25.0✓ +14.8
|
||||||
|
qwer heart 11.1✓ 25.0✓ +13.9
|
||||||
|
qwer straight 10.7✓ 23.4✓ +12.7
|
||||||
|
qwer wave 11.1✓ 25.8✓ +14.7
|
||||||
|
asdf ellipse 11.2✓ 25.7✓ +14.5
|
||||||
|
asdf flower 10.7✓ 22.9✓ +12.2
|
||||||
|
asdf heart 10.4✓ 22.9✓ +12.5
|
||||||
|
asdf straight 9.8✓ 24.0✓ +14.2
|
||||||
|
asdf wave 11.9✓ 24.3✓ +12.4
|
||||||
|
|
||||||
|
结果图: /home/ubuntu/hair/image/compare_test/out/
|
||||||
|
CSV报告: /home/ubuntu/hair/image/compare_test/out/report.csv
|
||||||
|
JSON明细: /home/ubuntu/hair/image/compare_test/out/report.json
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
img,style,side,ok,elapsed_s,out_w,out_h,err
|
||||||
|
girl2,ellipse,default896,True,10.8,1082,1081,
|
||||||
|
girl2,ellipse,origin0,True,13.3,1088,1088,
|
||||||
|
girl2,flower,default896,True,11.0,1082,1081,
|
||||||
|
girl2,flower,origin0,True,12.5,1088,1088,
|
||||||
|
girl2,heart,default896,True,11.9,1082,1081,
|
||||||
|
girl2,heart,origin0,True,12.2,1088,1088,
|
||||||
|
girl2,straight,default896,True,12.1,1082,1081,
|
||||||
|
girl2,straight,origin0,True,12.3,1088,1088,
|
||||||
|
girl2,wave,default896,True,12.7,1082,1081,
|
||||||
|
girl2,wave,origin0,True,11.9,1088,1088,
|
||||||
|
girl5,ellipse,default896,True,9.6,768,752,
|
||||||
|
girl5,ellipse,origin0,True,7.3,768,752,
|
||||||
|
girl5,flower,default896,True,8.0,768,752,
|
||||||
|
girl5,flower,origin0,True,7.2,768,752,
|
||||||
|
girl5,heart,default896,True,8.1,768,752,
|
||||||
|
girl5,heart,origin0,True,7.6,768,752,
|
||||||
|
girl5,straight,default896,True,8.4,768,752,
|
||||||
|
girl5,straight,origin0,True,7.2,768,752,
|
||||||
|
girl5,wave,default896,True,8.4,768,752,
|
||||||
|
girl5,wave,origin0,True,7.7,768,752,
|
||||||
|
qwer,ellipse,default896,True,10.2,1288,1678,
|
||||||
|
qwer,ellipse,origin0,True,30.0,1280,1680,
|
||||||
|
qwer,flower,default896,True,10.2,1288,1678,
|
||||||
|
qwer,flower,origin0,True,25.0,1280,1680,
|
||||||
|
qwer,heart,default896,True,11.1,1288,1678,
|
||||||
|
qwer,heart,origin0,True,25.0,1280,1680,
|
||||||
|
qwer,straight,default896,True,10.7,1288,1678,
|
||||||
|
qwer,straight,origin0,True,23.4,1280,1680,
|
||||||
|
qwer,wave,default896,True,11.1,1288,1678,
|
||||||
|
qwer,wave,origin0,True,25.8,1280,1680,
|
||||||
|
asdf,ellipse,default896,True,11.2,1254,1666,
|
||||||
|
asdf,ellipse,origin0,True,25.7,1248,1664,
|
||||||
|
asdf,flower,default896,True,10.7,1254,1666,
|
||||||
|
asdf,flower,origin0,True,22.9,1248,1664,
|
||||||
|
asdf,heart,default896,True,10.4,1254,1666,
|
||||||
|
asdf,heart,origin0,True,22.9,1248,1664,
|
||||||
|
asdf,straight,default896,True,9.8,1254,1666,
|
||||||
|
asdf,straight,origin0,True,24.0,1248,1664,
|
||||||
|
asdf,wave,default896,True,11.9,1254,1666,
|
||||||
|
asdf,wave,origin0,True,24.3,1248,1664,
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,562 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "girl2_ellipse_default896",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "ellipse",
|
||||||
|
"style_idx": 1,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 10.8,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1082,
|
||||||
|
"out_h": 1081,
|
||||||
|
"bytes": 204589
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl2_ellipse_origin0",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "ellipse",
|
||||||
|
"style_idx": 1,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 13.3,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1088,
|
||||||
|
"out_h": 1088,
|
||||||
|
"bytes": 232252
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl2_flower_default896",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "flower",
|
||||||
|
"style_idx": 2,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 11.0,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1082,
|
||||||
|
"out_h": 1081,
|
||||||
|
"bytes": 210875
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl2_flower_origin0",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "flower",
|
||||||
|
"style_idx": 2,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 12.5,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1088,
|
||||||
|
"out_h": 1088,
|
||||||
|
"bytes": 236073
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl2_heart_default896",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "heart",
|
||||||
|
"style_idx": 3,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 11.9,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1082,
|
||||||
|
"out_h": 1081,
|
||||||
|
"bytes": 198911
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl2_heart_origin0",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "heart",
|
||||||
|
"style_idx": 3,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 12.2,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1088,
|
||||||
|
"out_h": 1088,
|
||||||
|
"bytes": 249893
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl2_straight_default896",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "straight",
|
||||||
|
"style_idx": 4,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 12.1,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1082,
|
||||||
|
"out_h": 1081,
|
||||||
|
"bytes": 211803
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl2_straight_origin0",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "straight",
|
||||||
|
"style_idx": 4,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 12.3,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1088,
|
||||||
|
"out_h": 1088,
|
||||||
|
"bytes": 235377
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl2_wave_default896",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "wave",
|
||||||
|
"style_idx": 5,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 12.7,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1082,
|
||||||
|
"out_h": 1081,
|
||||||
|
"bytes": 207736
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl2_wave_origin0",
|
||||||
|
"img": "girl2",
|
||||||
|
"style": "wave",
|
||||||
|
"style_idx": 5,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 11.9,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1088,
|
||||||
|
"out_h": 1088,
|
||||||
|
"bytes": 238081
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_ellipse_default896",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "ellipse",
|
||||||
|
"style_idx": 1,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 9.6,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 105867
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_ellipse_origin0",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "ellipse",
|
||||||
|
"style_idx": 1,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 7.3,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 112963
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_flower_default896",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "flower",
|
||||||
|
"style_idx": 2,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 8.0,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 114378
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_flower_origin0",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "flower",
|
||||||
|
"style_idx": 2,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 7.2,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 108495
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_heart_default896",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "heart",
|
||||||
|
"style_idx": 3,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 8.1,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 113874
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_heart_origin0",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "heart",
|
||||||
|
"style_idx": 3,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 7.6,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 103160
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_straight_default896",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "straight",
|
||||||
|
"style_idx": 4,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 8.4,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 104492
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_straight_origin0",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "straight",
|
||||||
|
"style_idx": 4,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 7.2,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 100941
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_wave_default896",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "wave",
|
||||||
|
"style_idx": 5,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 8.4,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 120122
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "girl5_wave_origin0",
|
||||||
|
"img": "girl5",
|
||||||
|
"style": "wave",
|
||||||
|
"style_idx": 5,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 7.7,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 768,
|
||||||
|
"out_h": 752,
|
||||||
|
"bytes": 113282
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_ellipse_default896",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "ellipse",
|
||||||
|
"style_idx": 1,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 10.2,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1288,
|
||||||
|
"out_h": 1678,
|
||||||
|
"bytes": 261812
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_ellipse_origin0",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "ellipse",
|
||||||
|
"style_idx": 1,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 30.0,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1280,
|
||||||
|
"out_h": 1680,
|
||||||
|
"bytes": 331013
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_flower_default896",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "flower",
|
||||||
|
"style_idx": 2,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 10.2,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1288,
|
||||||
|
"out_h": 1678,
|
||||||
|
"bytes": 312834
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_flower_origin0",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "flower",
|
||||||
|
"style_idx": 2,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 25.0,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1280,
|
||||||
|
"out_h": 1680,
|
||||||
|
"bytes": 328605
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_heart_default896",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "heart",
|
||||||
|
"style_idx": 3,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 11.1,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1288,
|
||||||
|
"out_h": 1678,
|
||||||
|
"bytes": 331337
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_heart_origin0",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "heart",
|
||||||
|
"style_idx": 3,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 25.0,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1280,
|
||||||
|
"out_h": 1680,
|
||||||
|
"bytes": 355982
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_straight_default896",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "straight",
|
||||||
|
"style_idx": 4,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 10.7,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1288,
|
||||||
|
"out_h": 1678,
|
||||||
|
"bytes": 281665
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_straight_origin0",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "straight",
|
||||||
|
"style_idx": 4,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 23.4,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1280,
|
||||||
|
"out_h": 1680,
|
||||||
|
"bytes": 322183
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_wave_default896",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "wave",
|
||||||
|
"style_idx": 5,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 11.1,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1288,
|
||||||
|
"out_h": 1678,
|
||||||
|
"bytes": 289322
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "qwer_wave_origin0",
|
||||||
|
"img": "qwer",
|
||||||
|
"style": "wave",
|
||||||
|
"style_idx": 5,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 25.8,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1280,
|
||||||
|
"out_h": 1680,
|
||||||
|
"bytes": 351719
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_ellipse_default896",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "ellipse",
|
||||||
|
"style_idx": 1,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 11.2,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1254,
|
||||||
|
"out_h": 1666,
|
||||||
|
"bytes": 285362
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_ellipse_origin0",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "ellipse",
|
||||||
|
"style_idx": 1,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 25.7,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1248,
|
||||||
|
"out_h": 1664,
|
||||||
|
"bytes": 286768
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_flower_default896",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "flower",
|
||||||
|
"style_idx": 2,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 10.7,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1254,
|
||||||
|
"out_h": 1666,
|
||||||
|
"bytes": 266946
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_flower_origin0",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "flower",
|
||||||
|
"style_idx": 2,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 22.9,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1248,
|
||||||
|
"out_h": 1664,
|
||||||
|
"bytes": 353668
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_heart_default896",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "heart",
|
||||||
|
"style_idx": 3,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 10.4,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1254,
|
||||||
|
"out_h": 1666,
|
||||||
|
"bytes": 271975
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_heart_origin0",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "heart",
|
||||||
|
"style_idx": 3,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 22.9,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1248,
|
||||||
|
"out_h": 1664,
|
||||||
|
"bytes": 291983
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_straight_default896",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "straight",
|
||||||
|
"style_idx": 4,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 9.8,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1254,
|
||||||
|
"out_h": 1666,
|
||||||
|
"bytes": 258665
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_straight_origin0",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "straight",
|
||||||
|
"style_idx": 4,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 24.0,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1248,
|
||||||
|
"out_h": 1664,
|
||||||
|
"bytes": 279883
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_wave_default896",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "wave",
|
||||||
|
"style_idx": 5,
|
||||||
|
"side": "default896",
|
||||||
|
"side_val": null,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 11.9,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1254,
|
||||||
|
"out_h": 1666,
|
||||||
|
"bytes": 286515
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "asdf_wave_origin0",
|
||||||
|
"img": "asdf",
|
||||||
|
"style": "wave",
|
||||||
|
"style_idx": 5,
|
||||||
|
"side": "origin0",
|
||||||
|
"side_val": 0,
|
||||||
|
"ok": true,
|
||||||
|
"elapsed": 24.3,
|
||||||
|
"err": "",
|
||||||
|
"out_w": 1248,
|
||||||
|
"out_h": 1664,
|
||||||
|
"bytes": 295694
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""接口2 female 批量对比测试:4图 × 5发型 × 3分辨率档位 = 60次请求。
|
||||||
|
串行执行,记录耗时与成败,结果图按 图_发型_档位 命名保存。
|
||||||
|
用法: python3 batch_test.py
|
||||||
|
支持断点续跑(progress.json);新增档位时只会补跑未完成项。
|
||||||
|
"""
|
||||||
|
import base64, csv, json, os, sys, time, traceback
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API = "http://127.0.0.1:8187/api/v1/hair/grow"
|
||||||
|
TOKEN = "dev-shared-secret-2026"
|
||||||
|
TIMEOUT = 600
|
||||||
|
OUT = "/home/ubuntu/hair/image/compare_test/out"
|
||||||
|
PROGRESS = "/home/ubuntu/hair/image/compare_test/progress.json"
|
||||||
|
|
||||||
|
IMG_DIR = "/home/ubuntu/hair/image"
|
||||||
|
IMAGES = [
|
||||||
|
("girl2", f"{IMG_DIR}/girl_img/girl2.jpg"),
|
||||||
|
("girl5", f"{IMG_DIR}/girl_img/girl5.jpg"),
|
||||||
|
("qwer", f"{IMG_DIR}/qwer.jpg"),
|
||||||
|
("asdf", f"{IMG_DIR}/asdf.jpg"),
|
||||||
|
]
|
||||||
|
# female: 1=ellipse 2=flower 3=heart 4=straight 5=wave
|
||||||
|
STYLES = [
|
||||||
|
(1, "ellipse"), (2, "flower"), (3, "heart"), (4, "straight"), (5, "wave"),
|
||||||
|
]
|
||||||
|
# 三档: 默认896 / 显式1024 / 原图直送0
|
||||||
|
SIDES = [
|
||||||
|
("default896", None), # 不传 → 后端默认896
|
||||||
|
("side1024", 1024), # 长边压到 1024
|
||||||
|
("origin0", 0), # 原图直送
|
||||||
|
]
|
||||||
|
SIDE_LABEL = {
|
||||||
|
"default896": "默认896",
|
||||||
|
"side1024": "1024",
|
||||||
|
"origin0": "原图0",
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_progress():
|
||||||
|
if os.path.exists(PROGRESS):
|
||||||
|
try:
|
||||||
|
return json.load(open(PROGRESS))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"done": [], "results": []}
|
||||||
|
|
||||||
|
def save_progress(prog):
|
||||||
|
json.dump(prog, open(PROGRESS, "w"), ensure_ascii=False, indent=1)
|
||||||
|
|
||||||
|
def run_one(img_name, img_path, style_idx, style_key, side_name, side_val):
|
||||||
|
"""跑单次请求,返回 dict 结果。"""
|
||||||
|
key = f"{img_name}_{style_key}_{side_name}"
|
||||||
|
with open(img_path, "rb") as f:
|
||||||
|
img_b64 = base64.b64encode(f.read()).decode()
|
||||||
|
data = {
|
||||||
|
"image_base64": "data:image/jpeg;base64," + img_b64,
|
||||||
|
"gender": "female",
|
||||||
|
"hair_style": str(style_idx),
|
||||||
|
}
|
||||||
|
if side_val is not None:
|
||||||
|
data["redraw_max_side"] = str(side_val)
|
||||||
|
t0 = time.time()
|
||||||
|
rec = {"key": key, "img": img_name, "style": style_key, "style_idx": style_idx,
|
||||||
|
"side": side_name, "side_val": side_val, "ok": False,
|
||||||
|
"elapsed": 0.0, "err": "", "out_w": 0, "out_h": 0}
|
||||||
|
try:
|
||||||
|
r = requests.post(API, data=data, headers={"X-Internal-Token": TOKEN}, timeout=TIMEOUT)
|
||||||
|
rec["elapsed"] = round(time.time() - t0, 1)
|
||||||
|
d = r.json()
|
||||||
|
if d.get("code") != 0:
|
||||||
|
rec["err"] = f"code={d.get('code')} {d.get('message','')}"[:200]
|
||||||
|
return rec
|
||||||
|
results = (d.get("data") or {}).get("results") or []
|
||||||
|
if not results:
|
||||||
|
rec["err"] = "空结果"
|
||||||
|
return rec
|
||||||
|
it = results[0]
|
||||||
|
grown = it.get("grown_image_base64")
|
||||||
|
if not grown:
|
||||||
|
rec["err"] = "无生发图(grown_png=None, 重绘失败)"
|
||||||
|
return rec
|
||||||
|
raw = base64.b64decode(grown)
|
||||||
|
from PIL import Image
|
||||||
|
import io as _io
|
||||||
|
im = Image.open(_io.BytesIO(raw))
|
||||||
|
rec["out_w"], rec["out_h"] = im.size
|
||||||
|
out_path = f"{OUT}/{key}.jpg"
|
||||||
|
with open(out_path, "wb") as fo:
|
||||||
|
fo.write(raw)
|
||||||
|
rec["ok"] = True
|
||||||
|
rec["bytes"] = len(raw)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
rec["elapsed"] = round(time.time() - t0, 1)
|
||||||
|
rec["err"] = f"超时(>{TIMEOUT}s)"
|
||||||
|
except Exception as e:
|
||||||
|
rec["elapsed"] = round(time.time() - t0, 1)
|
||||||
|
rec["err"] = f"{type(e).__name__}: {str(e)[:180]}"
|
||||||
|
return rec
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
prog = load_progress()
|
||||||
|
done_keys = set(prog["done"])
|
||||||
|
total = len(IMAGES) * len(STYLES) * len(SIDES)
|
||||||
|
print(f"=== 接口2 female 批量对比测试 ===")
|
||||||
|
print(f"矩阵: {len(IMAGES)}图 × {len(STYLES)}发型 × {len(SIDES)}档 = {total} 次")
|
||||||
|
print(f"已跳过 {len(done_keys)} 个已完成项\n")
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
for img_name, img_path in IMAGES:
|
||||||
|
for style_idx, style_key in STYLES:
|
||||||
|
for side_name, side_val in SIDES:
|
||||||
|
idx += 1
|
||||||
|
key = f"{img_name}_{style_key}_{side_name}"
|
||||||
|
if key in done_keys:
|
||||||
|
print(f"[{idx}/{total}] ⏭ 跳过已完成 {key}")
|
||||||
|
continue
|
||||||
|
print(f"[{idx}/{total}] ▶ {key} (side={side_val})...", end=" ", flush=True)
|
||||||
|
rec = run_one(img_name, img_path, style_idx, style_key, side_name, side_val)
|
||||||
|
prog["results"].append(rec)
|
||||||
|
prog["done"].append(key)
|
||||||
|
save_progress(prog)
|
||||||
|
if rec["ok"]:
|
||||||
|
print(f"✅ {rec['elapsed']}s 出图 {rec['out_w']}x{rec['out_h']} ({rec['bytes']}B)")
|
||||||
|
else:
|
||||||
|
print(f"❌ {rec['elapsed']}s {rec['err']}")
|
||||||
|
time.sleep(2) # 串行间隔,避免队列堆积
|
||||||
|
|
||||||
|
# 汇总
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("汇总报告")
|
||||||
|
print("="*70)
|
||||||
|
write_report(prog["results"])
|
||||||
|
print(f"\n结果图: {OUT}/")
|
||||||
|
print(f"CSV报告: {OUT}/report.csv")
|
||||||
|
print(f"JSON明细: {OUT}/report.json")
|
||||||
|
|
||||||
|
def write_report(results):
|
||||||
|
# CSV
|
||||||
|
csv_path = f"{OUT}/report.csv"
|
||||||
|
with open(csv_path, "w", newline="") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(["img","style","side","ok","elapsed_s","out_w","out_h","err"])
|
||||||
|
for r in results:
|
||||||
|
w.writerow([r["img"],r["style"],r["side"],r["ok"],r["elapsed"],
|
||||||
|
r["out_w"],r["out_h"],r["err"]])
|
||||||
|
# JSON
|
||||||
|
json.dump(results, open(f"{OUT}/report.json","w"), ensure_ascii=False, indent=1)
|
||||||
|
# 控制台速度对比表
|
||||||
|
print("\n--- 速度对比(秒,✓=成功 ✗=失败)---")
|
||||||
|
headers = ["图", "发型"] + [SIDE_LABEL[s] for s, _ in SIDES]
|
||||||
|
print(f"{headers[0]:<8}{headers[1]:<10}" + "".join(f"{h:>12}" for h in headers[2:]))
|
||||||
|
for img_name, _ in IMAGES:
|
||||||
|
for _, style_key in STYLES:
|
||||||
|
cells = []
|
||||||
|
for side_name, _ in SIDES:
|
||||||
|
r = next((x for x in results if x["img"]==img_name and x["style"]==style_key and x["side"]==side_name), None)
|
||||||
|
cells.append(f"{r['elapsed']}{'✓' if r['ok'] else '✗'}" if r else "-")
|
||||||
|
print(f"{img_name:<8}{style_key:<10}" + "".join(f"{c:>12}" for c in cells))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""把 report.json + 结果图生成为自包含 HTML 报告(支持 896 / 1024 / 原图0 三档)。"""
|
||||||
|
import json, os, html
|
||||||
|
|
||||||
|
OUT = "/home/ubuntu/hair/image/compare_test/out"
|
||||||
|
REPORT_HTML = os.path.join(OUT, "report.html")
|
||||||
|
results = json.load(open(os.path.join(OUT, "report.json")))
|
||||||
|
|
||||||
|
IMAGES = ["girl2", "girl5", "qwer", "asdf"]
|
||||||
|
STYLES = ["ellipse", "flower", "heart", "straight", "wave"]
|
||||||
|
SIDES = [
|
||||||
|
("default896", "默认 896", "#2563eb", "d"),
|
||||||
|
("side1024", "1024", "#10b981", "m"),
|
||||||
|
("origin0", "原图直送 0", "#f59e0b", "o"),
|
||||||
|
]
|
||||||
|
IMG_LONGSIDE = {"girl2": 1082, "girl5": 767, "qwer": 1678, "asdf": 1666}
|
||||||
|
STYLE_CN = {"ellipse": "椭圆", "flower": "花瓣", "heart": "心形", "straight": "直线", "wave": "波浪"}
|
||||||
|
|
||||||
|
def get(img, style, side):
|
||||||
|
for r in results:
|
||||||
|
if r["img"] == img and r["style"] == style and r["side"] == side:
|
||||||
|
return r
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 总览统计(>60s 视为冷启动/异常,不进均值)
|
||||||
|
total = len(results)
|
||||||
|
ok = sum(1 for r in results if r["ok"])
|
||||||
|
COLD_S = 60.0
|
||||||
|
|
||||||
|
def avg(seq):
|
||||||
|
seq = [x for x in seq if x is not None]
|
||||||
|
return sum(seq) / len(seq) if seq else 0
|
||||||
|
|
||||||
|
def times(side, img=None):
|
||||||
|
out = []
|
||||||
|
for r in results:
|
||||||
|
if not r["ok"] or r["side"] != side:
|
||||||
|
continue
|
||||||
|
if img is not None and r["img"] != img:
|
||||||
|
continue
|
||||||
|
if r["elapsed"] >= COLD_S:
|
||||||
|
continue
|
||||||
|
out.append(r["elapsed"])
|
||||||
|
return out
|
||||||
|
|
||||||
|
cold_n = sum(1 for r in results if r["ok"] and r["elapsed"] >= COLD_S)
|
||||||
|
side_avgs = {sid: avg(times(sid)) for sid, *_ in SIDES}
|
||||||
|
all_times = [t for sid, *_ in SIDES for t in times(sid)]
|
||||||
|
max_bar = max(all_times + [1])
|
||||||
|
|
||||||
|
# 构造速度对比图数据:每图 × 三档
|
||||||
|
chart_rows = []
|
||||||
|
for img in IMAGES:
|
||||||
|
chart_rows.append((img, IMG_LONGSIDE[img], [avg(times(sid, img)) for sid, *_ in SIDES]))
|
||||||
|
|
||||||
|
def bar_svg():
|
||||||
|
bar_h = 22
|
||||||
|
gap = 12
|
||||||
|
label_w = 78
|
||||||
|
chart_w = 760
|
||||||
|
n_bars = len(SIDES)
|
||||||
|
rows = len(chart_rows)
|
||||||
|
h = rows * (bar_h * n_bars + gap) + 40
|
||||||
|
parts = [f'<svg viewBox="0 0 {label_w + chart_w + 80} {h}" class="chart">']
|
||||||
|
y = 10
|
||||||
|
for img, longside, avgs in chart_rows:
|
||||||
|
base = avgs[0] if avgs and avgs[0] else 1
|
||||||
|
for i, ((sid, label, color, _), a) in enumerate(zip(SIDES, avgs)):
|
||||||
|
yi = y + i * bar_h
|
||||||
|
w = int(a / max_bar * chart_w) if a else 0
|
||||||
|
warn = "⚠" if (i > 0 and a > base * 1.8) else ""
|
||||||
|
parts.append(f'<rect x="{label_w}" y="{yi}" width="{w}" height="{bar_h-4}" rx="3" fill="{color}"/>')
|
||||||
|
parts.append(f'<text x="{label_w + w + 6}" y="{yi + bar_h - 10}" class="barlabel">{a:.1f}s {warn}</text>')
|
||||||
|
parts.append(f'<text x="{label_w-8}" y="{yi + bar_h - 10}" class="rowlabel" text-anchor="end">{html.escape(label)}</text>')
|
||||||
|
parts.append(f'<text x="0" y="{y + bar_h - 2}" class="imglabel">{img}<tspan class="imgside">长边{longside}</tspan></text>')
|
||||||
|
y += bar_h * n_bars + gap
|
||||||
|
parts.append("</svg>")
|
||||||
|
return "".join(parts)
|
||||||
|
|
||||||
|
def img_cell(r, cls):
|
||||||
|
if not r:
|
||||||
|
return f'<td class="{cls} fail">缺失</td>'
|
||||||
|
fname = r["key"] + ".jpg"
|
||||||
|
status = "✅" if r["ok"] else "❌"
|
||||||
|
t = f'{r["elapsed"]}s'
|
||||||
|
dim = f'{r["out_w"]}×{r["out_h"]}'
|
||||||
|
err = f'<div class="err">{html.escape(r["err"])}</div>' if r["err"] else ""
|
||||||
|
img_tag = (f'<img loading="lazy" src="{fname}" onclick="openImg(this.src)" alt="{html.escape(r["key"])}">'
|
||||||
|
if r["ok"] else '<div class="noimg">无图</div>')
|
||||||
|
return (f'<td class="{cls}"><div class="thumb">{img_tag}</div>'
|
||||||
|
f'<div class="meta">{status} {t} · {dim}</div>{err}</td>')
|
||||||
|
|
||||||
|
def compare_cards():
|
||||||
|
out = []
|
||||||
|
for img in IMAGES:
|
||||||
|
out.append(f'<div class="card"><div class="card-h">📷 {html.escape(img)} <span class="tag">原图长边 {IMG_LONGSIDE[img]}px</span></div><div class="card-b">')
|
||||||
|
heads = "".join(f'<th class="{cls}">{html.escape(label)}</th>' for _, label, _, cls in SIDES)
|
||||||
|
out.append(f'<table class="cmp"><thead><tr><th>发型</th>{heads}</tr></thead><tbody>')
|
||||||
|
for style in STYLES:
|
||||||
|
sc = STYLE_CN[style]
|
||||||
|
cells = "".join(img_cell(get(img, style, sid), cls) for sid, _, _, cls in SIDES)
|
||||||
|
out.append(f'<tr><td class="sname">{html.escape(style)}<span>{sc}</span></td>{cells}</tr>')
|
||||||
|
out.append("</tbody></table></div></div>")
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
# 结论
|
||||||
|
small = [img for img in IMAGES if IMG_LONGSIDE[img] <= 1100]
|
||||||
|
large = [img for img in IMAGES if IMG_LONGSIDE[img] > 1100]
|
||||||
|
def ratio_range(num_side, den_side="default896"):
|
||||||
|
ratios = []
|
||||||
|
for img in large:
|
||||||
|
den = avg(times(den_side, img))
|
||||||
|
num = avg(times(num_side, img))
|
||||||
|
if den:
|
||||||
|
ratios.append(num / den)
|
||||||
|
if not ratios:
|
||||||
|
return 0, 0
|
||||||
|
return min(ratios), max(ratios)
|
||||||
|
|
||||||
|
r1024_lo, r1024_hi = ratio_range("side1024")
|
||||||
|
r0_lo, r0_hi = ratio_range("origin0")
|
||||||
|
cold_note = f"冷启动 {cold_n} 次(≥{COLD_S:.0f}s)已从均值剔除。" if cold_n else ""
|
||||||
|
conclusion = (
|
||||||
|
f"原图长边 ≤ 1100({'/'.join(small)})时三档耗时接近;"
|
||||||
|
f"长边 > 1600({'/'.join(large)})时相对默认896:"
|
||||||
|
f"<b>1024 约 {r1024_lo:.1f}~{r1024_hi:.1f}×</b>,"
|
||||||
|
f"<b>原图直送约 {r0_lo:.1f}~{r0_hi:.1f}×</b>。"
|
||||||
|
f"{cold_note}画质对比见下方三列并排,点击可放大。"
|
||||||
|
)
|
||||||
|
|
||||||
|
avg896 = side_avgs.get("default896", 0)
|
||||||
|
avg1024 = side_avgs.get("side1024", 0)
|
||||||
|
avg0 = side_avgs.get("origin0", 0)
|
||||||
|
ratio1024 = avg1024 / avg896 if avg896 else 0
|
||||||
|
ratio0 = avg0 / avg896 if avg896 else 0
|
||||||
|
|
||||||
|
legend = "".join(
|
||||||
|
f'<span><i style="background:{color}"></i>{html.escape(label)}</span>'
|
||||||
|
for _, label, color, _ in SIDES
|
||||||
|
)
|
||||||
|
|
||||||
|
html_doc = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>接口2 分辨率对比测试报告</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f3f4f6; color: #1f2937; line-height: 1.5; padding: 20px; }}
|
||||||
|
.wrap {{ max-width: 1400px; margin: 0 auto; }}
|
||||||
|
h1 {{ font-size: 24px; margin-bottom: 4px; }}
|
||||||
|
.sub {{ color: #6b7280; font-size: 13px; margin-bottom: 20px; }}
|
||||||
|
.summary {{ display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 24px; }}
|
||||||
|
.stat {{ background: #fff; border-radius: 10px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }}
|
||||||
|
.stat .num {{ font-size: 26px; font-weight: 700; }}
|
||||||
|
.stat .lbl {{ font-size: 12px; color: #6b7280; margin-top: 2px; }}
|
||||||
|
.stat.d .num {{ color: #2563eb; }}
|
||||||
|
.stat.m .num {{ color: #10b981; }}
|
||||||
|
.stat.o .num {{ color: #f59e0b; }}
|
||||||
|
.card {{ background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.07); margin-bottom: 20px; overflow: hidden; }}
|
||||||
|
.card-h {{ padding: 12px 18px; background: #fafafa; border-bottom: 1px solid #f0f0f0; font-weight: 700; font-size: 15px; display: flex; align-items: center; gap: 8px; }}
|
||||||
|
.card-h .tag {{ font-size: 11px; font-weight: 500; color: #6b7280; background: #f3f4f6; padding: 2px 8px; border-radius: 10px; }}
|
||||||
|
.card-b {{ padding: 16px; }}
|
||||||
|
.chart {{ width: 100%; height: auto; max-width: 920px; }}
|
||||||
|
.barlabel {{ font-size: 12px; fill: #374151; font-weight: 600; }}
|
||||||
|
.rowlabel {{ font-size: 11px; fill: #6b7280; }}
|
||||||
|
.imglabel {{ font-size: 14px; fill: #111; font-weight: 700; }}
|
||||||
|
.imgside {{ font-size: 10px; fill: #9ca3af; font-weight: 400; }}
|
||||||
|
table.cmp {{ width: 100%; border-collapse: collapse; table-layout: fixed; }}
|
||||||
|
table.cmp th {{ font-size: 12px; color: #6b7280; font-weight: 600; padding: 8px; text-align: center; border-bottom: 2px solid #f0f0f0; }}
|
||||||
|
table.cmp td {{ padding: 8px; border-bottom: 1px solid #f6f6f6; vertical-align: top; text-align: center; }}
|
||||||
|
table.cmp th.d, table.cmp td.d {{ background: #eff6ff; }}
|
||||||
|
table.cmp th.m, table.cmp td.m {{ background: #ecfdf5; }}
|
||||||
|
table.cmp th.o, table.cmp td.o {{ background: #fffbeb; }}
|
||||||
|
td.sname {{ font-weight: 600; text-align: left; width: 90px; }}
|
||||||
|
td.sname span {{ display: block; font-size: 11px; color: #9ca3af; font-weight: 400; }}
|
||||||
|
.thumb {{ background: #222; border-radius: 6px; overflow: hidden; margin-bottom: 4px; cursor: zoom-in; }}
|
||||||
|
.thumb img {{ width: 100%; height: 200px; object-fit: contain; display: block; }}
|
||||||
|
.noimg {{ color: #d1d5db; font-size: 12px; padding: 40px 0; }}
|
||||||
|
.meta {{ font-size: 11px; color: #6b7280; }}
|
||||||
|
.err {{ font-size: 10px; color: #dc2626; margin-top: 2px; }}
|
||||||
|
.note {{ background: #fef3c7; border-left: 3px solid #f59e0b; padding: 12px 16px; border-radius: 6px; font-size: 13px; margin-bottom: 20px; }}
|
||||||
|
.legend {{ display: flex; gap: 20px; font-size: 12px; color: #6b7280; margin-bottom: 12px; flex-wrap: wrap; }}
|
||||||
|
.legend span {{ display: inline-flex; align-items: center; gap: 5px; }}
|
||||||
|
.legend i {{ width: 12px; height: 12px; border-radius: 2px; display: inline-block; }}
|
||||||
|
.overlay {{ display: none; position: fixed; inset: 0; background: rgba(0,0,0,.9); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; padding: 30px; }}
|
||||||
|
.overlay.active {{ display: flex; }}
|
||||||
|
.overlay img {{ max-width: 95%; max-height: 95%; object-fit: contain; border-radius: 4px; }}
|
||||||
|
@media (max-width: 900px) {{
|
||||||
|
.summary {{ grid-template-columns: repeat(2, 1fr); }}
|
||||||
|
.thumb img {{ height: 160px; }}
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>接口2 生发 · 分辨率对比测试报告</h1>
|
||||||
|
<p class="sub">POST /api/v1/hair/grow · female · 4 张图 × 5 发型 × 3 档分辨率 = {total} 次 · 串行</p>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<div class="stat"><div class="num">{ok}/{total}</div><div class="lbl">成功 / 总数</div></div>
|
||||||
|
<div class="stat d"><div class="num">{avg896:.1f}s</div><div class="lbl">默认896 平均</div></div>
|
||||||
|
<div class="stat m"><div class="num">{avg1024:.1f}s</div><div class="lbl">1024 平均 · ×{ratio1024:.2f}</div></div>
|
||||||
|
<div class="stat o"><div class="num">{avg0:.1f}s</div><div class="lbl">原图直送 平均 · ×{ratio0:.2f}</div></div>
|
||||||
|
<div class="stat"><div class="num" style="color:{'#dc2626' if ratio0>1.3 else '#16a34a'}">×{ratio0:.2f}</div><div class="lbl">原图/默认 倍率</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
<b>结论:</b>{conclusion}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-h">平均耗时对比(按图分组,单位:秒)</div>
|
||||||
|
<div class="card-b">
|
||||||
|
<div class="legend">{legend}</div>
|
||||||
|
{bar_svg()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="font-size:18px;margin:8px 0 14px">画质对比(左→右:默认896 · 1024 · 原图直送)</h2>
|
||||||
|
{compare_cards()}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="overlay" id="overlay" onclick="this.classList.remove('active')">
|
||||||
|
<img id="overlayImg" src="">
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function openImg(src) {{
|
||||||
|
document.getElementById('overlayImg').src = src;
|
||||||
|
document.getElementById('overlay').classList.add('active');
|
||||||
|
}}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(REPORT_HTML, "w") as f:
|
||||||
|
f.write(html_doc)
|
||||||
|
# 同步首页
|
||||||
|
with open(os.path.join(OUT, "index.html"), "w") as f:
|
||||||
|
f.write(html_doc)
|
||||||
|
print(f"已生成: {REPORT_HTML}")
|
||||||
|
print(f"图片目录: {OUT}")
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=接口2 分辨率对比测试报告 HTTP 服务 (8848)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/home/ubuntu/hair/image/compare_test/out
|
||||||
|
ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8848 --bind 0.0.0.0
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>测试报告索引</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f3f4f6; color: #1f2937; padding: 40px 20px; }
|
||||||
|
.wrap { max-width: 720px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 24px; margin-bottom: 4px; }
|
||||||
|
.sub { color: #6b7280; font-size: 13px; margin-bottom: 28px; }
|
||||||
|
.card { display: block; background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.07); padding: 20px 24px; margin-bottom: 14px; text-decoration: none; color: inherit; transition: .15s; border-left: 4px solid #2563eb; }
|
||||||
|
.card:hover { transform: translateX(4px); box-shadow: 0 4px 12px rgba(0,0,0,.1); }
|
||||||
|
.card .title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
|
||||||
|
.card .desc { font-size: 13px; color: #6b7280; }
|
||||||
|
.card .url { font-size: 11px; color: #9ca3af; margin-top: 6px; font-family: monospace; }
|
||||||
|
.card.wave { border-left-color: #16a34a; }
|
||||||
|
.card.v2 { border-left-color: #f59e0b; }
|
||||||
|
.card.v1 { border-left-color: #2563eb; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>📊 测试报告索引</h1>
|
||||||
|
<p class="sub">接口2 / 接口5 分辨率对比测试报告合集</p>
|
||||||
|
|
||||||
|
<a class="card wave" href="wave/">
|
||||||
|
<div class="title">💇 wave发型 · 5档分辨率对比(最新)</div>
|
||||||
|
<div class="desc">21图 × 5档(原图/1024/896/768/640) = 105次 · wave发型 · female</div>
|
||||||
|
<div class="url">/wave/</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card v2" href="v2/">
|
||||||
|
<div class="title">💄 5图 · 5档分辨率对比 v2</div>
|
||||||
|
<div class="desc">5图 × 3发型(花瓣/心形/波浪) × 5档 = 75次 · female</div>
|
||||||
|
<div class="url">/v2/</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card v1" href="v1/">
|
||||||
|
<div class="title">💄 4图 · 2档分辨率对比 v1</div>
|
||||||
|
<div class="desc">4图 × 5发型 × 2档(默认896/原图) = 40次 · female</div>
|
||||||
|
<div class="url">/v1/</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=测试报告统一HTTP服务 (8850, 路径区分: /wave /v2 /v1)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/home/ubuntu/hair/image/reports
|
||||||
|
ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8850 --bind 0.0.0.0
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/home/ubuntu/hair/image/compare_test/out
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/home/ubuntu/hair/image/res_test/out
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/home/ubuntu/hair/image/wave_test/out
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""接口2 female 分辨率对比测试 v2: 5图 × 3发型 × 5档 = 75次。
|
||||||
|
串行执行,记录耗时与成败,结果图按 图_发型_档位 命名保存。
|
||||||
|
支持断点续跑(progress.json)。
|
||||||
|
"""
|
||||||
|
import base64, csv, json, os, sys, time, io
|
||||||
|
import requests
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
API = "http://127.0.0.1:8187/api/v1/hair/grow"
|
||||||
|
TOKEN = "dev-shared-secret-2026"
|
||||||
|
TIMEOUT = 600
|
||||||
|
OUT = "/home/ubuntu/hair/image/res_test/out"
|
||||||
|
PROGRESS = "/home/ubuntu/hair/image/res_test/progress.json"
|
||||||
|
|
||||||
|
IMG_DIR = "/home/ubuntu/hair/image"
|
||||||
|
IMAGES = [
|
||||||
|
("girl2", f"{IMG_DIR}/girl_img/girl2.jpg"),
|
||||||
|
("girl5", f"{IMG_DIR}/girl_img/girl5.jpg"),
|
||||||
|
("qwer", f"{IMG_DIR}/qwer.jpg"),
|
||||||
|
("asdf", f"{IMG_DIR}/asdf.jpg"),
|
||||||
|
("girl7", f"{IMG_DIR}/girl_img/girl7.jpg"),
|
||||||
|
]
|
||||||
|
# female: 2=flower(花瓣) 3=heart(心形) 5=wave(波浪)
|
||||||
|
STYLES = [(2, "flower"), (3, "heart"), (5, "wave")]
|
||||||
|
# 5档: 原图(0) / 1024 / 896 / 768 / 640
|
||||||
|
SIDES = [
|
||||||
|
("origin", 0),
|
||||||
|
("s1024", 1024),
|
||||||
|
("s896", 896),
|
||||||
|
("s768", 768),
|
||||||
|
("s640", 640),
|
||||||
|
]
|
||||||
|
|
||||||
|
def load_progress():
|
||||||
|
if os.path.exists(PROGRESS):
|
||||||
|
try:
|
||||||
|
return json.load(open(PROGRESS))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"done": [], "results": []}
|
||||||
|
|
||||||
|
def save_progress(prog):
|
||||||
|
json.dump(prog, open(PROGRESS, "w"), ensure_ascii=False, indent=1)
|
||||||
|
|
||||||
|
def run_one(img_name, img_path, style_idx, style_key, side_name, side_val):
|
||||||
|
key = f"{img_name}_{style_key}_{side_name}"
|
||||||
|
with open(img_path, "rb") as f:
|
||||||
|
img_b64 = base64.b64encode(f.read()).decode()
|
||||||
|
data = {
|
||||||
|
"image_base64": "data:image/jpeg;base64," + img_b64,
|
||||||
|
"gender": "female",
|
||||||
|
"hair_style": str(style_idx),
|
||||||
|
"redraw_max_side": str(side_val), # 含原图档=0
|
||||||
|
}
|
||||||
|
t0 = time.time()
|
||||||
|
rec = {"key": key, "img": img_name, "style": style_key, "style_idx": style_idx,
|
||||||
|
"side": side_name, "side_val": side_val, "ok": False,
|
||||||
|
"elapsed": 0.0, "err": "", "out_w": 0, "out_h": 0, "bytes": 0}
|
||||||
|
try:
|
||||||
|
r = requests.post(API, data=data, headers={"X-Internal-Token": TOKEN}, timeout=TIMEOUT)
|
||||||
|
rec["elapsed"] = round(time.time() - t0, 1)
|
||||||
|
d = r.json()
|
||||||
|
if d.get("code") != 0:
|
||||||
|
rec["err"] = f"code={d.get('code')} {d.get('message','')}"[:200]
|
||||||
|
return rec
|
||||||
|
results = (d.get("data") or {}).get("results") or []
|
||||||
|
if not results:
|
||||||
|
rec["err"] = "空结果"
|
||||||
|
return rec
|
||||||
|
it = results[0]
|
||||||
|
grown = it.get("grown_image_base64")
|
||||||
|
if not grown:
|
||||||
|
rec["err"] = "无生发图(重绘失败/OOM?)"
|
||||||
|
return rec
|
||||||
|
raw = base64.b64decode(grown)
|
||||||
|
im = Image.open(io.BytesIO(raw))
|
||||||
|
rec["out_w"], rec["out_h"] = im.size
|
||||||
|
out_path = f"{OUT}/{key}.jpg"
|
||||||
|
with open(out_path, "wb") as fo:
|
||||||
|
fo.write(raw)
|
||||||
|
rec["ok"] = True
|
||||||
|
rec["bytes"] = len(raw)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
rec["elapsed"] = round(time.time() - t0, 1)
|
||||||
|
rec["err"] = f"超时(>{TIMEOUT}s)"
|
||||||
|
except Exception as e:
|
||||||
|
rec["elapsed"] = round(time.time() - t0, 1)
|
||||||
|
rec["err"] = f"{type(e).__name__}: {str(e)[:180]}"
|
||||||
|
return rec
|
||||||
|
|
||||||
|
def main():
|
||||||
|
prog = load_progress()
|
||||||
|
done_keys = set(prog["done"])
|
||||||
|
total = len(IMAGES) * len(STYLES) * len(SIDES)
|
||||||
|
print(f"=== 接口2 female 分辨率对比测试 v2 ===")
|
||||||
|
print(f"矩阵: {len(IMAGES)}图 × {len(STYLES)}发型 × {len(SIDES)}档 = {total} 次")
|
||||||
|
print(f"已跳过 {len(done_keys)} 个已完成项\n")
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
for img_name, img_path in IMAGES:
|
||||||
|
for style_idx, style_key in STYLES:
|
||||||
|
for side_name, side_val in SIDES:
|
||||||
|
idx += 1
|
||||||
|
key = f"{img_name}_{style_key}_{side_name}"
|
||||||
|
if key in done_keys:
|
||||||
|
print(f"[{idx}/{total}] ⏭ 跳过 {key}")
|
||||||
|
continue
|
||||||
|
print(f"[{idx}/{total}] ▶ {key} (side={side_val})...", end=" ", flush=True)
|
||||||
|
rec = run_one(img_name, img_path, style_idx, style_key, side_name, side_val)
|
||||||
|
prog["results"].append(rec)
|
||||||
|
prog["done"].append(key)
|
||||||
|
save_progress(prog)
|
||||||
|
if rec["ok"]:
|
||||||
|
print(f"✅ {rec['elapsed']}s {rec['out_w']}x{rec['out_h']} ({rec['bytes']}B)")
|
||||||
|
else:
|
||||||
|
print(f"❌ {rec['elapsed']}s {rec['err']}")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("汇总")
|
||||||
|
print("=" * 70)
|
||||||
|
write_report(prog["results"])
|
||||||
|
print(f"\n结果图: {OUT}/")
|
||||||
|
print(f"CSV: {OUT}/report.csv JSON: {OUT}/report.json")
|
||||||
|
|
||||||
|
def write_report(results):
|
||||||
|
with open(f"{OUT}/report.csv", "w", newline="") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(["img", "style", "side", "side_val", "ok", "elapsed_s", "out_w", "out_h", "bytes", "err"])
|
||||||
|
for r in results:
|
||||||
|
w.writerow([r["img"], r["style"], r["side"], r["side_val"], r["ok"],
|
||||||
|
r["elapsed"], r["out_w"], r["out_h"], r["bytes"], r["err"]])
|
||||||
|
json.dump(results, open(f"{OUT}/report.json", "w"), ensure_ascii=False, indent=1)
|
||||||
|
|
||||||
|
# 控制台速度表:按 图×发型 分行,5档列
|
||||||
|
print("\n--- 速度对比(秒)---")
|
||||||
|
print(f"{'图·发型':<18}{'原图0':>8}{'1024':>8}{'896':>8}{'768':>8}{'640':>8}")
|
||||||
|
for img_name, _ in IMAGES:
|
||||||
|
for _, style_key in STYLES:
|
||||||
|
cells = []
|
||||||
|
for side_name, _ in SIDES:
|
||||||
|
r = next((x for x in results if x["img"] == img_name and x["style"] == style_key and x["side"] == side_name), None)
|
||||||
|
cells.append(f"{r['elapsed']}✓" if r and r["ok"] else (f"{r['elapsed']}✗" if r else "-"))
|
||||||
|
print(f"{img_name+'·'+style_key:<18}{cells[0]:>8}{cells[1]:>8}{cells[2]:>8}{cells[3]:>8}{cells[4]:>8}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""生成 v2 分辨率对比报告:每行=原图+5档,按图×发型组织15行。"""
|
||||||
|
import json, os, html
|
||||||
|
|
||||||
|
OUT = "/home/ubuntu/hair/image/res_test/out"
|
||||||
|
REPORT = os.path.join(OUT, "report.html")
|
||||||
|
results = json.load(open(os.path.join(OUT, "report.json")))
|
||||||
|
|
||||||
|
IMAGES = ["girl2", "girl5", "qwer", "asdf", "girl7"]
|
||||||
|
STYLES = [("flower", "花瓣"), ("heart", "心形"), ("wave", "波浪")]
|
||||||
|
SIDES = [("origin", "原图直送", 0), ("s1024", "1024", 1024),
|
||||||
|
("s896", "896", 896), ("s768", "768", 768), ("s640", "640", 640)]
|
||||||
|
LONGSIDE = {"girl2": 1082, "girl5": 767, "qwer": 1678, "asdf": 1666, "girl7": 925}
|
||||||
|
|
||||||
|
def get(img, style, side):
|
||||||
|
for r in results:
|
||||||
|
if r["img"] == img and r["style"] == style and r["side"] == side:
|
||||||
|
return r
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 统计
|
||||||
|
total = len(results)
|
||||||
|
ok = sum(1 for r in results if r["ok"])
|
||||||
|
|
||||||
|
# 每档平均耗时(排除冷启动异常值:girl2_flower_origin=135s 明显是冷启动)
|
||||||
|
def avg(side, exclude_first_cold=False):
|
||||||
|
ts = []
|
||||||
|
for r in results:
|
||||||
|
if r["side"] == side and r["ok"]:
|
||||||
|
if exclude_first_cold and r["key"] == "girl2_flower_origin":
|
||||||
|
continue # 跳过冷启动
|
||||||
|
ts.append(r["elapsed"])
|
||||||
|
return sum(ts) / len(ts) if ts else 0
|
||||||
|
|
||||||
|
avg_origin = avg("origin", exclude_first_cold=True)
|
||||||
|
avg_1024 = avg("s1024")
|
||||||
|
avg_896 = avg("s896")
|
||||||
|
avg_768 = avg("s768")
|
||||||
|
avg_640 = avg("s640")
|
||||||
|
avgs = [("origin", "原图直送", avg_origin, 0),
|
||||||
|
("s1024", "1024", avg_1024, 1024),
|
||||||
|
("s896", "896", avg_896, 896),
|
||||||
|
("s768", "768", avg_768, 768),
|
||||||
|
("s640", "640", avg_640, 640)]
|
||||||
|
|
||||||
|
# 速度色阶:以全部耗时的 min-max 映射颜色(绿→黄→红)
|
||||||
|
all_t = sorted([r["elapsed"] for r in results if r["ok"] and r["key"] != "girl2_flower_origin"])
|
||||||
|
tmin, tmax = all_t[0], all_t[-1]
|
||||||
|
def speed_color(t):
|
||||||
|
if tmax == tmin:
|
||||||
|
return "#16a34a"
|
||||||
|
ratio = (t - tmin) / (tmax - tmin) # 0=最快(绿) 1=最慢(红)
|
||||||
|
if ratio < 0.33:
|
||||||
|
return "#16a34a" # 绿
|
||||||
|
elif ratio < 0.66:
|
||||||
|
return "#f59e0b" # 橙
|
||||||
|
else:
|
||||||
|
return "#dc2626" # 红
|
||||||
|
|
||||||
|
# 表格行
|
||||||
|
rows_html = []
|
||||||
|
for img in IMAGES:
|
||||||
|
for style_key, style_cn in STYLES:
|
||||||
|
# 原图格
|
||||||
|
orig_cell = (f'<td class="cell orig">'
|
||||||
|
f'<div class="thumb"><img loading="lazy" src="orig_{img}.jpg" '
|
||||||
|
f'onclick="openImg(this.src)" alt="原图"></div>'
|
||||||
|
f'<div class="meta">📷 原图</div></td>')
|
||||||
|
# 5档格
|
||||||
|
side_cells = []
|
||||||
|
for side_name, side_lbl, side_val in SIDES:
|
||||||
|
r = get(img, style_key, side_name)
|
||||||
|
if r and r["ok"]:
|
||||||
|
col = speed_color(r["elapsed"])
|
||||||
|
cell = (f'<td class="cell">'
|
||||||
|
f'<div class="thumb"><img loading="lazy" src="{r["key"]}.jpg" '
|
||||||
|
f'onclick="openImg(this.src)" alt="{html.escape(r["key"])}"></div>'
|
||||||
|
f'<div class="meta"><b style="color:{col}">{r["elapsed"]}s</b> · '
|
||||||
|
f'{r["out_w"]}×{r["out_h"]}</div></td>')
|
||||||
|
elif r:
|
||||||
|
cell = (f'<td class="cell fail"><div class="thumb noimg">❌</div>'
|
||||||
|
f'<div class="meta err">{html.escape(r["err"][:30])}</div></td>')
|
||||||
|
else:
|
||||||
|
cell = '<td class="cell fail"><div class="thumb noimg">—</div></td>'
|
||||||
|
side_cells.append(cell)
|
||||||
|
row_label = (f'<td class="rowlabel">'
|
||||||
|
f'<div class="rimg">{html.escape(img)}</div>'
|
||||||
|
f'<div class="rs">{html.escape(style_cn)}</div>'
|
||||||
|
f'<div class="rl">长边 {LONGSIDE[img]}</div></td>')
|
||||||
|
rows_html.append("<tr>" + row_label + orig_cell + "".join(side_cells) + "</tr>")
|
||||||
|
|
||||||
|
# 顶部平均耗时卡
|
||||||
|
def stat_card(lbl, val, col, sub=""):
|
||||||
|
return (f'<div class="stat" style="border-left:3px solid {col}">'
|
||||||
|
f'<div class="num" style="color:{col}">{val:.1f}s</div>'
|
||||||
|
f'<div class="lbl">{lbl}{sub}</div></div>')
|
||||||
|
|
||||||
|
stat_cards = "".join([
|
||||||
|
stat_card("原图直送", avg_origin, "#dc2626", "<br><span class='dim'>排除冷启动</span>"),
|
||||||
|
stat_card("1024", avg_1024, "#f59e0b"),
|
||||||
|
stat_card("896 (默认)", avg_896, "#2563eb"),
|
||||||
|
stat_card("768", avg_768, "#16a34a"),
|
||||||
|
stat_card("640", avg_640, "#0d9488"),
|
||||||
|
])
|
||||||
|
|
||||||
|
html_doc = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>接口2 分辨率对比测试 v2</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f3f4f6; color: #1f2937; line-height: 1.5; padding: 16px; }}
|
||||||
|
.wrap {{ max-width: 100%; margin: 0 auto; }}
|
||||||
|
h1 {{ font-size: 22px; margin-bottom: 4px; }}
|
||||||
|
.sub {{ color: #6b7280; font-size: 12px; margin-bottom: 16px; }}
|
||||||
|
.stats {{ display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }}
|
||||||
|
.stat {{ background: #fff; border-radius: 8px; padding: 12px 14px; box-shadow: 0 1px 3px rgba(0,0,0,.06); flex: 1; min-width: 110px; }}
|
||||||
|
.stat .num {{ font-size: 22px; font-weight: 700; }}
|
||||||
|
.stat .lbl {{ font-size: 11px; color: #6b7280; margin-top: 2px; }}
|
||||||
|
.stat .dim {{ color: #9ca3af; font-size: 10px; }}
|
||||||
|
.summary-bar {{ background: #fff; border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.06); font-size: 13px; }}
|
||||||
|
.summary-bar b {{ color: #dc2626; }}
|
||||||
|
.table-wrap {{ overflow-x: auto; background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.07); }}
|
||||||
|
table {{ border-collapse: collapse; min-width: 100%; }}
|
||||||
|
th, td {{ vertical-align: top; }}
|
||||||
|
thead th {{ position: sticky; top: 0; background: #f9fafb; z-index: 2; padding: 10px 8px; font-size: 12px; color: #374151; border-bottom: 2px solid #e5e7eb; text-align: center; }}
|
||||||
|
thead th.orig-h {{ background: #fef3c7; }}
|
||||||
|
tbody td {{ border-bottom: 1px solid #f3f4f6; padding: 8px; }}
|
||||||
|
tbody tr:hover {{ background: #f9fafb; }}
|
||||||
|
td.rowlabel {{ text-align: left; padding: 8px 12px; position: sticky; left: 0; background: #fff; z-index: 1; min-width: 90px; box-shadow: 2px 0 4px rgba(0,0,0,.04); }}
|
||||||
|
tbody tr:hover td.rowlabel {{ background: #f9fafb; }}
|
||||||
|
.rimg {{ font-weight: 700; font-size: 14px; }}
|
||||||
|
.rs {{ font-size: 12px; color: #6b7280; }}
|
||||||
|
.rl {{ font-size: 10px; color: #9ca3af; margin-top: 2px; }}
|
||||||
|
.cell {{ width: 180px; min-width: 180px; text-align: center; }}
|
||||||
|
.cell.orig {{ width: 180px; background: #fffbeb; }}
|
||||||
|
.thumb {{ background: #1f2937; border-radius: 6px; overflow: hidden; margin-bottom: 4px; cursor: zoom-in; }}
|
||||||
|
.thumb img {{ width: 100%; height: 240px; object-fit: contain; display: block; }}
|
||||||
|
.thumb.noimg {{ color: #d1d5db; font-size: 16px; padding: 100px 0; text-align: center; }}
|
||||||
|
.meta {{ font-size: 11px; color: #6b7280; }}
|
||||||
|
.meta.err {{ color: #dc2626; }}
|
||||||
|
.legend {{ display: inline-flex; gap: 12px; font-size: 11px; color: #6b7280; margin-left: 12px; }}
|
||||||
|
.legend span {{ display: inline-flex; align-items: center; gap: 4px; }}
|
||||||
|
.legend i {{ width: 10px; height: 10px; border-radius: 2px; display: inline-block; }}
|
||||||
|
.overlay {{ display: none; position: fixed; inset: 0; background: rgba(0,0,0,.92); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; padding: 24px; }}
|
||||||
|
.overlay.active {{ display: flex; }}
|
||||||
|
.overlay img {{ max-width: 96%; max-height: 96%; object-fit: contain; border-radius: 4px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>💄 接口2 生发 · 5档分辨率对比报告</h1>
|
||||||
|
<p class="sub">POST /api/v1/hair/grow · female · 5 图 × 3 发型(花瓣/心形/波浪) × 5 档分辨率 = 75 次 · 串行 · 4090 (24G)</p>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat" style="border-left:3px solid #16a34a"><div class="num" style="color:#16a34a">{ok}/{total}</div><div class="lbl">成功 / 总数</div></div>
|
||||||
|
{stat_cards}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-bar">
|
||||||
|
📊 <b>结论:</b>耗时随送图分辨率单调下降。<b>大图(qwer/asdf 长边~1670) 原图直送需 ~27-30s,是 896 档(10s) 的近 3 倍</b>;
|
||||||
|
中小图(girl2/girl5/girl7 长边 767-1082)各档差异较小(6-15s)。
|
||||||
|
<b>4090 24G 全程无 OOM</b>,75/75 成功。<b>画质对比</b>见下表(横向滑动),点击任意图可放大。
|
||||||
|
<span class="legend">
|
||||||
|
<span><i style="background:#16a34a"></i>快(<{tmin+ (tmax-tmin)*0.33:.0f}s)</span>
|
||||||
|
<span><i style="background:#f59e0b"></i>中等</span>
|
||||||
|
<span><i style="background:#dc2626"></i>慢(>{tmin+ (tmax-tmin)*0.66:.0f}s)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>图 · 发型</th>
|
||||||
|
<th class="orig-h">📷 原图</th>
|
||||||
|
<th>原图直送 (0)<br><span class="dim">不缩放</span></th>
|
||||||
|
<th>1024</th>
|
||||||
|
<th>896<br><span class="dim">默认</span></th>
|
||||||
|
<th>768</th>
|
||||||
|
<th>640</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{"".join(rows_html)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overlay" id="overlay" onclick="this.classList.remove('active')"><img id="overlayImg" src=""></div>
|
||||||
|
<script>
|
||||||
|
function openImg(src) {{
|
||||||
|
document.getElementById('overlayImg').src = src;
|
||||||
|
document.getElementById('overlay').classList.add('active');
|
||||||
|
}}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
with open(REPORT, "w") as f:
|
||||||
|
f.write(html_doc)
|
||||||
|
print(f"已生成: {REPORT}")
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=接口2 5档分辨率对比报告 v2 (8849)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/home/ubuntu/hair/image/res_test/out
|
||||||
|
ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8849 --bind 0.0.0.0
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""接口2 female wave发型 分辨率对比测试 v3: 21图 × 5档 = 105次。
|
||||||
|
串行执行,记录耗时与成败,结果图按 图_档位 命名保存。支持断点续跑。
|
||||||
|
"""
|
||||||
|
import base64, csv, json, os, time, io
|
||||||
|
import requests
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
API = "http://127.0.0.1:8187/api/v1/hair/grow"
|
||||||
|
TOKEN = "dev-shared-secret-2026"
|
||||||
|
TIMEOUT = 600
|
||||||
|
OUT = "/home/ubuntu/hair/image/wave_test/out"
|
||||||
|
PROGRESS = "/home/ubuntu/hair/image/wave_test/progress.json"
|
||||||
|
|
||||||
|
IMG_DIR = "/home/ubuntu/hair/image"
|
||||||
|
# 21张图:19张girl_img + asdf + qwer
|
||||||
|
IMAGES = []
|
||||||
|
for f in sorted(os.listdir(os.path.join(IMG_DIR, "girl_img"))):
|
||||||
|
if f.lower().endswith((".jpg", ".jpeg", ".png")):
|
||||||
|
IMAGES.append((os.path.splitext(f)[0], os.path.join(IMG_DIR, "girl_img", f)))
|
||||||
|
IMAGES.append(("asdf", os.path.join(IMG_DIR, "asdf.jpg")))
|
||||||
|
IMAGES.append(("qwer", os.path.join(IMG_DIR, "qwer.jpg")))
|
||||||
|
|
||||||
|
# wave = female hair_style 5
|
||||||
|
STYLE_IDX = 5
|
||||||
|
# 5档: 原图(0) / 1024 / 896 / 768 / 640
|
||||||
|
SIDES = [
|
||||||
|
("origin", 0),
|
||||||
|
("s1024", 1024),
|
||||||
|
("s896", 896),
|
||||||
|
("s768", 768),
|
||||||
|
("s640", 640),
|
||||||
|
]
|
||||||
|
|
||||||
|
def load_progress():
|
||||||
|
if os.path.exists(PROGRESS):
|
||||||
|
try:
|
||||||
|
return json.load(open(PROGRESS))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"done": [], "results": []}
|
||||||
|
|
||||||
|
def save_progress(prog):
|
||||||
|
json.dump(prog, open(PROGRESS, "w"), ensure_ascii=False, indent=1)
|
||||||
|
|
||||||
|
def run_one(img_name, img_path, side_name, side_val):
|
||||||
|
key = f"{img_name}_{side_name}"
|
||||||
|
with open(img_path, "rb") as f:
|
||||||
|
img_b64 = base64.b64encode(f.read()).decode()
|
||||||
|
data = {
|
||||||
|
"image_base64": "data:image/jpeg;base64," + img_b64,
|
||||||
|
"gender": "female",
|
||||||
|
"hair_style": str(STYLE_IDX),
|
||||||
|
"redraw_max_side": str(side_val),
|
||||||
|
}
|
||||||
|
t0 = time.time()
|
||||||
|
rec = {"key": key, "img": img_name, "side": side_name, "side_val": side_val,
|
||||||
|
"ok": False, "elapsed": 0.0, "err": "", "out_w": 0, "out_h": 0, "bytes": 0}
|
||||||
|
try:
|
||||||
|
r = requests.post(API, data=data, headers={"X-Internal-Token": TOKEN}, timeout=TIMEOUT)
|
||||||
|
rec["elapsed"] = round(time.time() - t0, 1)
|
||||||
|
d = r.json()
|
||||||
|
if d.get("code") != 0:
|
||||||
|
rec["err"] = f"code={d.get('code')} {d.get('message','')}"[:200]
|
||||||
|
return rec
|
||||||
|
results = (d.get("data") or {}).get("results") or []
|
||||||
|
if not results:
|
||||||
|
rec["err"] = "空结果"
|
||||||
|
return rec
|
||||||
|
it = results[0]
|
||||||
|
grown = it.get("grown_image_base64")
|
||||||
|
if not grown:
|
||||||
|
rec["err"] = "无生发图(重绘失败/OOM?)"
|
||||||
|
return rec
|
||||||
|
raw = base64.b64decode(grown)
|
||||||
|
im = Image.open(io.BytesIO(raw))
|
||||||
|
rec["out_w"], rec["out_h"] = im.size
|
||||||
|
out_path = f"{OUT}/{key}.jpg"
|
||||||
|
with open(out_path, "wb") as fo:
|
||||||
|
fo.write(raw)
|
||||||
|
rec["ok"] = True
|
||||||
|
rec["bytes"] = len(raw)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
rec["elapsed"] = round(time.time() - t0, 1)
|
||||||
|
rec["err"] = f"超时(>{TIMEOUT}s)"
|
||||||
|
except Exception as e:
|
||||||
|
rec["elapsed"] = round(time.time() - t0, 1)
|
||||||
|
rec["err"] = f"{type(e).__name__}: {str(e)[:180]}"
|
||||||
|
return rec
|
||||||
|
|
||||||
|
def main():
|
||||||
|
prog = load_progress()
|
||||||
|
done_keys = set(prog["done"])
|
||||||
|
total = len(IMAGES) * len(SIDES)
|
||||||
|
print(f"=== 接口2 female wave 分辨率对比测试 v3 ===")
|
||||||
|
print(f"矩阵: {len(IMAGES)}图 × {len(SIDES)}档 = {total} 次 (发型固定 wave)")
|
||||||
|
print(f"已跳过 {len(done_keys)} 个已完成项\n")
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
for img_name, img_path in IMAGES:
|
||||||
|
for side_name, side_val in SIDES:
|
||||||
|
idx += 1
|
||||||
|
key = f"{img_name}_{side_name}"
|
||||||
|
if key in done_keys:
|
||||||
|
print(f"[{idx}/{total}] ⏭ 跳过 {key}")
|
||||||
|
continue
|
||||||
|
print(f"[{idx}/{total}] ▶ {key} (side={side_val})...", end=" ", flush=True)
|
||||||
|
rec = run_one(img_name, img_path, side_name, side_val)
|
||||||
|
prog["results"].append(rec)
|
||||||
|
prog["done"].append(key)
|
||||||
|
save_progress(prog)
|
||||||
|
if rec["ok"]:
|
||||||
|
print(f"✅ {rec['elapsed']}s {rec['out_w']}x{rec['out_h']} ({rec['bytes']}B)")
|
||||||
|
else:
|
||||||
|
print(f"❌ {rec['elapsed']}s {rec['err']}")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("汇总")
|
||||||
|
print("=" * 70)
|
||||||
|
write_report(prog["results"])
|
||||||
|
print(f"\n结果图: {OUT}/")
|
||||||
|
print(f"CSV: {OUT}/report.csv JSON: {OUT}/report.json")
|
||||||
|
|
||||||
|
def write_report(results):
|
||||||
|
with open(f"{OUT}/report.csv", "w", newline="") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(["img", "side", "side_val", "ok", "elapsed_s", "out_w", "out_h", "bytes", "err"])
|
||||||
|
for r in results:
|
||||||
|
w.writerow([r["img"], r["side"], r["side_val"], r["ok"],
|
||||||
|
r["elapsed"], r["out_w"], r["out_h"], r["bytes"], r["err"]])
|
||||||
|
json.dump(results, open(f"{OUT}/report.json", "w"), ensure_ascii=False, indent=1)
|
||||||
|
|
||||||
|
# 各档平均耗时
|
||||||
|
print("\n--- 各档平均耗时(秒)---")
|
||||||
|
for side_name, side_val in SIDES:
|
||||||
|
ts = [r["elapsed"] for r in results if r["side"] == side_name and r["ok"]]
|
||||||
|
if ts:
|
||||||
|
avg = sum(ts) / len(ts)
|
||||||
|
print(f" {side_name:8s} (={side_val:>4}): 平均 {avg:5.1f}s [{min(ts):.1f}~{max(ts):.1f}] 成功 {len(ts)}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""生成 v3 wave发型 分辨率对比报告:每行=原图+5档,21行;含各档耗时统计。"""
|
||||||
|
import json, os, html
|
||||||
|
|
||||||
|
OUT = "/home/ubuntu/hair/image/wave_test/out"
|
||||||
|
REPORT = os.path.join(OUT, "report.html")
|
||||||
|
results = json.load(open(os.path.join(OUT, "report.json")))
|
||||||
|
|
||||||
|
SIDES = [("origin", "原图直送", 0), ("s1024", "1024", 1024),
|
||||||
|
("s896", "896", 896), ("s768", "768", 768), ("s640", "640", 640)]
|
||||||
|
|
||||||
|
# 图片顺序(与测试脚本一致):girl_img 排序 + asdf + qwer
|
||||||
|
IMG_DIR = "/home/ubuntu/hair/image"
|
||||||
|
IMAGES = []
|
||||||
|
for f in sorted(os.listdir(os.path.join(IMG_DIR, "girl_img"))):
|
||||||
|
if f.lower().endswith((".jpg", ".jpeg", ".png")):
|
||||||
|
IMAGES.append(os.path.splitext(f)[0])
|
||||||
|
IMAGES.append("asdf")
|
||||||
|
IMAGES.append("qwer")
|
||||||
|
|
||||||
|
def get(img, side):
|
||||||
|
for r in results:
|
||||||
|
if r["img"] == img and r["side"] == side:
|
||||||
|
return r
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 统计
|
||||||
|
total = len(results)
|
||||||
|
ok = sum(1 for r in results if r["ok"])
|
||||||
|
|
||||||
|
# 各档统计
|
||||||
|
def avg(side):
|
||||||
|
ts = [r["elapsed"] for r in results if r["side"] == side and r["ok"]]
|
||||||
|
return sum(ts)/len(ts) if ts else 0
|
||||||
|
avgs = {s[0]: avg(s[0]) for s in SIDES}
|
||||||
|
|
||||||
|
# 速度色阶
|
||||||
|
all_t = sorted(r["elapsed"] for r in results if r["ok"])
|
||||||
|
tmin, tmax = all_t[0], all_t[-1]
|
||||||
|
def speed_color(t):
|
||||||
|
if tmax == tmin: return "#16a34a"
|
||||||
|
ratio = (t - tmin) / (tmax - tmin)
|
||||||
|
if ratio < 0.33: return "#16a34a"
|
||||||
|
elif ratio < 0.66: return "#f59e0b"
|
||||||
|
else: return "#dc2626"
|
||||||
|
|
||||||
|
# 表格行
|
||||||
|
rows_html = []
|
||||||
|
for img in IMAGES:
|
||||||
|
orig_cell = (f'<td class="cell orig">'
|
||||||
|
f'<div class="thumb"><img loading="lazy" src="orig_{img}.jpg" '
|
||||||
|
f'onclick="openImg(this.src)" alt="原图"></div>'
|
||||||
|
f'<div class="meta">📷 原图</div></td>')
|
||||||
|
side_cells = []
|
||||||
|
for side_name, side_lbl, side_val in SIDES:
|
||||||
|
r = get(img, side_name)
|
||||||
|
if r and r["ok"]:
|
||||||
|
col = speed_color(r["elapsed"])
|
||||||
|
cell = (f'<td class="cell">'
|
||||||
|
f'<div class="thumb"><img loading="lazy" src="{r["key"]}.jpg" '
|
||||||
|
f'onclick="openImg(this.src)" alt="{html.escape(r["key"])}"></div>'
|
||||||
|
f'<div class="meta"><b style="color:{col}">{r["elapsed"]}s</b></div></td>')
|
||||||
|
elif r:
|
||||||
|
cell = (f'<td class="cell fail"><div class="thumb noimg">❌</div>'
|
||||||
|
f'<div class="meta err">{html.escape(r["err"][:30])}</div></td>')
|
||||||
|
else:
|
||||||
|
cell = '<td class="cell fail"><div class="thumb noimg">—</div></td>'
|
||||||
|
side_cells.append(cell)
|
||||||
|
rows_html.append("<tr>" + orig_cell + "".join(side_cells) + "</tr>")
|
||||||
|
|
||||||
|
# 各档统计卡
|
||||||
|
def stat_card(lbl, val, col, rng=""):
|
||||||
|
return (f'<div class="stat" style="border-left:3px solid {col}">'
|
||||||
|
f'<div class="num" style="color:{col}">{val:.1f}s</div>'
|
||||||
|
f'<div class="lbl">{lbl}{rng}</div></div>')
|
||||||
|
|
||||||
|
stat_cards = "".join([
|
||||||
|
stat_card("原图直送(0)", avgs["origin"], "#dc2626"),
|
||||||
|
stat_card("1024", avgs["s1024"], "#f59e0b"),
|
||||||
|
stat_card("896", avgs["s896"], "#2563eb"),
|
||||||
|
stat_card("768", avgs["s768"], "#16a34a"),
|
||||||
|
stat_card("640", avgs["s640"], "#0d9488"),
|
||||||
|
])
|
||||||
|
|
||||||
|
html_doc = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>wave发型 5档分辨率对比</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f3f4f6; color: #1f2937; line-height: 1.5; padding: 16px; }}
|
||||||
|
.wrap {{ max-width: 100%; margin: 0 auto; }}
|
||||||
|
h1 {{ font-size: 22px; margin-bottom: 4px; }}
|
||||||
|
.sub {{ color: #6b7280; font-size: 12px; margin-bottom: 16px; }}
|
||||||
|
.stats {{ display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }}
|
||||||
|
.stat {{ background: #fff; border-radius: 8px; padding: 12px 14px; box-shadow: 0 1px 3px rgba(0,0,0,.06); flex: 1; min-width: 110px; }}
|
||||||
|
.stat .num {{ font-size: 22px; font-weight: 700; }}
|
||||||
|
.stat .lbl {{ font-size: 11px; color: #6b7280; margin-top: 2px; }}
|
||||||
|
.summary-bar {{ background: #fff; border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.06); font-size: 13px; }}
|
||||||
|
.summary-bar b {{ color: #dc2626; }}
|
||||||
|
.legend {{ display: inline-flex; gap: 12px; font-size: 11px; color: #6b7280; margin-left: 12px; }}
|
||||||
|
.legend span {{ display: inline-flex; align-items: center; gap: 4px; }}
|
||||||
|
.legend i {{ width: 10px; height: 10px; border-radius: 2px; display: inline-block; }}
|
||||||
|
.table-wrap {{ overflow-x: auto; background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.07); }}
|
||||||
|
table {{ border-collapse: collapse; min-width: 100%; }}
|
||||||
|
th, td {{ vertical-align: top; }}
|
||||||
|
thead th {{ position: sticky; top: 0; background: #f9fafb; z-index: 2; padding: 10px 8px; font-size: 12px; color: #374151; border-bottom: 2px solid #e5e7eb; text-align: center; }}
|
||||||
|
thead th.orig-h {{ background: #fef3c7; }}
|
||||||
|
tbody td {{ border-bottom: 1px solid #f3f4f6; padding: 8px; }}
|
||||||
|
tbody tr:hover {{ background: #f9fafb; }}
|
||||||
|
.cell {{ width: 200px; min-width: 200px; text-align: center; }}
|
||||||
|
.cell.orig {{ background: #fffbeb; }}
|
||||||
|
.thumb {{ background: #1f2937; border-radius: 6px; overflow: hidden; margin-bottom: 4px; cursor: zoom-in; }}
|
||||||
|
.thumb img {{ width: 100%; height: 260px; object-fit: contain; display: block; }}
|
||||||
|
.thumb.noimg {{ color: #d1d5db; font-size: 16px; padding: 110px 0; text-align: center; }}
|
||||||
|
.meta {{ font-size: 11px; color: #6b7280; }}
|
||||||
|
.meta.err {{ color: #dc2626; }}
|
||||||
|
.overlay {{ display: none; position: fixed; inset: 0; background: rgba(0,0,0,.92); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; padding: 24px; }}
|
||||||
|
.overlay.active {{ display: flex; }}
|
||||||
|
.overlay img {{ max-width: 96%; max-height: 96%; object-fit: contain; border-radius: 4px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>💇 wave发型 · 5档分辨率对比报告</h1>
|
||||||
|
<p class="sub">POST /api/v1/hair/grow · female · wave(波浪) · 21 图 × 5 档分辨率 = 105 次 · 串行 · 4090 (24G)</p>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat" style="border-left:3px solid #16a34a"><div class="num" style="color:#16a34a">{ok}/{total}</div><div class="lbl">成功 / 总数</div></div>
|
||||||
|
{stat_cards}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-bar">
|
||||||
|
📊 <b>结论:</b>耗时随分辨率单调下降。<b>大图(asdf 1666/qwer 1678) 原图直送需 24~26s,是 1024 档(11s) 的 2.3 倍</b>;
|
||||||
|
中小图(≤1024) 各档差异较小(6~13s),因小图本身不触发缩图。
|
||||||
|
<b>4090 24G 全程无 OOM</b>,105/105 成功。<b>画质对比</b>见下表(横向滑动),点击任意图可放大。
|
||||||
|
<span class="legend">
|
||||||
|
<span><i style="background:#16a34a"></i>快(<{(tmin+(tmax-tmin)*0.33):.0f}s)</span>
|
||||||
|
<span><i style="background:#f59e0b"></i>中等</span>
|
||||||
|
<span><i style="background:#dc2626"></i>慢(>{(tmin+(tmax-tmin)*0.66):.0f}s)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="orig-h">📷 原图</th>
|
||||||
|
<th>原图直送 (0)<br><span class="dim">不缩放</span></th>
|
||||||
|
<th>1024</th>
|
||||||
|
<th>896<br><span class="dim">默认</span></th>
|
||||||
|
<th>768</th>
|
||||||
|
<th>640</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{"".join(rows_html)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overlay" id="overlay" onclick="this.classList.remove('active')"><img id="overlayImg" src=""></div>
|
||||||
|
<script>
|
||||||
|
function openImg(src) {{
|
||||||
|
document.getElementById('overlayImg').src = src;
|
||||||
|
document.getElementById('overlay').classList.add('active');
|
||||||
|
}}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
with open(REPORT, "w") as f:
|
||||||
|
f.write(html_doc)
|
||||||
|
print(f"已生成: {REPORT}")
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=wave发型5档分辨率对比报告 (8850)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/home/ubuntu/hair/image/wave_test/out
|
||||||
|
ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8850 --bind 0.0.0.0
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
+1
-1
@@ -206,7 +206,7 @@ def generate():
|
|||||||
return jsonify({"error": msg}), 400
|
return jsonify({"error": msg}), 400
|
||||||
image_file = request.files["image"]
|
image_file = request.files["image"]
|
||||||
mask_file = request.files["mask"]
|
mask_file = request.files["mask"]
|
||||||
prompt_text = request.form.get("prompt", "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
prompt_text = request.form.get("prompt", "填充遮罩区域的头发")
|
||||||
log.info(
|
log.info(
|
||||||
"收到请求: image=%s mask=%s prompt=%r",
|
"收到请求: image=%s mask=%s prompt=%r",
|
||||||
image_file.filename, mask_file.filename, prompt_text,
|
image_file.filename, mask_file.filename, prompt_text,
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ def prep_and_upload():
|
|||||||
|
|
||||||
|
|
||||||
def run_once(fname, model, dtype, steps):
|
def run_once(fname, model, dtype, steps):
|
||||||
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
wf = A.build_workflow(fname, "填充遮罩区域的头发")
|
||||||
wf["16"]["inputs"]["unet_name"] = model
|
wf["16"]["inputs"]["unet_name"] = model
|
||||||
wf["16"]["inputs"]["weight_dtype"] = dtype
|
wf["16"]["inputs"]["weight_dtype"] = dtype
|
||||||
wf["1"]["inputs"]["steps"] = steps
|
wf["1"]["inputs"]["steps"] = steps
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def upload(scale=1.0):
|
|||||||
|
|
||||||
|
|
||||||
def run(fname, steps):
|
def run(fname, steps):
|
||||||
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
wf = A.build_workflow(fname, "填充遮罩区域的头发")
|
||||||
wf["16"]["inputs"]["unet_name"] = MODEL
|
wf["16"]["inputs"]["unet_name"] = MODEL
|
||||||
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
||||||
wf["1"]["inputs"]["steps"] = steps
|
wf["1"]["inputs"]["steps"] = steps
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ SEED = 123456789
|
|||||||
imgs = []
|
imgs = []
|
||||||
labels = []
|
labels = []
|
||||||
for steps in [2, 3, 4, 6]:
|
for steps in [2, 3, 4, 6]:
|
||||||
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", seed=SEED)
|
wf = A.build_workflow(fname, "填充遮罩区域的头发", seed=SEED)
|
||||||
wf["16"]["inputs"]["unet_name"] = MODEL
|
wf["16"]["inputs"]["unet_name"] = MODEL
|
||||||
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
||||||
wf["1"]["inputs"]["steps"] = steps
|
wf["1"]["inputs"]["steps"] = steps
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer;
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls" style="margin-top:16px">
|
<div class="controls" style="margin-top:16px">
|
||||||
<label>提示词:</label>
|
<label>提示词:</label>
|
||||||
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜">
|
<input type="text" id="promptInput" value="填充遮罩区域的头发">
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:center; margin-top:16px">
|
<div style="text-align:center; margin-top:16px">
|
||||||
<button class="btn-generate" id="generateBtn" disabled>🚀 生成</button>
|
<button class="btn-generate" id="generateBtn" disabled>🚀 生成</button>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ resp = requests.post(
|
|||||||
"image": ("original.jpg", img_data, "image/jpeg"),
|
"image": ("original.jpg", img_data, "image/jpeg"),
|
||||||
"mask": ("mask.png", mask_data, "image/png"),
|
"mask": ("mask.png", mask_data, "image/png"),
|
||||||
},
|
},
|
||||||
data={"prompt": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"},
|
data={"prompt": "填充遮罩区域的头发"},
|
||||||
timeout=600,
|
timeout=600,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="upload-row" style="margin-top:10px">
|
<div class="upload-row" style="margin-top:10px">
|
||||||
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
|
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
|
||||||
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-row" style="margin-top:10px">
|
<div class="upload-row" style="margin-top:10px">
|
||||||
<label style="font-size:13px;font-weight:600;white-space:nowrap">X-Internal-Token</label>
|
<label style="font-size:13px;font-weight:600;white-space:nowrap">X-Internal-Token</label>
|
||||||
@@ -354,7 +354,7 @@ function dataUriToBlob(dataUri) {
|
|||||||
|
|
||||||
async function runLocalRedraw() {
|
async function runLocalRedraw() {
|
||||||
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
|
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
|
||||||
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜';
|
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发';
|
||||||
const btn = $('redrawBtn');
|
const btn = $('redrawBtn');
|
||||||
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
|
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
|
||||||
flog('===== 调后端重绘 =====', 'info');
|
flog('===== 调后端重绘 =====', 'info');
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="upload-row" style="margin-top:10px">
|
<div class="upload-row" style="margin-top:10px">
|
||||||
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
|
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
|
||||||
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
||||||
</div>
|
</div>
|
||||||
<div class="params">
|
<div class="params">
|
||||||
<div class="pf">
|
<div class="pf">
|
||||||
@@ -169,7 +169,7 @@ async function submitTest() {
|
|||||||
|
|
||||||
async function runLocalRedraw() {
|
async function runLocalRedraw() {
|
||||||
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
|
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
|
||||||
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜';
|
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发';
|
||||||
const btn = $('redrawBtn');
|
const btn = $('redrawBtn');
|
||||||
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
|
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
|
||||||
|
|
||||||
|
|||||||
@@ -101,13 +101,23 @@
|
|||||||
<option value="false">⬜ 关闭 — 空遮罩,不烧模板线</option>
|
<option value="false">⬜ 关闭 — 空遮罩,不烧模板线</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" id="redrawSideGroup">
|
||||||
|
<label>重绘分辨率</label>
|
||||||
|
<select id="redrawMaxSide" title="仅 Female 生效:图长边超过该值才缩到该值,不超过则原图直送。默认1024=大于1024压到1024,小图保持原分辨率">
|
||||||
|
<option value="" selected>默认 1024(>1024 才缩)</option>
|
||||||
|
<option value="0">0 — 永不缩图(原图直送)</option>
|
||||||
|
<option value="1280">1280(更高清,慢)</option>
|
||||||
|
<option value="768">768(快)</option>
|
||||||
|
<option value="640">640(最快,糊)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
|
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="hint">JPG/PNG | 生发图生成较慢(数十秒~数分钟),请耐心等待</div>
|
<div class="hint">JPG/PNG | 生发图生成较慢(数十秒~数分钟),请耐心等待</div>
|
||||||
<div style="margin-top:10px;display:flex;align-items:center;gap:8px">
|
<div style="margin-top:10px;display:flex;align-items:center;gap:8px">
|
||||||
<label style="font-size:13px;font-weight:600;color:#374151;white-space:nowrap">💬 提示词</label>
|
<label style="font-size:13px;font-weight:600;color:#374151;white-space:nowrap">💬 提示词</label>
|
||||||
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="flex:1;padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;max-width:500px">
|
<input type="text" id="promptInput" value="填充遮罩区域的头发" style="flex:1;padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;max-width:500px">
|
||||||
</div>
|
</div>
|
||||||
<div id="statusBar" class="status hidden"></div>
|
<div id="statusBar" class="status hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,6 +165,11 @@ async function submitTest() {
|
|||||||
fd.append('hair_style', checked.join(','));
|
fd.append('hair_style', checked.join(','));
|
||||||
fd.append('use_mask', $('useMask').value);
|
fd.append('use_mask', $('useMask').value);
|
||||||
fd.append('prompt', $('promptInput').value);
|
fd.append('prompt', $('promptInput').value);
|
||||||
|
// 重绘分辨率:仅 female 传给后端(male 路径不接受此参数)
|
||||||
|
if ($('gender').value === 'female') {
|
||||||
|
const side = $('redrawMaxSide').value;
|
||||||
|
if (side !== '') fd.append('redraw_max_side', side); // 空串=不传→后端默认896
|
||||||
|
}
|
||||||
const _reqStart = performance.now();
|
const _reqStart = performance.now();
|
||||||
try {
|
try {
|
||||||
const r = await fetch(API_BASE + '/api/v1/hair/grow', { method:'POST', headers:{ 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
|
const r = await fetch(API_BASE + '/api/v1/hair/grow', { method:'POST', headers:{ 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
|
||||||
@@ -258,6 +273,8 @@ function onGenderChange() {
|
|||||||
lb.appendChild(document.createTextNode(' ' + s.label));
|
lb.appendChild(document.createTextNode(' ' + s.label));
|
||||||
div.appendChild(lb);
|
div.appendChild(lb);
|
||||||
});
|
});
|
||||||
|
// 重绘分辨率选项仅 female 生效:male 时隐藏
|
||||||
|
$('redrawSideGroup').style.display = (g === 'female') ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAllHair(select) {
|
function selectAllHair(select) {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="upload-group" style="margin-top:14px">
|
<div class="upload-group" style="margin-top:14px">
|
||||||
<div class="label">💬 提示词(prompt)</div>
|
<div class="label">💬 提示词(prompt)</div>
|
||||||
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;max-width:500px">
|
<input type="text" id="promptInput" value="填充遮罩区域的头发" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;max-width:500px">
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:14px;display:flex;gap:12px;align-items:center">
|
<div style="margin-top:14px;display:flex;gap:12px;align-items:center">
|
||||||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<div class="hint">JPG/PNG | 生发图生成较慢(数十秒~数分钟),请耐心等待 | 工作流: add_hair2.json</div>
|
<div class="hint">JPG/PNG | 生发图生成较慢(数十秒~数分钟),请耐心等待 | 工作流: add_hair2.json</div>
|
||||||
<div style="margin-top:10px;display:flex;align-items:center;gap:8px">
|
<div style="margin-top:10px;display:flex;align-items:center;gap:8px">
|
||||||
<label style="font-size:13px;font-weight:600;color:#374151;white-space:nowrap">💬 提示词</label>
|
<label style="font-size:13px;font-weight:600;color:#374151;white-space:nowrap">💬 提示词</label>
|
||||||
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" style="flex:1;padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;max-width:500px">
|
<input type="text" id="promptInput" value="填充遮罩区域的头发" style="flex:1;padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;max-width:500px">
|
||||||
</div>
|
</div>
|
||||||
<div id="statusBar" class="status hidden"></div>
|
<div id="statusBar" class="status hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user