Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
616fc26143 | ||
|
|
48c9875381 | ||
|
|
5bcaeb2594 | ||
|
|
8a82d00056 | ||
|
|
e2bbcca7ad | ||
|
|
f8ed477696 | ||
|
|
2877d07a69 | ||
|
|
bee7a487ac | ||
|
|
531dcb8279 | ||
|
|
62d2660e41 | ||
|
|
e4d03a782d | ||
|
|
ae5a3b8f1d | ||
|
|
d318efcff0 |
@@ -65,16 +65,3 @@ 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
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from typing import Any, List, Optional
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
from fastapi import FastAPI, File, Form, Request, UploadFile
|
from fastapi import FastAPI, File, Form, Request, UploadFile
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
@@ -448,7 +448,7 @@ def _run_face_measure_data(image, variant="v1"):
|
|||||||
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线。
|
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线。
|
||||||
# 竖向范围:发际线弃用时用眉心做上界(hair_top 不可靠),否则用头顶。
|
# 竖向范围:发际线弃用时用眉心做上界(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
|
top_y = (vd["brow_center"][1] if result.hairline_discarded
|
||||||
else vd["hair_top"][1])
|
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,
|
||||||
@@ -464,6 +464,398 @@ def _run_face_measure_data(image, variant="v1"):
|
|||||||
return data, result, hair_mask, ear_mask
|
return data, result, hair_mask, ear_mask
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 接口1 调试:分步可视化(每一步的中间产物图)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# 调试接口 9 张分步图的 key(与前端 STEPS 一一对应)
|
||||||
|
_DEBUG_STEP_KEYS = [
|
||||||
|
"input", "landmarks", "pose", "segmentation",
|
||||||
|
"hairline", "vertical", "seven_eyes", "scale", "final",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _overlay_mask(image_bgr, mask, color, alpha=0.45):
|
||||||
|
"""在 BGR 图上把 mask 区域以 color(BGR) 半透明叠加。mask 为 bool/uint8。"""
|
||||||
|
out = image_bgr.copy()
|
||||||
|
m = np.asarray(mask).astype(bool)
|
||||||
|
if m.shape[:2] != out.shape[:2]:
|
||||||
|
return out
|
||||||
|
overlay = out[m]
|
||||||
|
# alpha 混合
|
||||||
|
overlay = (overlay * (1 - alpha) + np.array(color, dtype=np.float32) * alpha)
|
||||||
|
out[m] = np.clip(overlay, 0, 255).astype(np.uint8)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_DEBUG_FONT_PATH = os.path.join(
|
||||||
|
os.path.dirname(__file__), "face_analysis", "fonts", "NotoSansCJKsc-Regular.otf")
|
||||||
|
_debug_font_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _debug_font(size):
|
||||||
|
f = _debug_font_cache.get(size)
|
||||||
|
if f is None:
|
||||||
|
f = ImageFont.truetype(_DEBUG_FONT_PATH, size)
|
||||||
|
_debug_font_cache[size] = f
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_text_cv2(img, text, org, color=(255, 255, 255), scale=None, thickness=None,
|
||||||
|
bg=True, anchor="lt"):
|
||||||
|
"""在 BGR 图上绘制文字(支持中文,用 PIL + 思源黑体)。org=(x,y)。
|
||||||
|
|
||||||
|
cv2.putText 不支持中文(会显示成问号),故统一改用 PIL 渲染。color 为 BGR 三元组。
|
||||||
|
anchor: lt=左上角对齐 org / lb=左下角 / ct=水平垂直居中。bg=True 时画黑色背景框。
|
||||||
|
"""
|
||||||
|
h, w = img.shape[:2]
|
||||||
|
s = min(w, h)
|
||||||
|
scale = scale if scale else max(0.4, s * 0.0016)
|
||||||
|
thickness = thickness if thickness else max(1, round(s * 0.0022))
|
||||||
|
# PIL 字号与 cv2 scale 大致对应(cv2 scale≈字号/30)
|
||||||
|
font_size = max(10, round(scale * 30))
|
||||||
|
font = _debug_font(font_size)
|
||||||
|
# BGR → RGB
|
||||||
|
rgb = (int(color[2]), int(color[1]), int(color[0]))
|
||||||
|
pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
||||||
|
draw = ImageDraw.Draw(pil_img)
|
||||||
|
bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
|
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||||
|
x, y = org
|
||||||
|
if anchor == "lb":
|
||||||
|
text_y = y - th
|
||||||
|
elif anchor == "ct":
|
||||||
|
x = x - tw // 2
|
||||||
|
text_y = y - th // 2
|
||||||
|
else:
|
||||||
|
text_y = y
|
||||||
|
if bg:
|
||||||
|
pad = max(2, round(thickness * 1.2))
|
||||||
|
draw.rectangle(
|
||||||
|
[max(0, x - pad), max(0, text_y - pad),
|
||||||
|
min(w, x + tw + pad), min(h, text_y + th + pad)],
|
||||||
|
fill=(0, 0, 0))
|
||||||
|
# PIL text 的 y 是文字顶部基线,bbox 偏移需校正
|
||||||
|
draw.text((x, text_y - bbox[1]), text, fill=rgb, font=font)
|
||||||
|
img[:] = cv2.cvtColor(np.asarray(pil_img), cv2.COLOR_RGB2BGR)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def _run_face_measure_data_debug(image):
|
||||||
|
"""接口1 调试:产出 9 步中间图 + 数值,逐步塞进返回 dict。
|
||||||
|
|
||||||
|
与 _run_face_measure_data 同链路,但每步把中间产物渲染成叠加图(JPG base64)
|
||||||
|
放进 data["steps"][key + "_base64"],关键数值放进 data["debug"]。
|
||||||
|
检测/姿态失败时,仍返回已完成的步骤图 + 对应错误码,供前端展示「卡在哪一步」。
|
||||||
|
|
||||||
|
返回 (data, error_code_or_None, error_msg_or_None)。
|
||||||
|
"""
|
||||||
|
h, w = image.shape[:2]
|
||||||
|
from face_analysis.detector import detector
|
||||||
|
from face_analysis.pose import estimate_head_pose, check_frontal_face
|
||||||
|
from face_analysis.measure import measure_face, _brow_center
|
||||||
|
from face_analysis.calibration import (
|
||||||
|
normalized_to_pixel, estimate_scale_factor,
|
||||||
|
_iris_diameter_px, _eye_width_px, _lm_list,
|
||||||
|
AVG_IRIS_DIAMETER_CM, AVG_EYE_WIDTH_CM,
|
||||||
|
)
|
||||||
|
from face_analysis.face_mesh_landmarks import (
|
||||||
|
GLABELLA_9, GLABELLA_151, NOSE_BOTTOM, CHIN_TIP,
|
||||||
|
LEFT_EYE_OUTER, LEFT_EYE_INNER, RIGHT_EYE_INNER, RIGHT_EYE_OUTER,
|
||||||
|
LEFT_CHEEK, RIGHT_CHEEK, LEFT_POSITION, RIGHT_POSITION,
|
||||||
|
IRIS_LEFT_LEFT, IRIS_LEFT_RIGHT, IRIS_RIGHT_LEFT, IRIS_RIGHT_RIGHT,
|
||||||
|
PNP_INDICES,
|
||||||
|
)
|
||||||
|
from face_analysis.hair_segmenter import locate_hairline_by_segmentation
|
||||||
|
|
||||||
|
data = {"steps": {}, "debug": {"image_width": w, "image_height": h}}
|
||||||
|
steps = data["steps"]
|
||||||
|
dbg = data["debug"]
|
||||||
|
|
||||||
|
def put(key, bgr_img):
|
||||||
|
steps[key + "_base64"] = "data:image/jpeg;base64," + _jpg_b64(bgr_img)
|
||||||
|
|
||||||
|
# ① 输入原图
|
||||||
|
put("input", image)
|
||||||
|
|
||||||
|
# ② 人脸关键点检测
|
||||||
|
landmarks = detector.detect(image)
|
||||||
|
if landmarks is None:
|
||||||
|
dbg["num_landmarks"] = 0
|
||||||
|
return data, 1001, "无法识别人像"
|
||||||
|
lm = _lm_list(landmarks)
|
||||||
|
dbg["num_landmarks"] = len(lm)
|
||||||
|
|
||||||
|
vis_lm = image.copy()
|
||||||
|
# 先画全部 478 点(小白点)
|
||||||
|
s = min(w, h)
|
||||||
|
r_all = max(1, round(s * 0.0018))
|
||||||
|
for p in lm:
|
||||||
|
px = normalized_to_pixel(p, w, h)
|
||||||
|
cv2.circle(vis_lm, (int(px[0]), int(px[1])), r_all, (220, 220, 220), -1)
|
||||||
|
# 虹膜点 468~477(青色稍大)
|
||||||
|
r_iris = max(2, round(s * 0.0035))
|
||||||
|
for idx in [468, 469, 470, 471, 472, 473, 474, 475, 476, 477]:
|
||||||
|
if idx < len(lm):
|
||||||
|
px = normalized_to_pixel(lm[idx], w, h)
|
||||||
|
cv2.circle(vis_lm, (int(px[0]), int(px[1])), r_iris, (255, 200, 0), -1)
|
||||||
|
# 七眼 6 点 + 5 纵向点(红色 + 标号)
|
||||||
|
key_pts = {
|
||||||
|
"头顶(推算)": None, # 纵向点除眉心外由后续 measure 给出,这里只画能拿到的
|
||||||
|
"眉心": GLABELLA_9,
|
||||||
|
}
|
||||||
|
important = [
|
||||||
|
(GLABELLA_9, "眉间9"), (GLABELLA_151, "眉间151"), (NOSE_BOTTOM, "鼻翼下94"),
|
||||||
|
(CHIN_TIP, "下巴152"), (LEFT_EYE_OUTER, "左眼外33"), (LEFT_EYE_INNER, "左眼内133"),
|
||||||
|
(RIGHT_EYE_INNER, "右眼内362"), (RIGHT_EYE_OUTER, "右眼外263"),
|
||||||
|
(LEFT_CHEEK, "左脸234"), (RIGHT_CHEEK, "右脸454"),
|
||||||
|
]
|
||||||
|
r_imp = max(3, round(s * 0.005))
|
||||||
|
for idx, name in important:
|
||||||
|
px = normalized_to_pixel(lm[idx], w, h)
|
||||||
|
cv2.circle(vis_lm, (int(px[0]), int(px[1])), r_imp, (0, 0, 255), -1)
|
||||||
|
_draw_text_cv2(vis_lm, name, (int(px[0]) + r_imp + 2, int(px[1])),
|
||||||
|
color=(0, 255, 255), scale=max(0.35, s * 0.0013))
|
||||||
|
put("landmarks", vis_lm)
|
||||||
|
|
||||||
|
# ③ 头部姿态校验
|
||||||
|
head_pose = estimate_head_pose(lm, w, h) if hasattr(landmarks, "landmark") else estimate_head_pose(lm, w, h)
|
||||||
|
frontal = check_frontal_face(landmarks, w, h)
|
||||||
|
vis_pose = image.copy()
|
||||||
|
# 画 6 个 PnP 点(黄)
|
||||||
|
nose_tip_px = None
|
||||||
|
for idx in PNP_INDICES:
|
||||||
|
px = normalized_to_pixel(lm[idx], w, h)
|
||||||
|
cv2.circle(vis_pose, (int(px[0]), int(px[1])), max(3, round(s * 0.004)), (0, 255, 255), -1)
|
||||||
|
if idx == 1:
|
||||||
|
nose_tip_px = (int(px[0]), int(px[1]))
|
||||||
|
# 三轴箭头(鼻尖为原点)
|
||||||
|
if nose_tip_px is not None and head_pose is not None:
|
||||||
|
L = max(30, round(s * 0.08))
|
||||||
|
# yaw 绕 Y(竖轴) → 在屏幕上表现为左右;pitch 绕 X → 上下;roll 绕 Z → 面内旋转
|
||||||
|
yaw, pitch, roll = head_pose
|
||||||
|
import math
|
||||||
|
# 简化:用 roll 直接旋转 X/Y 轴示意,yaw 投影到横向、pitch 到纵向
|
||||||
|
cosr, sinr = math.cos(math.radians(roll)), math.sin(math.radians(roll))
|
||||||
|
# X 轴(红,向右)
|
||||||
|
cv2.arrowedLine(vis_pose, nose_tip_px,
|
||||||
|
(int(nose_tip_px[0] + L * cosr), int(nose_tip_px[1] + L * sinr)),
|
||||||
|
(0, 0, 255), max(2, round(s * 0.003)), tipLength=0.2)
|
||||||
|
# Y 轴(绿,向下)
|
||||||
|
cv2.arrowedLine(vis_pose, nose_tip_px,
|
||||||
|
(int(nose_tip_px[0] - L * sinr), int(nose_tip_px[1] + L * cosr)),
|
||||||
|
(0, 255, 0), max(2, round(s * 0.003)), tipLength=0.2)
|
||||||
|
# Z 轴(青,向内用圆圈示意)
|
||||||
|
cv2.circle(vis_pose, nose_tip_px, max(6, round(s * 0.012)), (255, 255, 0), max(1, round(s * 0.002)))
|
||||||
|
# 角度文字
|
||||||
|
txt = f"yaw={yaw:.1f} pitch={pitch:.1f} roll={roll:.1f}"
|
||||||
|
_draw_text_cv2(vis_pose, txt, (10, 10), color=(50, 255, 50),
|
||||||
|
scale=max(0.5, s * 0.0022))
|
||||||
|
_draw_text_cv2(vis_pose, f"frontal={'YES' if frontal else 'NO'}", (10, 40),
|
||||||
|
color=(50, 255, 50) if frontal else (50, 50, 255),
|
||||||
|
scale=max(0.5, s * 0.0022))
|
||||||
|
dbg["head_pose"] = {"yaw": round(yaw, 2), "pitch": round(pitch, 2),
|
||||||
|
"roll": round(roll, 2), "frontal": bool(frontal)}
|
||||||
|
put("pose", vis_pose)
|
||||||
|
|
||||||
|
if not frontal:
|
||||||
|
return data, 1003, "角度问题,请上传正面照"
|
||||||
|
|
||||||
|
# ④ 头发/耳朵分割
|
||||||
|
hair_mask = None
|
||||||
|
ear_mask = None
|
||||||
|
try:
|
||||||
|
from face_analysis.hair_segmenter import get_segmenter
|
||||||
|
pxs = [normalized_to_pixel(p, w, h) for p in lm]
|
||||||
|
face_box = (min(p[0] for p in pxs), min(p[1] for p in pxs),
|
||||||
|
max(p[0] for p in pxs), max(p[1] for p in pxs))
|
||||||
|
hair_mask, ear_mask = get_segmenter().segment_hair_and_ears(image, face_box=face_box)
|
||||||
|
except Exception as seg_e: # noqa: BLE001
|
||||||
|
logger.warning("[debug] 头发/耳朵分割失败:%s", seg_e)
|
||||||
|
|
||||||
|
vis_seg = image.copy()
|
||||||
|
if hair_mask is not None:
|
||||||
|
vis_seg = _overlay_mask(vis_seg, hair_mask, (0, 200, 0), alpha=0.45)
|
||||||
|
dbg["hair_pixels"] = int(np.asarray(hair_mask).astype(bool).sum())
|
||||||
|
if ear_mask is not None:
|
||||||
|
vis_seg = _overlay_mask(vis_seg, ear_mask, (200, 80, 0), alpha=0.5)
|
||||||
|
dbg["ear_pixels"] = int(np.asarray(ear_mask).astype(bool).sum())
|
||||||
|
_draw_text_cv2(vis_seg, "绿=头发(hair=17) 蓝=耳朵(ear=7/8)", (10, 10),
|
||||||
|
color=(50, 255, 50), scale=max(0.45, s * 0.0018))
|
||||||
|
put("segmentation", vis_seg)
|
||||||
|
|
||||||
|
# 主测量(复用 measure_face,内部含 ⑤ 纵向决策 + 七眼 + 尺度)
|
||||||
|
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
||||||
|
v = result.vertical
|
||||||
|
dbg["hairline_source"] = result.hairline_source
|
||||||
|
|
||||||
|
# ⑤ 纵向定位(发际线/头顶)—— 复刻方案 B 的中轴线扫描
|
||||||
|
vis_hl = image.copy()
|
||||||
|
if hair_mask is not None:
|
||||||
|
vis_hl = _overlay_mask(vis_hl, hair_mask, (0, 180, 0), alpha=0.3)
|
||||||
|
brow_x, brow_y = _brow_center(lm, w, h)
|
||||||
|
# 中轴线 ±3px 列带高亮(黄)
|
||||||
|
cx = int(round(brow_x))
|
||||||
|
cv2.line(vis_hl, (max(0, cx - 3), 0), (max(0, cx - 3), h), (0, 230, 255), 1)
|
||||||
|
cv2.line(vis_hl, (min(w - 1, cx + 3), 0), (min(w - 1, cx + 3), h), (0, 230, 255), 1)
|
||||||
|
# 画 hairline_y / hair_top_y 两条横线
|
||||||
|
hairline_y = int(v["hairline"][1])
|
||||||
|
hair_top_y = int(v["hair_top"][1])
|
||||||
|
cv2.line(vis_hl, (0, hair_top_y), (w, hair_top_y), (255, 255, 0), max(2, round(s * 0.0025)))
|
||||||
|
cv2.line(vis_hl, (0, hairline_y), (w, hairline_y), (0, 100, 255), max(2, round(s * 0.0025)))
|
||||||
|
_draw_text_cv2(vis_hl, f"hair_top_y={hair_top_y}", (hair_top_y if hair_top_y < h - 40 else h - 40, 0),
|
||||||
|
color=(255, 255, 0), scale=max(0.4, s * 0.0015))
|
||||||
|
# 文字标注位置:hairline_y 行右侧
|
||||||
|
_draw_text_cv2(vis_hl, f"hairline_y={hairline_y} (source={result.hairline_source})",
|
||||||
|
(hairline_y, w - int(s * 0.5)), color=(0, 200, 255),
|
||||||
|
scale=max(0.4, s * 0.0015))
|
||||||
|
# 发际线弃用提示:顶庭 < 0.7cm 视为贴近头顶、不可靠
|
||||||
|
if result.hairline_discarded:
|
||||||
|
gap_cm = result.top_cm
|
||||||
|
_draw_text_cv2(vis_hl,
|
||||||
|
f"⚠️ 发际线离头顶仅 {gap_cm:.2f}cm (<0.7cm),已弃用",
|
||||||
|
(10, 10), color=(40, 40, 255), scale=max(0.5, s * 0.0022))
|
||||||
|
put("hairline", vis_hl)
|
||||||
|
|
||||||
|
# ⑥ 四庭纵向点
|
||||||
|
vis_v = image.copy()
|
||||||
|
v_names = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
|
||||||
|
v_labels = ["头顶", "发际线", "眉心", "鼻翼下缘", "下巴尖"]
|
||||||
|
v_court_px = [v["top_court_px"], v["upper_court_px"], v["middle_court_px"], v["lower_court_px"]]
|
||||||
|
court_names = ["顶庭", "上庭", "中庭", "下庭"]
|
||||||
|
court_cm = [result.top_cm, result.upper_cm, result.middle_cm, result.lower_cm]
|
||||||
|
vx0 = min(int(v[n][0]) for n in v_names)
|
||||||
|
for i, name in enumerate(v_names):
|
||||||
|
x, y = int(v[name][0]), int(v[name][1])
|
||||||
|
cv2.circle(vis_v, (x, y), max(3, round(s * 0.004)), (0, 0, 255), -1)
|
||||||
|
# 画一条横线
|
||||||
|
cv2.line(vis_v, (vx0 - max(20, round(s * 0.04)), y),
|
||||||
|
(min(w - 1, vx0 + int(s * 0.02)), y), (0, 200, 255), 1)
|
||||||
|
_draw_text_cv2(vis_v, v_labels[i], (min(w - 60, x + 8), y),
|
||||||
|
color=(50, 255, 255), scale=max(0.4, s * 0.0015))
|
||||||
|
# 各庭段高(竖向虚线 + cm 文字)
|
||||||
|
for i in range(4):
|
||||||
|
y_a = int(v[v_names[i]][1])
|
||||||
|
y_b = int(v[v_names[i + 1]][1])
|
||||||
|
lx = max(10, vx0 - max(40, round(s * 0.08)))
|
||||||
|
cv2.line(vis_v, (lx, y_a), (lx, y_b), (0, 255, 100), max(2, round(s * 0.0025)))
|
||||||
|
cv2.circle(vis_v, (lx, y_a), 3, (0, 255, 100), -1)
|
||||||
|
cv2.circle(vis_v, (lx, y_b), 3, (0, 255, 100), -1)
|
||||||
|
_draw_text_cv2(vis_v, f"{court_names[i]} {court_cm[i]:.2f}cm",
|
||||||
|
(lx - int(s * 0.18), (y_a + y_b) // 2),
|
||||||
|
color=(100, 255, 100), scale=max(0.4, s * 0.0015))
|
||||||
|
dbg["vertical_points"] = {n: {"x": int(v[n][0]), "y": int(v[n][1])} for n in v_names}
|
||||||
|
put("vertical", vis_v)
|
||||||
|
|
||||||
|
# ⑦ 七眼横向点
|
||||||
|
vis_e = image.copy()
|
||||||
|
epts = result.eyes["points"]
|
||||||
|
seven_keys = ["left_cheek", "left_outer", "left_inner", "right_inner", "right_outer", "right_cheek"]
|
||||||
|
seven_labels = ["左脸颊", "左眼外", "左眼内", "右眼内", "右眼外", "右脸颊"]
|
||||||
|
ey0 = min(int(epts[k][1]) for k in seven_keys)
|
||||||
|
for i, k in enumerate(seven_keys):
|
||||||
|
x, y = int(epts[k][0]), int(epts[k][1])
|
||||||
|
cv2.circle(vis_e, (x, y), max(3, round(s * 0.004)), (0, 0, 255), -1)
|
||||||
|
cv2.line(vis_e, (x, max(0, ey0 - 20)), (x, min(h - 1, ey0 + 20)),
|
||||||
|
(0, 200, 255), 1)
|
||||||
|
_draw_text_cv2(vis_e, seven_labels[i], (x, ey0 - max(25, round(s * 0.04))),
|
||||||
|
color=(50, 255, 255), scale=max(0.4, s * 0.0015), anchor="ct")
|
||||||
|
# 头部最左/最右端线(耳朵外缘)
|
||||||
|
try:
|
||||||
|
from face_analysis.annotation import _ear_edges_from_mask
|
||||||
|
lcx, rcx = epts["left_cheek"][0], epts["right_cheek"][0]
|
||||||
|
head_l, head_r = _ear_edges_from_mask(
|
||||||
|
ear_mask, hair_mask, v["hair_top"][1], v["chin_tip"][1],
|
||||||
|
lcx, rcx, (lcx + rcx) / 2)
|
||||||
|
if head_l is not None:
|
||||||
|
cv2.line(vis_e, (int(head_l), 0), (int(head_l), h), (255, 100, 255), max(1, round(s * 0.002)))
|
||||||
|
_draw_text_cv2(vis_e, "人头最左", (int(head_l), 10),
|
||||||
|
color=(255, 150, 255), scale=max(0.35, s * 0.0013))
|
||||||
|
if head_r is not None:
|
||||||
|
cv2.line(vis_e, (int(head_r), 0), (int(head_r), h), (255, 100, 255), max(1, round(s * 0.002)))
|
||||||
|
_draw_text_cv2(vis_e, "人头最右", (int(head_r), 10),
|
||||||
|
color=(255, 150, 255), scale=max(0.35, s * 0.0013))
|
||||||
|
dbg["head_left_x"] = head_l
|
||||||
|
dbg["head_right_x"] = head_r
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("[debug] 七眼端线绘制失败:%s", e)
|
||||||
|
dbg["seven_eye_points"] = {k: {"x": int(epts[k][0]), "y": int(epts[k][1])} for k in seven_keys}
|
||||||
|
put("seven_eyes", vis_e)
|
||||||
|
|
||||||
|
# ⑧ 尺度校准
|
||||||
|
vis_sc = image.copy()
|
||||||
|
px_per_cm = result.px_per_cm
|
||||||
|
iris_px = _iris_diameter_px(lm, w, h)
|
||||||
|
if iris_px is not None and iris_px > 0:
|
||||||
|
# 画左右虹膜直径线(青)
|
||||||
|
for (li, ri) in [(IRIS_LEFT_LEFT, IRIS_LEFT_RIGHT), (IRIS_RIGHT_LEFT, IRIS_RIGHT_RIGHT)]:
|
||||||
|
p1 = normalized_to_pixel(lm[li], w, h)
|
||||||
|
p2 = normalized_to_pixel(lm[ri], w, h)
|
||||||
|
cv2.line(vis_sc, (int(p1[0]), int(p1[1])), (int(p2[0]), int(p2[1])),
|
||||||
|
(255, 200, 0), max(2, round(s * 0.004)))
|
||||||
|
cv2.circle(vis_sc, (int(p1[0]), int(p1[1])), max(2, round(s * 0.003)), (255, 200, 0), -1)
|
||||||
|
cv2.circle(vis_sc, (int(p2[0]), int(p2[1])), max(2, round(s * 0.003)), (255, 200, 0), -1)
|
||||||
|
method = "iris"
|
||||||
|
_draw_text_cv2(vis_sc, f"虹膜直径法: {iris_px:.1f}px / {AVG_IRIS_DIAMETER_CM}cm", (10, 10),
|
||||||
|
color=(255, 200, 0), scale=max(0.45, s * 0.0018))
|
||||||
|
else:
|
||||||
|
# 降级眼宽法(黄)
|
||||||
|
eye_px = _eye_width_px(lm, w, h)
|
||||||
|
method = "eye_width"
|
||||||
|
for (oi, ii) in [(LEFT_EYE_OUTER, LEFT_EYE_INNER), (RIGHT_EYE_INNER, RIGHT_EYE_OUTER)]:
|
||||||
|
p1 = normalized_to_pixel(lm[oi], w, h)
|
||||||
|
p2 = normalized_to_pixel(lm[ii], w, h)
|
||||||
|
cv2.line(vis_sc, (int(p1[0]), int(p1[1])), (int(p2[0]), int(p2[1])),
|
||||||
|
(0, 255, 255), max(2, round(s * 0.004)))
|
||||||
|
_draw_text_cv2(vis_sc, f"眼宽法(降级): {eye_px:.1f}px / {AVG_EYE_WIDTH_CM}cm", (10, 10),
|
||||||
|
color=(0, 255, 255), scale=max(0.45, s * 0.0018))
|
||||||
|
_draw_text_cv2(vis_sc, f"px_per_cm = {px_per_cm:.3f}", (10, 40),
|
||||||
|
color=(50, 255, 50), scale=max(0.5, s * 0.002))
|
||||||
|
dbg["px_per_cm"] = round(px_per_cm, 4)
|
||||||
|
dbg["scale_method"] = method
|
||||||
|
put("scale", vis_sc)
|
||||||
|
|
||||||
|
# 把 to_response 的数值并入 data(前端指标速览复用)
|
||||||
|
data.update(result.to_response())
|
||||||
|
# 七眼段宽
|
||||||
|
try:
|
||||||
|
pc = result.px_per_cm
|
||||||
|
inner_xs = [epts["left_cheek"][0], epts["left_outer"][0], epts["left_inner"][0],
|
||||||
|
epts["right_inner"][0], epts["right_outer"][0], epts["right_cheek"][0]]
|
||||||
|
data.setdefault("seven_eyes", {})
|
||||||
|
for i in range(5):
|
||||||
|
a, b = inner_xs[i], inner_xs[i + 1]
|
||||||
|
data["seven_eyes"][f"eye{i + 2}"] = (
|
||||||
|
None if (a is None or b is None) else round((b - a) / pc, 2))
|
||||||
|
from face_analysis.annotation import _ear_edges_from_mask as _eef
|
||||||
|
lcx, rcx = epts["left_cheek"][0], epts["right_cheek"][0]
|
||||||
|
# 弃用时用眉心做上界(与 _run_face_measure_data 一致)
|
||||||
|
top_y = v["brow_center"][1] if result.hairline_discarded else v["hair_top"][1]
|
||||||
|
head_l, head_r = _eef(ear_mask, hair_mask, top_y, v["chin_tip"][1],
|
||||||
|
lcx, rcx, (lcx + rcx) / 2)
|
||||||
|
data["seven_eyes"]["eye1"] = None if head_l is None else round((lcx - head_l) / pc, 2)
|
||||||
|
data["seven_eyes"]["eye7"] = None if head_r is None else round((head_r - rcx) / pc, 2)
|
||||||
|
except Exception as seg_e: # noqa: BLE001
|
||||||
|
logger.warning("[debug] 七眼段宽计算失败:%s", seg_e)
|
||||||
|
|
||||||
|
# ⑨ 最终标注图(原图 + 标注层叠加)
|
||||||
|
try:
|
||||||
|
from face_analysis.annotation import create_annotated_image
|
||||||
|
annotated = create_annotated_image(image, result, ear_mask=ear_mask, hair_mask=hair_mask)
|
||||||
|
anno_rgba = np.asarray(annotated)
|
||||||
|
# 叠加到原图
|
||||||
|
vis_final = image.copy()
|
||||||
|
alpha = anno_rgba[:, :, 3:4].astype(np.float32) / 255.0
|
||||||
|
vis_final = (vis_final.astype(np.float32) * (1 - alpha)
|
||||||
|
+ anno_rgba[:, :, :3].astype(np.float32) * alpha)
|
||||||
|
vis_final = np.clip(vis_final, 0, 255).astype(np.uint8)
|
||||||
|
put("final", vis_final)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("[debug] 标注图叠加失败:%s", e)
|
||||||
|
|
||||||
|
return data, None, None
|
||||||
|
|
||||||
|
|
||||||
async def _face_measure_impl(image_file, image_url, image_base64, variant="v1"):
|
async def _face_measure_impl(image_file, image_url, image_base64, variant="v1"):
|
||||||
"""接口1/6 共用实现:四庭七眼测量 + 标注图生成。返回 (ok_dict, err_dict)。
|
"""接口1/6 共用实现:四庭七眼测量 + 标注图生成。返回 (ok_dict, err_dict)。
|
||||||
|
|
||||||
@@ -599,6 +991,39 @@ async def face_measure(
|
|||||||
return ok_data if ok_data is not None else err_data
|
return ok_data if ok_data is not None else err_data
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 接口1 调试:分步可视化(每一步中间产物图 + 原理)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@app.post("/api/v1/face/measure-debug", include_in_schema=False)
|
||||||
|
async def face_measure_debug(
|
||||||
|
image_file: Optional[UploadFile] = File(default=None),
|
||||||
|
image_url: Optional[str] = Form(default=None),
|
||||||
|
image_base64: Optional[str] = Form(default=None),
|
||||||
|
):
|
||||||
|
"""接口1 调试:返回算法每一步的中间产物图(data.steps.*_base64)+ 数值(data.debug)。
|
||||||
|
|
||||||
|
与正式接口同链路,但额外产出 9 张分步叠加图(输入/关键点/姿态/分割/发际线/
|
||||||
|
四庭/七眼/尺度/最终标注),供调试页分步可视化。错误时仍返回已完成的步骤图。
|
||||||
|
"""
|
||||||
|
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
|
||||||
|
if e is not None:
|
||||||
|
return e
|
||||||
|
image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
|
||||||
|
if image is None:
|
||||||
|
return err(1008, "图片格式不支持(仅 JPG / PNG)")
|
||||||
|
try:
|
||||||
|
data, code, msg = _run_face_measure_data_debug(image)
|
||||||
|
if code is not None:
|
||||||
|
# 仍带分步图返回,前端可展示卡在哪一步
|
||||||
|
return {"code": code, "message": msg,
|
||||||
|
"request_id": "mock-request-id", "data": data}
|
||||||
|
return ok(data)
|
||||||
|
except Exception as ex: # noqa: BLE001
|
||||||
|
logger.exception("接口1 调试处理异常")
|
||||||
|
return err(1007, f"处理失败:{ex}")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 接口 6:四庭七眼测量标注 v2(复刻接口1)
|
# 接口 6:四庭七眼测量标注 v2(复刻接口1)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -714,11 +1139,10 @@ async def face_measure_v2(
|
|||||||
- 发际线类型 `hairline_type`(英文 key)
|
- 发际线类型 `hairline_type`(英文 key)
|
||||||
- 顺序 `order`(本期固定 `1..N`,不排序)
|
- 顺序 `order`(本期固定 `1..N`,不排序)
|
||||||
|
|
||||||
> **female 走「换发型」模式**:1..5 的生发图 `grown_image_base64` 由换发型(change_hair)
|
> **female 走「换发型」模式**:生发图 `grown_image_base64` 由换发型(change_hair)
|
||||||
> + Flux-2 整帧重绘(= 接口12 final 管线,整帧美颜+整帧重绘)生成,其余参数用固化默认值。
|
> + Flux-2 整帧重绘(= 接口12 final 管线,整帧美颜+整帧重绘)生成,其余参数用固化默认值。
|
||||||
> **6/7(bigflower/clasicalflower)与 male 一样走原生发(ComfyUI add_hair)管线**。
|
> **male 仍走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。
|
||||||
> **male 全部走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。
|
> female 依赖 change_hair 与 ComfyUI(:8188) 均在跑。
|
||||||
> female 1..5 依赖 change_hair 与 ComfyUI(:8188) 均在跑。
|
|
||||||
|
|
||||||
{_image_fields_desc}
|
{_image_fields_desc}
|
||||||
|
|
||||||
@@ -726,15 +1150,15 @@ async def face_measure_v2(
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 7 张 / male 6 张)。
|
- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 5 张 / male 4 张)。
|
||||||
非法或缺失返回 `1004`。
|
非法或缺失返回 `1004`。
|
||||||
- **hair_style**(必填):发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数量。
|
- **hair_style**(必填):发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数量。
|
||||||
`female`:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave, 6=bigflower, 7=clasicalflower;
|
`female`:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;
|
||||||
`male`:1=ellipse, 2=inverse_arc, 3=m, 4=straight, 5=heart, 6=Softpetal。越界/非法返回 `1007`。
|
`male`:1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界/非法返回 `1007`。
|
||||||
- **beauty_enabled**:本期保留但不生效。
|
- **beauty_enabled**:本期保留但不生效。
|
||||||
|
|
||||||
`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave` / `bigflower` / `clasicalflower`(female),
|
`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave`(female),
|
||||||
`ellipse` / `m` / `straight` / `inverse_arc` / `heart` / `Softpetal`(male)。
|
`ellipse` / `m` / `straight` / `inverse_arc`(male)。
|
||||||
""",
|
""",
|
||||||
responses={
|
responses={
|
||||||
200: {
|
200: {
|
||||||
@@ -773,7 +1197,7 @@ async def hair_grow(
|
|||||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||||
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-7 male:1-6"),
|
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的文本"),
|
||||||
@@ -785,7 +1209,7 @@ async def hair_grow(
|
|||||||
return err(1004, "gender 必填且只能为 male / female")
|
return err(1004, "gender 必填且只能为 male / female")
|
||||||
|
|
||||||
# 2. hair_style 必填校验(解析逗号分隔,越界 → 1007)
|
# 2. hair_style 必填校验(解析逗号分隔,越界 → 1007)
|
||||||
max_styles = {"female": 7, "male": 6}[gender]
|
max_styles = {"female": 5, "male": 4}[gender]
|
||||||
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
||||||
if hair_styles is None:
|
if hair_styles is None:
|
||||||
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
||||||
@@ -808,7 +1232,7 @@ async def hair_grow(
|
|||||||
from hairline.service import generate_grow_results_swap
|
from hairline.service import generate_grow_results_swap
|
||||||
items = await run_in_threadpool(
|
items = await run_in_threadpool(
|
||||||
generate_grow_results_swap, image, hair_styles, _V2_FINAL_DEFAULTS,
|
generate_grow_results_swap, image, hair_styles, _V2_FINAL_DEFAULTS,
|
||||||
redraw_max_side=redraw_max_side, unet_name=flux_model, prompt=prompt)
|
redraw_max_side=redraw_max_side, unet_name=flux_model)
|
||||||
else:
|
else:
|
||||||
from hairline.service import generate_grow_results
|
from hairline.service import generate_grow_results
|
||||||
items = await run_in_threadpool(
|
items = await run_in_threadpool(
|
||||||
@@ -871,7 +1295,7 @@ async def debug_grow_timing(
|
|||||||
return err(1008, "图片格式不支持(仅 JPG / PNG)")
|
return err(1008, "图片格式不支持(仅 JPG / PNG)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
max_styles = 7
|
max_styles = 5
|
||||||
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
||||||
if hair_styles is None:
|
if hair_styles is None:
|
||||||
return err(1007, f"hair_style 必须为 1..{max_styles}")
|
return err(1007, f"hair_style 必须为 1..{max_styles}")
|
||||||
@@ -1160,18 +1584,13 @@ async def face_features(
|
|||||||
---
|
---
|
||||||
|
|
||||||
**入参**(同接口2:先选性别,再多选发型):
|
**入参**(同接口2:先选性别,再多选发型):
|
||||||
- 必填 `gender`(`male`/`female`),决定发型集合(female 7 / male 6)。
|
- 必填 `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,6=bigflower,7=clasicalflower;
|
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave;`male`:1=ellipse,2=inverse_arc,3=m,4=straight。
|
||||||
`male`:1=ellipse,2=inverse_arc,3=m,4=straight,5=heart,6=Softpetal。
|
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。
|
||||||
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数(仅 male 路径生效)。
|
|
||||||
注:生发黑模板固定取 `hairline_texture_black/`(middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle。
|
注:生发黑模板固定取 `hairline_texture_black/`(middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle。
|
||||||
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(ComfyUI 生发,全流程最耗时)。
|
- 可选 `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 路径生效)。
|
|
||||||
|
|
||||||
**返回说明**:
|
**返回说明**:
|
||||||
|
|
||||||
@@ -1247,18 +1666,16 @@ async def hairline_generate(
|
|||||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||||
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-7 male:1-6"),
|
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,仅 male 路径生效)"),
|
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词(同接口2,仅 male 路径生效),会替换工作流节点60的文本"),
|
prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
||||||
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
|
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(ComfyUI 生发,最耗时)。默认 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")
|
||||||
|
|
||||||
# hair_style 必填(同接口2):解析逗号分隔,缺失/越界/非法 → 1007
|
# hair_style 必填(同接口2):解析逗号分隔,缺失/越界/非法 → 1007
|
||||||
max_styles = {"female": 7, "male": 6}[gender]
|
max_styles = {"female": 5, "male": 4}[gender]
|
||||||
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
||||||
if hair_styles is None:
|
if hair_styles is None:
|
||||||
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
||||||
@@ -1277,9 +1694,7 @@ 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, "无法识别人像")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""快速测试:3张图×花瓣发型×896分辨率,新提示词"填充遮罩区域的头发"。
|
||||||
|
预热1次+正式1次。
|
||||||
|
"""
|
||||||
|
import base64, json, os, time
|
||||||
|
from pathlib import Path
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API = "http://127.0.0.1:8187/api/v1/debug/grow-timing"
|
||||||
|
TOKEN = "dev-shared-secret-2026"
|
||||||
|
PROMPT = "填充遮罩区域的头发"
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/bench6")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
IMGS = [
|
||||||
|
("asdf", "/home/ubuntu/hair/image/asdf.jpg"),
|
||||||
|
("qwer", "/home/ubuntu/hair/image/qwer.jpg"),
|
||||||
|
("girl5", "/home/ubuntu/hair/image/girl_img/girl5.jpg"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def call(img_path, save_grown=None, timeout=300):
|
||||||
|
data = {"hair_style": "2", "webui_steps": "15", "redraw_max_side": "896", "redraw_prompt": PROMPT}
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
with open(img_path, "rb") as f:
|
||||||
|
r = requests.post(API, headers={"X-Internal-Token": TOKEN},
|
||||||
|
files={"image_file": (os.path.basename(img_path), f, "image/jpeg")},
|
||||||
|
data=data, timeout=timeout)
|
||||||
|
wall = time.perf_counter() - t0
|
||||||
|
j = r.json()
|
||||||
|
d = j["data"]; hs = d["per_hairstyle"][0]
|
||||||
|
if save_grown and hs.get("grown_b64"):
|
||||||
|
b = hs["grown_b64"].split(",")[1] if "," in hs["grown_b64"] else hs["grown_b64"]
|
||||||
|
open(save_grown, "wb").write(base64.b64decode(b))
|
||||||
|
return {"ok": hs.get("ok"), "total_ms": d["total_ms"], "comfy_ms": hs.get("comfyui_redraw_ms"),
|
||||||
|
"grown_path": str(save_grown) if save_grown and hs.get("ok") else None}
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for ilabel, ipath in IMGS:
|
||||||
|
print(f"预热 {ilabel}...", flush=True)
|
||||||
|
call(ipath)
|
||||||
|
save = OUT / f"{ilabel}_flower_896.jpg"
|
||||||
|
print(f"正式 {ilabel}...", flush=True)
|
||||||
|
r = call(ipath, save_grown=save)
|
||||||
|
r["img"] = ilabel
|
||||||
|
print(f" -> total={r['total_ms']}ms ok={r['ok']}", flush=True)
|
||||||
|
results.append(r)
|
||||||
|
|
||||||
|
json.dump({"prompt": PROMPT, "results": results}, open(OUT/"results.json","w"), ensure_ascii=False, indent=2)
|
||||||
|
print(f"\n✓ 完成 {sum(1 for r in results if r['ok'])}/3", flush=True)
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""分辨率对比测试:4图×5发型=20行,每行4种分辨率(不缩放/896/768/640),steps=15。
|
||||||
|
热数据:每个组合预热1次(丢弃)+正式1次。OOM的跳过记录为失败。
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API = "http://127.0.0.1:8187/api/v1/debug/grow-timing"
|
||||||
|
TOKEN = "dev-shared-secret-2026"
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/bench3")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
IMGS = [
|
||||||
|
("asdf", "/home/ubuntu/hair/image/asdf.jpg"),
|
||||||
|
("qwer", "/home/ubuntu/hair/image/qwer.jpg"),
|
||||||
|
("girl2", "/home/ubuntu/hair/image/girl_img/girl2.jpg"),
|
||||||
|
("girl5", "/home/ubuntu/hair/image/girl_img/girl5.jpg"),
|
||||||
|
]
|
||||||
|
HAIRSTYLES = [
|
||||||
|
(1, "ellipse", "椭圆"), (2, "flower", "花瓣"), (3, "heart", "心形"),
|
||||||
|
(4, "straight", "直线"), (5, "wave", "波浪"),
|
||||||
|
]
|
||||||
|
# 分辨率档:0=不缩放(原图)
|
||||||
|
RES_LIST = [("orig", "0"), ("896", "896"), ("768", "768"), ("640", "640")]
|
||||||
|
RES_TITLES = ["原图(不缩放)", "896", "768", "640"]
|
||||||
|
STEPS = 15
|
||||||
|
|
||||||
|
|
||||||
|
def call(img_path, hair_num, redraw_max_side, save_grown=None, timeout=300):
|
||||||
|
data = {"hair_style": str(hair_num), "webui_steps": str(STEPS),
|
||||||
|
"redraw_max_side": str(redraw_max_side)}
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
try:
|
||||||
|
with open(img_path, "rb") as f:
|
||||||
|
r = requests.post(API, headers={"X-Internal-Token": TOKEN},
|
||||||
|
files={"image_file": (os.path.basename(img_path), f, "image/jpeg")},
|
||||||
|
data=data, timeout=timeout)
|
||||||
|
wall = time.perf_counter() - t0
|
||||||
|
j = r.json()
|
||||||
|
if j.get("code") != 0:
|
||||||
|
return {"ok": False, "error": j.get("message", "")[:80], "wall": wall}
|
||||||
|
d = j["data"]
|
||||||
|
hs = d["per_hairstyle"][0]
|
||||||
|
if save_grown and hs.get("grown_b64"):
|
||||||
|
b = hs["grown_b64"].split(",")[1] if "," in hs["grown_b64"] else hs["grown_b64"]
|
||||||
|
with open(save_grown, "wb") as gf:
|
||||||
|
gf.write(base64.b64decode(b))
|
||||||
|
return {
|
||||||
|
"ok": hs.get("ok", False), "wall": wall,
|
||||||
|
"total_ms": d["total_ms"], "swap_ms": hs.get("swap_ms"),
|
||||||
|
"comfy_ms": hs.get("comfyui_redraw_ms"),
|
||||||
|
"error": hs.get("error"),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": str(e)[:80], "wall": time.perf_counter() - t0}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rows = []
|
||||||
|
total = len(IMGS) * len(HAIRSTYLES) * len(RES_LIST) * 2
|
||||||
|
idx = 0
|
||||||
|
for ilabel, ipath in IMGS:
|
||||||
|
for hnum, hkey, hname in HAIRSTYLES:
|
||||||
|
cells = []
|
||||||
|
for (rlabel, rval), rtitle in zip(RES_LIST, RES_TITLES):
|
||||||
|
# 预热
|
||||||
|
idx += 1
|
||||||
|
print(f"[{idx}/{total}] 预热 {ilabel}|{hname}|{rtitle}", flush=True)
|
||||||
|
try:
|
||||||
|
call(ipath, hnum, rval, timeout=120)
|
||||||
|
except Exception:
|
||||||
|
pass # 预热失败(可能OOM)不中断
|
||||||
|
# 正式
|
||||||
|
idx += 1
|
||||||
|
save = OUT / f"{ilabel}_{hkey}_{rlabel}.jpg"
|
||||||
|
print(f"[{idx}/{total}] 正式 {ilabel}|{hname}|{rtitle}", flush=True)
|
||||||
|
r = call(ipath, hnum, rval, save_grown=save, timeout=300)
|
||||||
|
r["res_label"] = rlabel; r["res_title"] = rtitle
|
||||||
|
r["grown_path"] = str(save) if r.get("ok") else None
|
||||||
|
status = f"{r.get('total_ms')}ms" if r.get("ok") else f"FAIL:{r.get('error','')[:30]}"
|
||||||
|
print(f" -> {status}", flush=True)
|
||||||
|
cells.append(r)
|
||||||
|
rows.append({"img": ilabel, "img_path": ipath,
|
||||||
|
"hair_num": hnum, "hair_key": hkey, "hair_name": hname,
|
||||||
|
"cells": cells})
|
||||||
|
with open(OUT / "results.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"res_titles": RES_TITLES, "rows": rows}, f, ensure_ascii=False, indent=2)
|
||||||
|
ok = sum(1 for row in rows for c in row["cells"] if c.get("ok"))
|
||||||
|
print(f"\n✓ 完成: {ok}/{len(rows)*len(RES_LIST)} 成功 -> {OUT/'results.json'}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""生成分辨率对比报告:20行(4图×5发型) × 4列(原图不缩放/896/768/640)。"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/bench3")
|
||||||
|
RESULTS = OUT / "results.json"
|
||||||
|
HTML = OUT / "report.html"
|
||||||
|
|
||||||
|
|
||||||
|
def img_src(path):
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
return None
|
||||||
|
return "bench3/" + os.path.basename(path)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
d = json.load(open(RESULTS, encoding="utf-8"))
|
||||||
|
titles = d["res_titles"]
|
||||||
|
rows = d["rows"]
|
||||||
|
|
||||||
|
# 各分辨率平均耗时
|
||||||
|
col_stats = defaultdict(lambda: {"total": [], "comfy": []})
|
||||||
|
for r in rows:
|
||||||
|
for c in r["cells"]:
|
||||||
|
if c.get("ok"):
|
||||||
|
col_stats[c["res_title"]]["total"].append(c["total_ms"])
|
||||||
|
col_stats[c["res_title"]]["comfy"].append(c.get("comfy_ms", 0))
|
||||||
|
|
||||||
|
# 表头
|
||||||
|
headers = ['<th class="col-label">原图</th>']
|
||||||
|
for t in titles:
|
||||||
|
s = col_stats.get(t)
|
||||||
|
avg = sum(s["total"]) // len(s["total"]) if s and s["total"] else 0
|
||||||
|
headers.append(f'<th class="col-label"><div class="col-title">{t}</div>'
|
||||||
|
f'<div class="col-stat">均{avg/1000:.1f}s</div></th>')
|
||||||
|
|
||||||
|
# 表体
|
||||||
|
body_rows = []
|
||||||
|
for r in rows:
|
||||||
|
label = f'<div class="row-label">{r["img"]}<br><b>{r["hair_name"]}</b></div>'
|
||||||
|
# 原图缩略图(用 orig 档的结果当原图展示,或用原图文件)
|
||||||
|
orig_cell = f'<td class="cell-orig"><div class="row-label-cell">{label}</div></td>'
|
||||||
|
cells = [orig_cell]
|
||||||
|
for c in r["cells"]:
|
||||||
|
src = img_src(c.get("grown_path")) if c.get("ok") else None
|
||||||
|
if src:
|
||||||
|
t = c.get("total_ms", 0)
|
||||||
|
cells.append(f'<td class="cell-result"><img class="result-img" src="{src}" loading="lazy">'
|
||||||
|
f'<div class="cell-time">{t/1000:.1f}s</div></td>')
|
||||||
|
else:
|
||||||
|
cells.append(f'<td class="cell-result"><div class="na">⚠<br>{c.get("error","")[:20]}</div></td>')
|
||||||
|
body_rows.append(f'<tr>{"".join(cells)}</tr>')
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>重绘分辨率对比报告 — steps=15</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: -apple-system, "Segoe UI", sans-serif; background: #f5f5f5; padding: 16px; }}
|
||||||
|
h1 {{ font-size: 20px; margin-bottom: 4px; }}
|
||||||
|
.subtitle {{ color: #888; font-size: 12px; margin-bottom: 12px; }}
|
||||||
|
.legend {{ background: #fff; border-radius: 8px; padding: 10px 16px; margin-bottom: 12px; font-size: 12px; color: #555; }}
|
||||||
|
.scroll-wrap {{ overflow-x: auto; }}
|
||||||
|
table {{ border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,.06); }}
|
||||||
|
th, td {{ border: 1px solid #eee; padding: 6px; vertical-align: top; text-align: center; }}
|
||||||
|
th {{ background: #f9fafb; position: sticky; top: 0; }}
|
||||||
|
.col-label {{ min-width: 130px; max-width: 150px; }}
|
||||||
|
.col-title {{ font-size: 12px; font-weight: 700; color: #374151; }}
|
||||||
|
.col-stat {{ font-size: 10px; color: #9ca3af; margin-top: 2px; }}
|
||||||
|
.row-label {{ font-size: 11px; color: #6b7280; }}
|
||||||
|
.row-label b {{ color: #1f2937; }}
|
||||||
|
img {{ border-radius: 4px; max-width: 130px; max-height: 160px; object-fit: contain; background: #f3f4f6; }}
|
||||||
|
.cell-time {{ font-size: 10px; color: #9ca3af; margin-top: 2px; }}
|
||||||
|
.na {{ color: #d1d5db; font-size: 12px; padding: 40px 10px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📊 重绘分辨率对比报告</h1>
|
||||||
|
<p class="subtitle">4图×5发型=20行 · 每行4分辨率(原图不缩放/896/768/640) · steps=15 · 热数据 · 80/80成功 · 峰值21.2GB · 0 OOM</p>
|
||||||
|
<div class="legend">列标题下显示<b>平均总耗时</b>。横向滚动查看。原图列含图片名+发型名。每格下方为该次总耗时。</div>
|
||||||
|
<div class="scroll-wrap">
|
||||||
|
<table>
|
||||||
|
<tr>{"".join(headers)}</tr>
|
||||||
|
{"".join(body_rows)}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
with open(HTML, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html)
|
||||||
|
print(f"✓ 报告: {HTML} ({HTML.stat().st_size // 1024} KB)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""分辨率对比测试(新提示词版):4图×5发型=20行,每行4种分辨率,steps=15。
|
||||||
|
提示词固定为 "填充遮罩区域的头发"。
|
||||||
|
热数据:预热1次+正式1次。
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API = "http://127.0.0.1:8187/api/v1/debug/grow-timing"
|
||||||
|
TOKEN = "dev-shared-secret-2026"
|
||||||
|
PROMPT = "填充遮罩区域的头发"
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/bench7")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
IMGS = [
|
||||||
|
("asdf", "/home/ubuntu/hair/image/asdf.jpg"),
|
||||||
|
("qwer", "/home/ubuntu/hair/image/qwer.jpg"),
|
||||||
|
("girl2", "/home/ubuntu/hair/image/girl_img/girl2.jpg"),
|
||||||
|
("girl5", "/home/ubuntu/hair/image/girl_img/girl5.jpg"),
|
||||||
|
]
|
||||||
|
HAIRSTYLES = [
|
||||||
|
(1, "ellipse", "椭圆"), (2, "flower", "花瓣"), (3, "heart", "心形"),
|
||||||
|
(4, "straight", "直线"), (5, "wave", "波浪"),
|
||||||
|
]
|
||||||
|
RES_LIST = [("orig", "0"), ("896", "896"), ("768", "768"), ("640", "640")]
|
||||||
|
RES_TITLES = ["原图(不缩放)", "896", "768", "640"]
|
||||||
|
STEPS = 15
|
||||||
|
|
||||||
|
|
||||||
|
def call(img_path, hair_num, redraw_max_side, save_grown=None, timeout=300):
|
||||||
|
data = {"hair_style": str(hair_num), "webui_steps": str(STEPS),
|
||||||
|
"redraw_max_side": str(redraw_max_side), "redraw_prompt": PROMPT}
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
try:
|
||||||
|
with open(img_path, "rb") as f:
|
||||||
|
r = requests.post(API, headers={"X-Internal-Token": TOKEN},
|
||||||
|
files={"image_file": (os.path.basename(img_path), f, "image/jpeg")},
|
||||||
|
data=data, timeout=timeout)
|
||||||
|
wall = time.perf_counter() - t0
|
||||||
|
j = r.json()
|
||||||
|
if j.get("code") != 0:
|
||||||
|
return {"ok": False, "error": j.get("message", "")[:80], "wall": wall}
|
||||||
|
d = j["data"]
|
||||||
|
hs = d["per_hairstyle"][0]
|
||||||
|
if save_grown and hs.get("grown_b64"):
|
||||||
|
b = hs["grown_b64"].split(",")[1] if "," in hs["grown_b64"] else hs["grown_b64"]
|
||||||
|
with open(save_grown, "wb") as gf:
|
||||||
|
gf.write(base64.b64decode(b))
|
||||||
|
return {
|
||||||
|
"ok": hs.get("ok", False), "wall": wall,
|
||||||
|
"total_ms": d["total_ms"], "swap_ms": hs.get("swap_ms"),
|
||||||
|
"comfy_ms": hs.get("comfyui_redraw_ms"),
|
||||||
|
"error": hs.get("error"),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": str(e)[:80], "wall": time.perf_counter() - t0}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rows = []
|
||||||
|
total = len(IMGS) * len(HAIRSTYLES) * len(RES_LIST) * 2
|
||||||
|
idx = 0
|
||||||
|
for ilabel, ipath in IMGS:
|
||||||
|
for hnum, hkey, hname in HAIRSTYLES:
|
||||||
|
cells = []
|
||||||
|
for (rlabel, rval), rtitle in zip(RES_LIST, RES_TITLES):
|
||||||
|
idx += 1
|
||||||
|
print(f"[{idx}/{total}] 预热 {ilabel}|{hname}|{rtitle}", flush=True)
|
||||||
|
try:
|
||||||
|
call(ipath, hnum, rval, timeout=120)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
idx += 1
|
||||||
|
save = OUT / f"{ilabel}_{hkey}_{rlabel}.jpg"
|
||||||
|
print(f"[{idx}/{total}] 正式 {ilabel}|{hname}|{rtitle}", flush=True)
|
||||||
|
r = call(ipath, hnum, rval, save_grown=save, timeout=300)
|
||||||
|
r["res_label"] = rlabel; r["res_title"] = rtitle
|
||||||
|
r["grown_path"] = str(save) if r.get("ok") else None
|
||||||
|
status = f"{r.get('total_ms')}ms" if r.get("ok") else f"FAIL:{r.get('error','')[:30]}"
|
||||||
|
print(f" -> {status}", flush=True)
|
||||||
|
cells.append(r)
|
||||||
|
rows.append({"img": ilabel, "img_path": ipath,
|
||||||
|
"hair_num": hnum, "hair_key": hkey, "hair_name": hname,
|
||||||
|
"cells": cells})
|
||||||
|
with open(OUT / "results.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"res_titles": RES_TITLES, "prompt": PROMPT, "rows": rows}, f, ensure_ascii=False, indent=2)
|
||||||
|
ok = sum(1 for row in rows for c in row["cells"] if c.get("ok"))
|
||||||
|
print(f"\n✓ 完成: {ok}/{len(rows)*len(RES_LIST)} 成功 -> {OUT/'results.json'}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""swap步数 + 重绘分辨率 对比测试(热数据)。
|
||||||
|
|
||||||
|
每个组合: 预热1次(丢弃) + 正式测1次(取热数据)。
|
||||||
|
B维度: steps=10/15/20 (分辨率固定896)
|
||||||
|
C维度: 分辨率=640/896/1024 (steps固定15)
|
||||||
|
4图×2发型=8组 × 6档 × 2次(预热+正式) = 96次
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API = "http://127.0.0.1:8187/api/v1/debug/grow-timing"
|
||||||
|
TOKEN = "dev-shared-secret-2026"
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/bench2")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
IMGS = [
|
||||||
|
("asdf", "/home/ubuntu/hair/image/asdf.jpg"),
|
||||||
|
("qwer", "/home/ubuntu/hair/image/qwer.jpg"),
|
||||||
|
("girl2", "/home/ubuntu/hair/image/girl_img/girl2.jpg"),
|
||||||
|
("girl5", "/home/ubuntu/hair/image/girl_img/girl5.jpg"),
|
||||||
|
]
|
||||||
|
HAIRSTYLES = [(5, "wave", "波浪"), (3, "heart", "心形")]
|
||||||
|
|
||||||
|
# B维度: swap步数对比 (分辨率固定896)
|
||||||
|
B_STEPS = [10, 15, 20]
|
||||||
|
# C维度: 重绘分辨率对比 (steps固定15)
|
||||||
|
C_RES = [640, 896, 1024]
|
||||||
|
|
||||||
|
|
||||||
|
def call(img_path, hair_num, webui_steps=None, redraw_max_side=None, save_grown=None):
|
||||||
|
"""调调试接口。返回 dict。save_grown 非None时把结果图存到该路径。"""
|
||||||
|
data = {"hair_style": str(hair_num)}
|
||||||
|
if webui_steps is not None:
|
||||||
|
data["webui_steps"] = str(webui_steps)
|
||||||
|
if redraw_max_side is not None:
|
||||||
|
data["redraw_max_side"] = str(redraw_max_side)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
try:
|
||||||
|
with open(img_path, "rb") as f:
|
||||||
|
r = requests.post(API, headers={"X-Internal-Token": TOKEN},
|
||||||
|
files={"image_file": (os.path.basename(img_path), f, "image/jpeg")},
|
||||||
|
data=data, timeout=300)
|
||||||
|
wall = time.perf_counter() - t0
|
||||||
|
j = r.json()
|
||||||
|
if j.get("code") != 0:
|
||||||
|
return {"ok": False, "error": j.get("message", "")[:100], "wall": wall}
|
||||||
|
d = j["data"]
|
||||||
|
hs = d["per_hairstyle"][0]
|
||||||
|
if save_grown and hs.get("grown_b64"):
|
||||||
|
b = hs["grown_b64"].split(",")[1] if "," in hs["grown_b64"] else hs["grown_b64"]
|
||||||
|
with open(save_grown, "wb") as gf:
|
||||||
|
gf.write(base64.b64decode(b))
|
||||||
|
return {
|
||||||
|
"ok": hs.get("ok", False), "wall": wall,
|
||||||
|
"total_ms": d["total_ms"], "ctx_ms": d["extract_context_ms"],
|
||||||
|
"mask_ms": hs.get("mask_ms"), "swap_ms": hs.get("swap_ms"),
|
||||||
|
"blend_ms": hs.get("blend_ms"), "comfy_ms": hs.get("comfyui_redraw_ms"),
|
||||||
|
"error": hs.get("error"),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": str(e)[:100], "wall": time.perf_counter() - t0}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
results = {"B_steps": [], "C_res": []}
|
||||||
|
total_calls = len(IMGS) * len(HAIRSTYLES) * (len(B_STEPS) + len(C_RES)) * 2
|
||||||
|
idx = 0
|
||||||
|
|
||||||
|
# ===== B维度: swap步数对比 (分辨率固定896) =====
|
||||||
|
print("\n===== B维度: swap步数对比 (分辨率=896) =====", flush=True)
|
||||||
|
for steps in B_STEPS:
|
||||||
|
print(f"\n--- steps={steps} ---", flush=True)
|
||||||
|
for ilabel, ipath in IMGS:
|
||||||
|
for hnum, hkey, hname in HAIRSTYLES:
|
||||||
|
# 预热(丢弃)
|
||||||
|
idx += 1
|
||||||
|
print(f"[{idx}/{total_calls}] 预热 {ilabel}|{hname}|steps={steps}", flush=True)
|
||||||
|
call(ipath, hnum, webui_steps=steps, redraw_max_side=896)
|
||||||
|
# 正式(热数据)
|
||||||
|
idx += 1
|
||||||
|
save = OUT / f"B_steps{steps}_{ilabel}_{hkey}.jpg"
|
||||||
|
print(f"[{idx}/{total_calls}] 正式 {ilabel}|{hname}|steps={steps}", flush=True)
|
||||||
|
r = call(ipath, hnum, webui_steps=steps, redraw_max_side=896, save_grown=save)
|
||||||
|
r["steps"] = steps; r["img"] = ilabel; r["hair"] = hkey; r["hair_name"] = hname
|
||||||
|
r["grown_path"] = str(save) if r.get("ok") else None
|
||||||
|
print(f" -> total={r.get('total_ms')}ms swap={r.get('swap_ms')}ms comfy={r.get('comfy_ms')}ms ok={r.get('ok')}", flush=True)
|
||||||
|
results["B_steps"].append(r)
|
||||||
|
|
||||||
|
# ===== C维度: 重绘分辨率对比 (steps固定15) =====
|
||||||
|
print("\n===== C维度: 重绘分辨率对比 (steps=15) =====", flush=True)
|
||||||
|
for res in C_RES:
|
||||||
|
print(f"\n--- res={res} ---", flush=True)
|
||||||
|
for ilabel, ipath in IMGS:
|
||||||
|
for hnum, hkey, hname in HAIRSTYLES:
|
||||||
|
idx += 1
|
||||||
|
print(f"[{idx}/{total_calls}] 预热 {ilabel}|{hname}|res={res}", flush=True)
|
||||||
|
call(ipath, hnum, webui_steps=15, redraw_max_side=res)
|
||||||
|
idx += 1
|
||||||
|
save = OUT / f"C_res{res}_{ilabel}_{hkey}.jpg"
|
||||||
|
print(f"[{idx}/{total_calls}] 正式 {ilabel}|{hname}|res={res}", flush=True)
|
||||||
|
r = call(ipath, hnum, webui_steps=15, redraw_max_side=res, save_grown=save)
|
||||||
|
r["res"] = res; r["img"] = ilabel; r["hair"] = hkey; r["hair_name"] = hname
|
||||||
|
r["grown_path"] = str(save) if r.get("ok") else None
|
||||||
|
print(f" -> total={r.get('total_ms')}ms swap={r.get('swap_ms')}ms comfy={r.get('comfy_ms')}ms ok={r.get('ok')}", flush=True)
|
||||||
|
results["C_res"].append(r)
|
||||||
|
|
||||||
|
with open(OUT / "results.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||||
|
ok = sum(1 for r in results["B_steps"] + results["C_res"] if r.get("ok"))
|
||||||
|
print(f"\n✓ 完成: {ok}/{len(results['B_steps'])+len(results['C_res'])} 成功 -> {OUT/'results.json'}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""生成 swap步数 + 重绘分辨率 对比报告 HTML。"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/bench2")
|
||||||
|
RESULTS = OUT / "results.json"
|
||||||
|
HTML = OUT / "report.html"
|
||||||
|
|
||||||
|
|
||||||
|
def img_src(path):
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
return None
|
||||||
|
# benchmark_out/bench2/xxx.jpg -> bench2/xxx.jpg (报告在 static/ 下部署时调整)
|
||||||
|
p = str(path)
|
||||||
|
return "bench2/" + os.path.basename(p)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
d = json.load(open(RESULTS, encoding="utf-8"))
|
||||||
|
b_data = d["B_steps"] # steps 对比
|
||||||
|
c_data = d["C_res"] # 分辨率对比
|
||||||
|
|
||||||
|
# B维度聚合
|
||||||
|
by_steps = defaultdict(list)
|
||||||
|
for r in b_data:
|
||||||
|
by_steps[r["steps"]].append(r)
|
||||||
|
b_summary = []
|
||||||
|
for s in sorted(by_steps):
|
||||||
|
rs = by_steps[s]
|
||||||
|
b_summary.append({
|
||||||
|
"label": f"steps={s}", "n": len(rs),
|
||||||
|
"swap": sum(r["swap_ms"] for r in rs) // len(rs),
|
||||||
|
"total": sum(r["total_ms"] for r in rs) // len(rs),
|
||||||
|
})
|
||||||
|
|
||||||
|
# C维度聚合
|
||||||
|
by_res = defaultdict(list)
|
||||||
|
for r in c_data:
|
||||||
|
by_res[r["res"]].append(r)
|
||||||
|
c_summary = []
|
||||||
|
for res in sorted(by_res):
|
||||||
|
rs = by_res[res]
|
||||||
|
c_summary.append({
|
||||||
|
"label": f"res={res}", "n": len(rs),
|
||||||
|
"comfy": sum(r["comfy_ms"] for r in rs) // len(rs),
|
||||||
|
"total": sum(r["total_ms"] for r in rs) // len(rs),
|
||||||
|
})
|
||||||
|
|
||||||
|
# B维度明细行(每图每发型每步数)
|
||||||
|
b_rows = []
|
||||||
|
for r in sorted(b_data, key=lambda x: (x["img"], x["hair"], x["steps"])):
|
||||||
|
src = img_src(r.get("grown_path"))
|
||||||
|
b_rows.append(f"""<tr>
|
||||||
|
<td>{r['img']}</td><td>{r['hair_name']}</td><td>{r['steps']}</td>
|
||||||
|
<td>{r.get('swap_ms','?')}</td><td>{r.get('comfy_ms','?')}</td><td>{r.get('total_ms','?')}</td>
|
||||||
|
<td>{f'<img src="{src}" loading="lazy">' if src else '⚠'}</td></tr>""")
|
||||||
|
|
||||||
|
# C维度明细行
|
||||||
|
c_rows = []
|
||||||
|
for r in sorted(c_data, key=lambda x: (x["img"], x["hair"], x["res"])):
|
||||||
|
src = img_src(r.get("grown_path"))
|
||||||
|
c_rows.append(f"""<tr>
|
||||||
|
<td>{r['img']}</td><td>{r['hair_name']}</td><td>{r['res']}</td>
|
||||||
|
<td>{r.get('swap_ms','?')}</td><td>{r.get('comfy_ms','?')}</td><td>{r.get('total_ms','?')}</td>
|
||||||
|
<td>{f'<img src="{src}" loading="lazy">' if src else '⚠'}</td></tr>""")
|
||||||
|
|
||||||
|
def bar_row(label, val, max_val, color, unit="ms"):
|
||||||
|
pct = max(1, val / max_val * 100) if max_val else 0
|
||||||
|
return f'<div class="step-row"><div class="step-name">{label}</div>' \
|
||||||
|
f'<div class="step-bar-wrap"><div class="step-bar {color}" style="width:{pct}%">{val}{unit}</div></div>' \
|
||||||
|
f'<div class="step-time">{val}{unit}</div></div>'
|
||||||
|
|
||||||
|
# B维度汇总条形图
|
||||||
|
b_max_swap = max(s["swap"] for s in b_summary)
|
||||||
|
b_bars = "".join(bar_row(s["label"], s["swap"], b_max_swap, "c-swap") for s in b_summary)
|
||||||
|
b_max_total = max(s["total"] for s in b_summary)
|
||||||
|
b_total_bars = "".join(bar_row(s["label"], s["total"], b_max_total, "c-total") for s in b_summary)
|
||||||
|
|
||||||
|
# C维度汇总条形图
|
||||||
|
c_max_comfy = max(s["comfy"] for s in c_summary)
|
||||||
|
c_bars = "".join(bar_row(s["label"], s["comfy"], c_max_comfy, "c-comfy") for s in c_summary)
|
||||||
|
c_max_total = max(s["total"] for s in c_summary)
|
||||||
|
c_total_bars = "".join(bar_row(s["label"], s["total"], c_max_total, "c-total") for s in c_summary)
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>swap步数 + 重绘分辨率 对比报告</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: -apple-system, "Segoe UI", sans-serif; background: #f5f5f5; padding: 16px; color: #333; }}
|
||||||
|
h1 {{ font-size: 20px; margin-bottom: 4px; }}
|
||||||
|
h2 {{ font-size: 16px; margin: 20px 0 10px; }}
|
||||||
|
.subtitle {{ color: #888; font-size: 12px; margin-bottom: 14px; }}
|
||||||
|
.card {{ background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 16px; overflow: hidden; }}
|
||||||
|
.card-header {{ font-weight: 700; font-size: 14px; padding: 12px 18px; border-bottom: 1px solid #f0f0f0; background: #fafafa; }}
|
||||||
|
.card-body {{ padding: 18px; }}
|
||||||
|
.summary-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }}
|
||||||
|
.step-row {{ display: flex; align-items: center; gap: 10px; margin-bottom: 8px; font-size: 13px; }}
|
||||||
|
.step-name {{ width: 100px; flex-shrink: 0; font-weight: 600; }}
|
||||||
|
.step-bar-wrap {{ flex: 1; background: #f3f4f6; border-radius: 4px; height: 24px; min-width: 200px; }}
|
||||||
|
.step-bar {{ height: 100%; border-radius: 4px; display: flex; align-items: center; padding-left: 8px; color: #fff; font-size: 11px; font-weight: 600; min-width: 2px; }}
|
||||||
|
.step-time {{ width: 70px; text-align: right; font-weight: 600; flex-shrink: 0; font-variant-numeric: tabular-nums; }}
|
||||||
|
.c-swap {{ background: #f59e0b; }} .c-comfy {{ background: #ef4444; }} .c-total {{ background: #2563eb; }}
|
||||||
|
table {{ border-collapse: collapse; width: 100%; font-size: 12px; }}
|
||||||
|
th, td {{ border: 1px solid #eee; padding: 5px 8px; text-align: center; }}
|
||||||
|
th {{ background: #f9fafb; font-weight: 600; position: sticky; top: 0; }}
|
||||||
|
td img {{ max-height: 100px; max-width: 80px; border-radius: 4px; }}
|
||||||
|
.scroll {{ max-height: 400px; overflow: auto; }}
|
||||||
|
.note {{ background: #fef3c7; border-radius: 8px; padding: 10px 14px; font-size: 12px; color: #92400e; margin-top: 10px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📊 swap步数 + 重绘分辨率 对比报告</h1>
|
||||||
|
<p class="subtitle">4图(asdf/qwer/girl2/girl5) × 2发型(波浪/心形) · 热数据(预热后取第2次) · 48/48成功 · 峰值20.6GB · 0 OOM</p>
|
||||||
|
|
||||||
|
<div class="note">💡 结论速览: B维度 steps 10→20 swap从3.0s→3.9s(每步省~90ms);C维度 res 640比896省3s(comfy 4.3s vs 7.3s),1024与896接近。</div>
|
||||||
|
|
||||||
|
<h2>B维度:swap步数对比(分辨率固定896)</h2>
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="card"><div class="card-header">swap 耗时(越低越快)</div><div class="card-body">{b_bars}</div></div>
|
||||||
|
<div class="card"><div class="card-header">总耗时(越低越快)</div><div class="card-body">{b_total_bars}</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>C维度:重绘分辨率对比(steps固定15)</h2>
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="card"><div class="card-header">ComfyUI重绘 耗时(越低越快)</div><div class="card-body">{c_bars}</div></div>
|
||||||
|
<div class="card"><div class="card-header">总耗时(越低越快)</div><div class="card-body">{c_total_bars}</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>B维度明细(每图每发型每步数)</h2>
|
||||||
|
<div class="card"><div class="scroll"><table>
|
||||||
|
<tr><th>图片</th><th>发型</th><th>steps</th><th>swap(ms)</th><th>comfy(ms)</th><th>总(ms)</th><th>结果</th></tr>
|
||||||
|
{"".join(b_rows)}
|
||||||
|
</table></div></div>
|
||||||
|
|
||||||
|
<h2>C维度明细(每图每发型每分辨率)</h2>
|
||||||
|
<div class="card"><div class="scroll"><table>
|
||||||
|
<tr><th>图片</th><th>发型</th><th>res</th><th>swap(ms)</th><th>comfy(ms)</th><th>总(ms)</th><th>结果</th></tr>
|
||||||
|
{"".join(c_rows)}
|
||||||
|
</table></div></div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
with open(HTML, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html)
|
||||||
|
print(f"✓ 报告: {HTML} ({HTML.stat().st_size // 1024} KB)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
按 `face_ext.obj` 的 UV 把发际线贴图渲染到额头(预览)。生发:黑贴图渲染遮罩 → 调本机 **ComfyUI 8182** 的
|
按 `face_ext.obj` 的 UV 把发际线贴图渲染到额头(预览)。生发:黑贴图渲染遮罩 → 调本机 **ComfyUI 8182** 的
|
||||||
`add_hair.json`(Flux-2) 出图。**关键坑**:obj 是重排序,需 `INDEX_MAP_468` 把 MP 序→OBJ 序。
|
`add_hair.json`(Flux-2) 出图。**关键坑**:obj 是重排序,需 `INDEX_MAP_468` 把 MP 序→OBJ 序。
|
||||||
返回 `results[].image_base64` + `grown_image_base64`。
|
返回 `results[].image_base64` + `grown_image_base64`。
|
||||||
- `hair_style` 映射:female 1=ellipse 2=flower 3=heart 4=straight 5=wave 6=bigflower 7=clasicalflower;male 1=ellipse 2=inverse_arc 3=m 4=straight 5=heart 6=Softpetal。female 1..5 走「换发型(change_hair)」+Flux-2 重绘管线;female 6/7 与 male 全部走原生发(ComfyUI add_hair)管线。
|
- `hair_style` 映射:female 1=ellipse 2=flower 3=heart 4=straight 5=wave;male 1=ellipse 2=inverse_arc 3=m 4=straight。
|
||||||
|
|
||||||
### 接口7 C端生发 v2 `/api/v1/hair/grow-v2`(worker)—— 接口2同款,add_hair2 工作流
|
### 接口7 C端生发 v2 `/api/v1/hair/grow-v2`(worker)—— 接口2同款,add_hair2 工作流
|
||||||
- **做什么**:与接口 2 完全一致(正面照 + `gender` + `hair_style` 逗号分隔多选 → N 组预览+生发图)。
|
- **做什么**:与接口 2 完全一致(正面照 + `gender` + `hair_style` 逗号分隔多选 → N 组预览+生发图)。
|
||||||
|
|||||||
@@ -265,7 +265,7 @@
|
|||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 |
|
| gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 |
|
||||||
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave, 6=bigflower, 7=clasicalflower;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight, 5=heart, 6=Softpetal。越界/非法返回 `1007` |
|
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界/非法返回 `1007` |
|
||||||
| beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) |
|
| beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) |
|
||||||
| use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
| use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
||||||
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
||||||
@@ -278,7 +278,7 @@
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| image_url | string | 发际线曲线**透明 PNG** URL(仅白色发际线曲线,透明底,**不含人物**,需前端叠加原图显示) |
|
| image_url | string | 发际线曲线**透明 PNG** URL(仅白色发际线曲线,透明底,**不含人物**,需前端叠加原图显示) |
|
||||||
| grown_image_url | string | **生发后图片** URL(ComfyUI/Flux「植发 3 个月」效果图,完整人像照片) |
|
| grown_image_url | string | **生发后图片** URL(ComfyUI/Flux「植发 3 个月」效果图,完整人像照片) |
|
||||||
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`/`bigflower`/`clasicalflower`(female),`ellipse`/`m`/`straight`/`inverse_arc`/`heart`/`Softpetal`(male) |
|
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`(female),`ellipse`/`m`/`straight`/`inverse_arc`(male) |
|
||||||
| order | int | 排序序号(当前阶段固定 `1..N`,按贴图顺序,暂不计算合适度) |
|
| order | int | 排序序号(当前阶段固定 `1..N`,按贴图顺序,暂不计算合适度) |
|
||||||
|
|
||||||
> ⚠️ 生发图由本机 ComfyUI(Flux-2,端口 8182)生成,**一次请求生成指定发型的 1 张、同步返回**。
|
> ⚠️ 生发图由本机 ComfyUI(Flux-2,端口 8182)生成,**一次请求生成指定发型的 1 张、同步返回**。
|
||||||
@@ -405,8 +405,8 @@
|
|||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| gender | string | **是** | 性别:`male` / `female`。决定发型集合(female 7 / male 6)。缺失/非法返回 `1004` |
|
| gender | string | **是** | 性别:`male` / `female`。决定发型集合(female 5 / male 4)。缺失/非法返回 `1004` |
|
||||||
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),决定返回哪些发际线类型。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave, 6=bigflower, 7=clasicalflower;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight, 5=heart, 6=Softpetal。缺失/越界/非法返回 `1007` |
|
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),决定返回哪些发际线类型。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight。缺失/越界/非法返回 `1007` |
|
||||||
| use_mask | bool | 否 | 生发是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
| use_mask | bool | 否 | 生发是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
||||||
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
||||||
| generate_grow_image | bool | 否 | 是否生成生发效果图(ComfyUI 生发,全流程最耗时),默认 `true`。传 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,可大幅降低耗时 |
|
| generate_grow_image | bool | 否 | 是否生成生发效果图(ComfyUI 生发,全流程最耗时),默认 `true`。传 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,可大幅降低耗时 |
|
||||||
@@ -427,7 +427,7 @@
|
|||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`/`bigflower`/`clasicalflower`(female),`ellipse`/`m`/`straight`/`inverse_arc`/`heart`/`Softpetal`(male) |
|
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`(female),`ellipse`/`m`/`straight`/`inverse_arc`(male) |
|
||||||
| image_middle_url | string | middle 档发际线曲线**透明 PNG** URL(仅曲线,透明底,**不含人物**,需叠加原图显示) |
|
| image_middle_url | string | middle 档发际线曲线**透明 PNG** URL(仅曲线,透明底,**不含人物**,需叠加原图显示) |
|
||||||
| image_high_url | string | high 档发际线曲线**透明 PNG** URL(同上,high 档曲线) |
|
| image_high_url | string | high 档发际线曲线**透明 PNG** URL(同上,high 档曲线) |
|
||||||
| image_low_url | string | low 档发际线曲线**透明 PNG** URL(同上,low 档曲线) |
|
| image_low_url | string | low 档发际线曲线**透明 PNG** URL(同上,low 档曲线) |
|
||||||
|
|||||||
@@ -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(9, round(s * 0.020)) # 字号上调一档
|
font_size = max(8, round(s * 0.017)) # 字体更小
|
||||||
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)) # 虚线更稠密(间隙<划线)
|
||||||
@@ -259,18 +259,6 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
draw = ImageDraw.Draw(canvas)
|
draw = ImageDraw.Draw(canvas)
|
||||||
font = _load_font(font_size)
|
font = _load_font(font_size)
|
||||||
|
|
||||||
# --- 2b. 每条横线在「中间线段」(两内眼角之间)中点画原点突出 ---
|
|
||||||
# 注意:原点不在整条线的中点 face_cx,而在被竖线切出的中间段(左内眼角↔右内眼角)
|
|
||||||
# 的正中,即脸的竖直中轴附近、两内眼角连线中点。
|
|
||||||
li_x = pts["left_inner"][0]
|
|
||||||
ri_x = pts["right_inner"][0]
|
|
||||||
dot_cx = (li_x + ri_x) / 2
|
|
||||||
dot_r = max(2, round(s * 0.0045)) # 原点半径,与 arrow_size 同档自适应
|
|
||||||
for cy in ys:
|
|
||||||
x0, y0 = dot_cx - dot_r, cy - dot_r
|
|
||||||
x1, y1 = dot_cx + dot_r, cy + dot_r
|
|
||||||
draw.ellipse((x0, y0, x1, y1), fill=LINE_COLOR)
|
|
||||||
|
|
||||||
# --- 3a. 横线右侧:线名(头顶/发际线/眉心/鼻翼下缘/下巴尖),文字纵向居中对齐到线 ---
|
# --- 3a. 横线右侧:线名(头顶/发际线/眉心/鼻翼下缘/下巴尖),文字纵向居中对齐到线 ---
|
||||||
name_x = fx1 + over + pad # 移到横线右端外侧一点(往右)
|
name_x = fx1 + over + pad # 移到横线右端外侧一点(往右)
|
||||||
for i, name in enumerate(order):
|
for i, name in enumerate(order):
|
||||||
|
|||||||
@@ -508,13 +508,14 @@ def _segment_hair(image_bgr, seg_model, landmarks, w, h):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _call_swap(image_bgr, hairline_id, is_hr, ext_mask_bool, denoising_strength,
|
def _call_swap(image_bgr, hairline_id, is_hr, ext_mask_bool, denoising_strength,
|
||||||
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0):
|
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0, webui_steps=None):
|
||||||
"""调 change_hair /api/swapHair/v1,返回与输入同分辨率同对齐的换发型结果(BGR)。
|
"""调 change_hair /api/swapHair/v1,返回与输入同分辨率同对齐的换发型结果(BGR)。
|
||||||
|
|
||||||
ext_mask_bool 非 None 时作为 ext_mask 传入(swap_mode=ext_mask)。
|
ext_mask_bool 非 None 时作为 ext_mask 传入(swap_mode=ext_mask)。
|
||||||
denoising_strength:webui img2img 重绘强度(越大生发越激进),透传给换发型。
|
denoising_strength:webui img2img 重绘强度(越大生发越激进),透传给换发型。
|
||||||
inpainting_fill / mask_blur / mask_dilate_scale:服务端重绘参数(透传给 change_hair,
|
inpainting_fill / mask_blur / mask_dilate_scale:服务端重绘参数(透传给 change_hair,
|
||||||
默认值=服务端原始硬编码值,未传时行为不变)。详见 change_hair 文档。
|
默认值=服务端原始硬编码值,未传时行为不变)。详见 change_hair 文档。
|
||||||
|
webui_steps:webui img2img 采样步数,None 用服务端默认(15)。
|
||||||
"""
|
"""
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -530,6 +531,8 @@ def _call_swap(image_bgr, hairline_id, is_hr, ext_mask_bool, denoising_strength,
|
|||||||
"mask_blur": int(mask_blur),
|
"mask_blur": int(mask_blur),
|
||||||
"mask_dilate_scale": float(mask_dilate_scale),
|
"mask_dilate_scale": float(mask_dilate_scale),
|
||||||
}
|
}
|
||||||
|
if webui_steps is not None:
|
||||||
|
payload["webui_steps"] = int(webui_steps)
|
||||||
if ext_mask_bool is not None:
|
if ext_mask_bool is not None:
|
||||||
mbuf = cv2.imencode(".png", (ext_mask_bool.astype(np.uint8)) * 255)[1]
|
mbuf = cv2.imencode(".png", (ext_mask_bool.astype(np.uint8)) * 255)[1]
|
||||||
payload["ext_mask"] = "data:image/png;base64," + base64.b64encode(mbuf.tobytes()).decode()
|
payload["ext_mask"] = "data:image/png;base64," + base64.b64encode(mbuf.tobytes()).decode()
|
||||||
@@ -855,7 +858,7 @@ def _grow_core(image_bgr, hairline_id, *, is_hr, seg_model, erode_cm, swap_mode,
|
|||||||
mb_levels, hairline_push_cm, hairline_edge, blend_method, color_match,
|
mb_levels, hairline_push_cm, hairline_edge, blend_method, color_match,
|
||||||
color_match_strength, mb_feather_px, transition_band_px,
|
color_match_strength, mb_feather_px, transition_band_px,
|
||||||
inpainting_fill, mask_blur, mask_dilate_scale, rid, render_viz=True,
|
inpainting_fill, mask_blur, mask_dilate_scale, rid, render_viz=True,
|
||||||
hair_mask=None):
|
hair_mask=None, webui_steps=None):
|
||||||
"""接口11 共享核心:遮罩(pushed)→生成→硬贴回→接缝融合,产出 ④ final。
|
"""接口11 共享核心:遮罩(pushed)→生成→硬贴回→接缝融合,产出 ④ final。
|
||||||
|
|
||||||
不做任何重绘。返回中间产物 dict(供接口11 构造响应、接口12 取 final+重绘带用):
|
不做任何重绘。返回中间产物 dict(供接口11 构造响应、接口12 取 final+重绘带用):
|
||||||
@@ -897,7 +900,7 @@ def _grow_core(image_bgr, hairline_id, *, is_hr, seg_model, erode_cm, swap_mode,
|
|||||||
ext_mask = mask_bool if swap_mode == "ext_mask" else None
|
ext_mask = mask_bool if swap_mode == "ext_mask" else None
|
||||||
swap_result = _call_swap(image_bgr, hairline_id, is_hr, ext_mask, denoising_strength,
|
swap_result = _call_swap(image_bgr, hairline_id, is_hr, ext_mask, denoising_strength,
|
||||||
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
|
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
|
||||||
mask_dilate_scale=mask_dilate_scale)
|
mask_dilate_scale=mask_dilate_scale, webui_steps=webui_steps)
|
||||||
t_swap = time.time() - t0
|
t_swap = time.time() - t0
|
||||||
|
|
||||||
# 步骤3:严格按遮罩硬贴回(无融合,用于对比)
|
# 步骤3:严格按遮罩硬贴回(无融合,用于对比)
|
||||||
@@ -935,7 +938,7 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
|
|||||||
color_match_strength=1.0, mb_feather_px=1,
|
color_match_strength=1.0, mb_feather_px=1,
|
||||||
transition_band_px=-1,
|
transition_band_px=-1,
|
||||||
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0,
|
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0,
|
||||||
rid=None):
|
rid=None, webui_steps=None):
|
||||||
"""接口11 完整管线(**不含重绘**,重绘见接口12 generate_hairline_redraw)。
|
"""接口11 完整管线(**不含重绘**,重绘见接口12 generate_hairline_redraw)。
|
||||||
返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。
|
返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。
|
||||||
|
|
||||||
@@ -957,7 +960,7 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
|
|||||||
color_match=color_match, color_match_strength=color_match_strength,
|
color_match=color_match, color_match_strength=color_match_strength,
|
||||||
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
|
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
|
||||||
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
|
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
|
||||||
mask_dilate_scale=mask_dilate_scale, rid=rid)
|
mask_dilate_scale=mask_dilate_scale, rid=rid, webui_steps=webui_steps)
|
||||||
mask_viz = core["mask_viz"]
|
mask_viz = core["mask_viz"]
|
||||||
alpha = core["alpha"]
|
alpha = core["alpha"]
|
||||||
w, h = core["w"], core["h"]
|
w, h = core["w"], core["h"]
|
||||||
@@ -1035,7 +1038,7 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg
|
|||||||
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0,
|
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0,
|
||||||
comfyui_prompt=None, beauty_alpha=0.6,
|
comfyui_prompt=None, beauty_alpha=0.6,
|
||||||
band_lo_mult=0.5, band_hi_mult=1.5, rid=None,
|
band_lo_mult=0.5, band_hi_mult=1.5, rid=None,
|
||||||
hair_mask=None):
|
hair_mask=None, webui_steps=None):
|
||||||
"""接口12 发际线带重绘。内部先跑接口11 核心拿到 ④ final,再取 ⑤-① 发际线重绘带
|
"""接口12 发际线带重绘。内部先跑接口11 核心拿到 ④ final,再取 ⑤-① 发际线重绘带
|
||||||
(外推↔内推之间、经 baseline 截断只留上部)作遮罩。
|
(外推↔内推之间、经 baseline 截断只留上部)作遮罩。
|
||||||
|
|
||||||
@@ -1064,7 +1067,7 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg
|
|||||||
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
|
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
|
||||||
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
|
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
|
||||||
mask_dilate_scale=mask_dilate_scale, rid=rid, render_viz=False,
|
mask_dilate_scale=mask_dilate_scale, rid=rid, render_viz=False,
|
||||||
hair_mask=hair_mask)
|
hair_mask=hair_mask, webui_steps=webui_steps)
|
||||||
final = core["final"]
|
final = core["final"]
|
||||||
mask_viz = core["mask_viz"]
|
mask_viz = core["mask_viz"]
|
||||||
w, h = core["w"], core["h"]
|
w, h = core["w"], core["h"]
|
||||||
|
|||||||
@@ -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, NOSE_BOTTOM, CHIN_TIP,
|
GLABELLA_9, GLABELLA_151, 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,8 +24,10 @@ _TOP_RATIO = 0.22 / 0.28 # 顶庭 ÷ 中庭(≈ 0.786)
|
|||||||
|
|
||||||
|
|
||||||
def _brow_center(lm, w, h):
|
def _brow_center(lm, w, h):
|
||||||
"""眉心 = 索引 9(眉间上点)。"""
|
"""眉心 = 索引 9 / 151 中点。"""
|
||||||
return normalized_to_pixel(lm[GLABELLA_9], w, h)
|
g9 = 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):
|
||||||
|
|||||||
@@ -32,18 +32,28 @@ _SEED_NODE = "6" # RandomNoise
|
|||||||
_PROMPT_NODE = "60" # JjkText:提示词
|
_PROMPT_NODE = "60" # JjkText:提示词
|
||||||
_UNET_NODE = "16" # UNETLoader / UnetLoaderGGUF:Flux 模型加载
|
_UNET_NODE = "16" # UNETLoader / UnetLoaderGGUF:Flux 模型加载
|
||||||
_CLIP_NODE = "61" # CLIPLoader:qwen 文本编码器
|
_CLIP_NODE = "61" # CLIPLoader:qwen 文本编码器
|
||||||
|
_VAE_NODE = "3" # VAELoader
|
||||||
|
|
||||||
# Flux 模型 → 配套文本编码器映射。切换 unet 时自动同步编码器,避免维度不匹配。
|
# Flux 模型 → 配套文本编码器映射。切换 unet 时自动同步编码器,避免维度不匹配。
|
||||||
# 规则:4b 系列配 qwen_3_4b,9b 系列(fp8/GGUF)配 qwen_3_8b_fp8mixed。
|
|
||||||
def _clip_for_unet(unet_name: str) -> str | None:
|
def _clip_for_unet(unet_name: str) -> str | None:
|
||||||
"""根据 unet 文件名推断配套的文本编码器文件名;无法推断返回 None。"""
|
"""根据 unet 文件名推断配套的文本编码器文件名;无法推断返回 None。"""
|
||||||
low = unet_name.lower()
|
low = unet_name.lower()
|
||||||
if "4b" in low and "9b" not in low:
|
if "9b" in low: # Flux.2 9B 系列
|
||||||
return "qwen_3_4b.safetensors"
|
|
||||||
if "9b" in low:
|
|
||||||
return "qwen_3_8b_fp8mixed.safetensors"
|
return "qwen_3_8b_fp8mixed.safetensors"
|
||||||
|
if "z-image" in low: # Z-Image-Turbo 用 4B 编码器
|
||||||
|
return "qwen_3_4b.safetensors"
|
||||||
|
if "4b" in low: # Flux.2 4B
|
||||||
|
return "qwen_3_4b.safetensors"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Flux 模型 → 配套 VAE 映射。Z-Image 用 ae.safetensors,Flux.2 系列用 flux2-vae。
|
||||||
|
def _vae_for_unet(unet_name: str) -> str | None:
|
||||||
|
"""根据 unet 文件名推断配套 VAE 文件名;无法推断返回 None(保持工作流原值)。"""
|
||||||
|
low = unet_name.lower()
|
||||||
|
if "z-image" in low:
|
||||||
|
return "ae.safetensors"
|
||||||
|
return None # Flux.2 系列 vae 在工作流里已正确配置,不覆盖
|
||||||
|
|
||||||
_wf_cache: dict[str, dict] = {} # path → workflow JSON
|
_wf_cache: dict[str, dict] = {} # path → workflow JSON
|
||||||
_wf_output_node: dict[str, str] = {} # path → SaveImage 节点 ID
|
_wf_output_node: dict[str, str] = {} # path → SaveImage 节点 ID
|
||||||
|
|
||||||
@@ -153,6 +163,11 @@ def run(rgba_png_bytes: bytes, timeout: float = COMFY_TIMEOUT, prompt: str = Non
|
|||||||
clip_name = _clip_for_unet(unet_name)
|
clip_name = _clip_for_unet(unet_name)
|
||||||
if clip_node is not None and clip_name is not None:
|
if clip_node is not None and clip_name is not None:
|
||||||
clip_node["inputs"]["clip_name"] = clip_name
|
clip_node["inputs"]["clip_name"] = clip_name
|
||||||
|
# 同步切换 VAE(Z-Image 用 ae.safetensors,Flux.2 保持 flux2-vae)
|
||||||
|
vae_node = wf.get(_VAE_NODE)
|
||||||
|
vae_name = _vae_for_unet(unet_name)
|
||||||
|
if vae_node is not None and vae_name is not None:
|
||||||
|
vae_node["inputs"]["vae_name"] = vae_name
|
||||||
|
|
||||||
# 诊断:落盘实际提交的工作流 + 输入图,便于和手动 ComfyUI 跑的对比
|
# 诊断:落盘实际提交的工作流 + 输入图,便于和手动 ComfyUI 跑的对比
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ import logging
|
|||||||
logger = logging.getLogger("hair.worker")
|
logger = logging.getLogger("hair.worker")
|
||||||
|
|
||||||
# 接口2 女性发型 key → change_hair hair_id(chang_*)映射:换发型+Flux-2 整帧重绘用。
|
# 接口2 女性发型 key → change_hair hair_id(chang_*)映射:换发型+Flux-2 整帧重绘用。
|
||||||
# 与接口12 final 的 5 型一一对应。female 6/7(bigflower/clasicalflower)无对应 LoRA,
|
# 与接口12 final 的 5 型一一对应。
|
||||||
# 走与男性一致的原生生发(ComfyUI add_hair)管线,故不在本表。
|
|
||||||
_FEMALE_KEY_TO_CHANG = {
|
_FEMALE_KEY_TO_CHANG = {
|
||||||
"ellipse": "chang_tuoyuan", # 椭圆
|
"ellipse": "chang_tuoyuan", # 椭圆
|
||||||
"flower": "chang_huaban", # 花瓣
|
"flower": "chang_huaban", # 花瓣
|
||||||
@@ -39,16 +38,6 @@ _FEMALE_KEY_TO_CHANG = {
|
|||||||
"wave": "chang_bolang", # 波浪
|
"wave": "chang_bolang", # 波浪
|
||||||
}
|
}
|
||||||
|
|
||||||
# 发际线贴图显式顺序表:决定 hair_style 序号(1-indexed)。
|
|
||||||
# 不再依赖文件名字母序——字母序会因新增/重命名文件而错位,破坏现有前端/客户端取值。
|
|
||||||
# key 须与 _gender_key 派生结果一致(已去空格):如 "inverse_arc"(源 man_ inverse_arc.png)、
|
|
||||||
# "Softpetal"(源 man_Soft petal.png,大写 S 保留)。表外未知 key 兜底排到末尾。
|
|
||||||
_HAIRSTYLE_ORDER = {
|
|
||||||
"female": ["ellipse", "flower", "heart", "straight", "wave",
|
|
||||||
"bigflower", "clasicalflower"], # 1..7
|
|
||||||
"male": ["ellipse", "inverse_arc", "m", "straight", "heart", "Softpetal"], # 1..6
|
|
||||||
}
|
|
||||||
|
|
||||||
_REPO = os.path.dirname(os.path.dirname(__file__))
|
_REPO = os.path.dirname(os.path.dirname(__file__))
|
||||||
_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture")
|
_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture")
|
||||||
_BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
_BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
||||||
@@ -60,14 +49,13 @@ _BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
|||||||
_REDRAW_PROMPT = os.getenv("REDRAW_PROMPT", "填充遮罩区域的头发")
|
_REDRAW_PROMPT = os.getenv("REDRAW_PROMPT", "填充遮罩区域的头发")
|
||||||
|
|
||||||
# 接口2 女重绘整条管线(swapHair + ComfyUI)送模型前限边。真实照片常达 1257x1495:
|
# 接口2 女重绘整条管线(swapHair + ComfyUI)送模型前限边。真实照片常达 1257x1495:
|
||||||
# 全分辨率 ComfyUI 重绘要 13~21s 且激活显存把模型挤出。
|
# 全分辨率 ComfyUI 重绘要 13~21s 且激活显存把模型挤出。女性路径含 swapHair(SD WebUI ~5.3s
|
||||||
# 策略:输入图长边 > REDRAW_MAX_SIDE 才等比缩到该长边;≤ 时原图分辨率直送(不放大)。
|
# 固定地板) + ComfyUI 两段串行。1024 档画质更好但部分大图会踩 12s 线,
|
||||||
# 默认 1024:大于 1024 的图压到 1024(画质/速度均衡),≤1024 的小图保持原分辨率重绘。
|
# 默认压到 896 兜底(ComfyUI ~4s,女性总耗时 9~11s);追画质可设 REDRAW_MAX_SIDE=1024。
|
||||||
# 可用 REDRAW_MAX_SIDE 覆盖;0=永不缩图(原图直送)。
|
_REDRAW_MAX_SIDE = int(os.getenv("REDRAW_MAX_SIDE", "896"))
|
||||||
_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, prompt=None):
|
||||||
"""直接调 ComfyUI 重绘(替代原 local_test HTTP 服务)。
|
"""直接调 ComfyUI 重绘(替代原 local_test HTTP 服务)。
|
||||||
|
|
||||||
传 final 图 + 纯红遮罩 PNG,返回重绘后的 PNG bytes。
|
传 final 图 + 纯红遮罩 PNG,返回重绘后的 PNG bytes。
|
||||||
@@ -75,6 +63,7 @@ def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0,
|
|||||||
|
|
||||||
max_side:送 ComfyUI 前长边压到多少像素,None 用全局默认 _REDRAW_MAX_SIDE。
|
max_side:送 ComfyUI 前长边压到多少像素,None 用全局默认 _REDRAW_MAX_SIDE。
|
||||||
unet_name:非 None 时切换 Flux 模型,None 用工作流内置默认。
|
unet_name:非 None 时切换 Flux 模型,None 用工作流内置默认。
|
||||||
|
prompt:None 用默认 _REDRAW_PROMPT,否则用传入的提示词。
|
||||||
"""
|
"""
|
||||||
from .redraw import run_redraw
|
from .redraw import run_redraw
|
||||||
eff_side = _REDRAW_MAX_SIDE if max_side is None else max_side
|
eff_side = _REDRAW_MAX_SIDE if max_side is None else max_side
|
||||||
@@ -96,7 +85,8 @@ def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0,
|
|||||||
orig_w, orig_h, nw, nh, eff_side)
|
orig_w, orig_h, nw, nh, eff_side)
|
||||||
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前,避免排在接口3/5 的批量任务后面
|
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前,避免排在接口3/5 的批量任务后面
|
||||||
out = run_redraw(image_png_bytes, mask_png_bytes, timeout=timeout,
|
out = run_redraw(image_png_bytes, mask_png_bytes, timeout=timeout,
|
||||||
prompt=_REDRAW_PROMPT, front=True, unet_name=unet_name)
|
prompt=prompt if prompt is not None else _REDRAW_PROMPT,
|
||||||
|
front=True, unet_name=unet_name)
|
||||||
if scale < 1.0 and out:
|
if scale < 1.0 and out:
|
||||||
out = _upscale_png_to(out, orig_w, orig_h)
|
out = _upscale_png_to(out, orig_w, orig_h)
|
||||||
return out
|
return out
|
||||||
@@ -140,13 +130,10 @@ def _gender_key(stem: str):
|
|||||||
|
|
||||||
|
|
||||||
def get_texture_map(level: str = "middle") -> dict:
|
def get_texture_map(level: str = "middle") -> dict:
|
||||||
"""扫描指定档位贴图目录建 {gender: [(key, path)]},按显式顺序表排序、按档位缓存。
|
"""扫描指定档位贴图目录建 {gender: [(key, path)]},按 key 排序、按档位缓存。
|
||||||
|
|
||||||
level:middle(默认) / high / low,分别对应 hairline_texture[/_high|/_low]。
|
level:middle(默认) / high / low,分别对应 hairline_texture[/_high|/_low]。
|
||||||
文件名规范化去空格(如 `man_ inverse_arc.png` → key `inverse_arc`)。
|
文件名规范化去空格(如 `man_ inverse_arc.png` → key `inverse_arc`)。
|
||||||
|
|
||||||
排序依据 _HAIRSTYLE_ORDER:表内 key 按表序、表外未知 key 兜底排到末尾(再按字母序),
|
|
||||||
保证新增/重命名文件不会打乱现有 hair_style 序号。
|
|
||||||
"""
|
"""
|
||||||
if level not in _TEXTURE_DIRS:
|
if level not in _TEXTURE_DIRS:
|
||||||
raise ValueError(f"hairline_level 必须是 middle/high/low,收到 {level!r}")
|
raise ValueError(f"hairline_level 必须是 middle/high/low,收到 {level!r}")
|
||||||
@@ -160,9 +147,7 @@ def get_texture_map(level: str = "middle") -> dict:
|
|||||||
if gender:
|
if gender:
|
||||||
mapping[gender].append((key, path))
|
mapping[gender].append((key, path))
|
||||||
for g in mapping:
|
for g in mapping:
|
||||||
order = _HAIRSTYLE_ORDER.get(g, [])
|
mapping[g].sort(key=lambda kp: kp[0])
|
||||||
idx = {k: i for i, k in enumerate(order)}
|
|
||||||
mapping[g].sort(key=lambda kp: (idx.get(kp[0], len(idx)), kp[0]))
|
|
||||||
_texture_maps[level] = mapping
|
_texture_maps[level] = mapping
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
@@ -226,7 +211,7 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
|
|||||||
unet_name: str | None = None):
|
unet_name: str | None = None):
|
||||||
"""指定发际线类型:发际线透明叠图(白线 RGBA) + 生发图(ComfyUI)。
|
"""指定发际线类型:发际线透明叠图(白线 RGBA) + 生发图(ComfyUI)。
|
||||||
|
|
||||||
hair_styles(1-indexed 列表):指定生成哪几张发际线(按贴图排序)。female: 1..7,male: 1..6。
|
hair_styles(1-indexed 列表):指定生成哪几张发际线(按贴图排序)。female: 1..5,male: 1..4。
|
||||||
为 None 时生成全部(兼容旧调用)。
|
为 None 时生成全部(兼容旧调用)。
|
||||||
use_mask(默认 True):是否启用 inpaint 遮罩,用于测试对比(同接口3)。
|
use_mask(默认 True):是否启用 inpaint 遮罩,用于测试对比(同接口3)。
|
||||||
False 时用**干净原图 + 空遮罩**送 ComfyUI(不烧黑色模板线)。
|
False 时用**干净原图 + 空遮罩**送 ComfyUI(不烧黑色模板线)。
|
||||||
@@ -300,20 +285,15 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
|
|||||||
def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | None,
|
def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | None,
|
||||||
redraw_defaults: dict,
|
redraw_defaults: dict,
|
||||||
redraw_max_side: int | None = None,
|
redraw_max_side: int | None = None,
|
||||||
unet_name: str | None = None,
|
unet_name: str | None = None):
|
||||||
prompt: str | None = None):
|
"""接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 换发型重绘图。
|
||||||
"""接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 生发图。
|
|
||||||
|
|
||||||
生发图来源按发型分两路:
|
grown 图来源(新流程):对每个选中发型把 female key 映射到 change_hair 的 chang_* hair_id,
|
||||||
- 换发型(chang_* 表内,1..5):female key → change_hair 的 chang_* hair_id,调
|
调 face_analysis.hairline_grow.generate_hairline_redraw(= 接口12 final 管线,参数用
|
||||||
face_analysis.hairline_grow.generate_hairline_redraw(= 接口12 final 管线,参数用
|
|
||||||
redraw_defaults)拿到 ④ final(接缝融合基底)+ ⑤-② 纯红遮罩 PNG,再**后端直接调
|
redraw_defaults)拿到 ④ final(接缝融合基底)+ ⑤-② 纯红遮罩 PNG,再**后端直接调
|
||||||
ComfyUI**(0716add-hair-api.json 工作流)完成发际线带重绘。
|
ComfyUI**(0716add-hair-api.json 工作流)完成发际线带重绘,重绘结果作为生发图。
|
||||||
- 原生生发(表外,6/7 bigflower/clasicalflower 等):无对应 change_hair LoRA,改走与男性
|
|
||||||
一致的原生生发(ComfyUI add_hair),由 _grow_native_one 完成(黑模板 + inpaint 遮罩)。
|
|
||||||
|
|
||||||
overlay 仍是发际线曲线透明层(与 generate_grow_results 完全一致)。
|
overlay 仍是发际线曲线透明层(与 generate_grow_results 完全一致)。
|
||||||
prompt 仅用于原生生发分支(换发型分支的提示词由 redraw 流程内部固定)。
|
|
||||||
Returns: list[dict] {"hairline_type","order","overlay","grown_png"(jpg bytes 或 None)};
|
Returns: list[dict] {"hairline_type","order","overlay","grown_png"(jpg bytes 或 None)};
|
||||||
无人脸返回 None。单个发型换发型/重绘失败时 grown_png=None,不抛异常。
|
无人脸返回 None。单个发型换发型/重绘失败时 grown_png=None,不抛异常。
|
||||||
"""
|
"""
|
||||||
@@ -358,10 +338,7 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
|
|||||||
grown_png = None
|
grown_png = None
|
||||||
chang_id = _FEMALE_KEY_TO_CHANG.get(key)
|
chang_id = _FEMALE_KEY_TO_CHANG.get(key)
|
||||||
if chang_id is None:
|
if chang_id is None:
|
||||||
# 无对应 change_hair LoRA(如 bigflower/clasicalflower)→ 走与男性一致的原生生发
|
logger.warning("接口2 换发型:female key=%s 无对应 chang_id,跳过生发图", key)
|
||||||
logger.info("接口2 女 key=%s 无 chang_id,走原生生发(ComfyUI add_hair)", key)
|
|
||||||
grown_png = _grow_native_one(image_bgr, ctx, white_path,
|
|
||||||
prompt=prompt, unet_name=unet_name)
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
import time as _t
|
import time as _t
|
||||||
@@ -432,51 +409,17 @@ def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _grow_native_one(image_bgr: np.ndarray, ctx: dict, white_path: str,
|
|
||||||
prompt: str | None = None, unet_name: str | None = None):
|
|
||||||
"""对单个发际线做原生生发(ComfyUI add_hair),与男性 generate_grow_results 一致。
|
|
||||||
|
|
||||||
接口2 女性新发型(bigflower/clasicalflower 等)无对应 change_hair LoRA,改走此路径:
|
|
||||||
黑模板 → build_inpaint_mask → 限边(_prep_comfy_input)→ comfyui.run(front=True)。
|
|
||||||
失败返回 None,不抛异常。结果按限边前原图尺寸放大回原尺寸(仅展示对齐)。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
h, w = image_bgr.shape[:2]
|
|
||||||
black = load_texture_rgba(_black_texture_path(white_path))
|
|
||||||
marked, mask = build_inpaint_mask(
|
|
||||||
image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black)
|
|
||||||
m_s, msk_s, gsc = _prep_comfy_input(marked, mask)
|
|
||||||
buf = io.BytesIO()
|
|
||||||
compose_comfy_rgba(m_s, msk_s).save(buf, format="PNG", compress_level=1)
|
|
||||||
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
|
||||||
grown_png = comfyui.run(buf.getvalue(), prompt=prompt, front=True, unet_name=unet_name)
|
|
||||||
if gsc < 1.0 and grown_png:
|
|
||||||
grown_png = _upscale_png_to(grown_png, w, h)
|
|
||||||
return grown_png
|
|
||||||
except Exception as e: # noqa: BLE001 单张失败不拖垮整请求
|
|
||||||
logger.warning("接口2 女原生生发图失败:%s", e)
|
|
||||||
return 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 档。
|
||||||
生发机制(同接口2,按性别分流):
|
use_mask/prompt:同接口2 的生发参数。
|
||||||
- female:generate_grow_results_swap(swapHair + Flux-2 整帧重绘)
|
generate_grow_image(默认 True):是否生成生发图(ComfyUI,最耗时)。False 时跳过生发,
|
||||||
- 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。
|
||||||
@@ -498,27 +441,11 @@ 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}
|
||||||
|
|
||||||
# 生发图(同接口2,按性别分流):一次性算出所有选中发型的生发图,按 order 对应回叠图。
|
# use_mask=False:干净原图+空遮罩与贴图无关,只跑一次 ComfyUI,选中项复用
|
||||||
# female→generate_grow_results_swap(swapHair+Flux-2 整帧重绘);
|
# generate_grow_image=False:完全跳过生发(最耗时),grown_png 恒为 None
|
||||||
# male→generate_grow_results(ComfyUI add_hair inpaint)。
|
shared_grown = None
|
||||||
# generate_grow_image=False 时跳过,grown_by_order 为空 dict(各发型 grown_png 恒 None)。
|
if generate_grow_image and not use_mask:
|
||||||
grown_by_order: dict[int, bytes | None] = {}
|
shared_grown = _grow_from_texture(image_bgr, ctx, None, use_mask=False, prompt=prompt)
|
||||||
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。"""
|
||||||
@@ -531,13 +458,18 @@ 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)
|
||||||
# 生发图:从按性别算好的结果里按 order 取(generate_grow_image=False 时缺省 None)
|
# 生发:固定 middle 黑模板(generate_grow_image=False 时跳过,恒 None)
|
||||||
grown_png = grown_by_order.get(s)
|
if not generate_grow_image:
|
||||||
|
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)发际线中点
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -1,75 +0,0 @@
|
|||||||
=== 接口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
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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,
|
|
||||||
|
@@ -1,562 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
#!/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}")
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[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
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/ubuntu/hair/image/compare_test/out
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/ubuntu/hair/image/res_test/out
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/ubuntu/hair/image/wave_test/out
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
#!/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}")
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[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
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
#!/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}")
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[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
|
|
||||||
@@ -48,7 +48,7 @@ cd /home/ubuntu/hair/local_test
|
|||||||
|--------|------|------|------|
|
|--------|------|------|------|
|
||||||
| image | File | 是 | 人物图片(支持 jpg, png 等常见格式) |
|
| image | File | 是 | 人物图片(支持 jpg, png 等常见格式) |
|
||||||
| mask | File | 是 | 遮罩图片(支持 jpg, png,遮罩区域可用红色/白色/alpha 通道标识) |
|
| mask | File | 是 | 遮罩图片(支持 jpg, png,遮罩区域可用红色/白色/alpha 通道标识) |
|
||||||
| prompt | String | 否 | 提示词,默认值:"填充遮罩区域的头发,皮肤加一点磨皮" |
|
| prompt | String | 否 | 提示词,默认值:"填充遮罩区域的头发" |
|
||||||
|
|
||||||
#### 遮罩图片格式说明
|
#### 遮罩图片格式说明
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ cd /home/ubuntu/hair/local_test
|
|||||||
curl -X POST http://127.0.0.1:8899/api/generate \
|
curl -X POST http://127.0.0.1:8899/api/generate \
|
||||||
-F "image=@/path/to/person.jpg" \
|
-F "image=@/path/to/person.jpg" \
|
||||||
-F "mask=@/path/to/mask.png" \
|
-F "mask=@/path/to/mask.png" \
|
||||||
-F "prompt=填充遮罩区域的头发,皮肤加一点磨皮" \
|
-F "prompt=填充遮罩区域的头发" \
|
||||||
--output result.png
|
--output result.png
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ files = {
|
|||||||
"mask": open("mask.png", "rb"),
|
"mask": open("mask.png", "rb"),
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
"prompt": "填充遮罩区域的头发,皮肤加一点磨皮"
|
"prompt": "填充遮罩区域的头发"
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = requests.post(url, files=files, data=data, timeout=600)
|
resp = requests.post(url, files=files, data=data, timeout=600)
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""重新生成 bench3/4/5/7 报告,在最左边加原图列。"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPOS_ROOT = Path("/home/ubuntu/hair")
|
||||||
|
ORIG_SRC = {"asdf": "bench/orig/asdf.jpg", "qwer": "bench/orig/qwer.jpg",
|
||||||
|
"girl2": "bench/orig/girl2.jpg", "girl5": "bench/orig/girl5.jpg"}
|
||||||
|
|
||||||
|
# bench编号 -> (输出目录, 部署HTML名, 报告标题后缀)
|
||||||
|
BENCHES = [
|
||||||
|
(3, "美颜(磨皮+美颜)"),
|
||||||
|
(4, "磨皮"),
|
||||||
|
(5, "美白"),
|
||||||
|
(7, "纯生发"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def img_src(path, bench_num):
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
return None
|
||||||
|
return f"bench{bench_num}/" + os.path.basename(path)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_report(bench_num, title_suffix):
|
||||||
|
bench_dir = REPOS_ROOT / f"benchmark_out/bench{bench_num}"
|
||||||
|
results = bench_dir / "results.json"
|
||||||
|
if not results.exists():
|
||||||
|
print(f" 跳过 bench{bench_num}: results.json 不存在")
|
||||||
|
return
|
||||||
|
d = json.load(open(results, encoding="utf-8"))
|
||||||
|
titles = d["res_titles"]
|
||||||
|
rows = d["rows"]
|
||||||
|
prompt = d.get("prompt", "")
|
||||||
|
|
||||||
|
col_stats = defaultdict(lambda: {"total": []})
|
||||||
|
for r in rows:
|
||||||
|
for c in r["cells"]:
|
||||||
|
if c.get("ok"):
|
||||||
|
col_stats[c["res_title"]]["total"].append(c["total_ms"])
|
||||||
|
|
||||||
|
# 表头:原图列 + 分辨率列
|
||||||
|
headers = ['<th class="col-label">原图</th>']
|
||||||
|
for t in titles:
|
||||||
|
s = col_stats.get(t)
|
||||||
|
avg = sum(s["total"]) // len(s["total"]) if s and s["total"] else 0
|
||||||
|
headers.append(f'<th class="col-label"><div class="col-title">{t}</div>'
|
||||||
|
f'<div class="col-stat">均{avg/1000:.1f}s</div></th>')
|
||||||
|
|
||||||
|
body_rows = []
|
||||||
|
for r in rows:
|
||||||
|
orig_src = ORIG_SRC.get(r["img"])
|
||||||
|
label = f'<div class="row-label">{r["img"]}<br><b>{r["hair_name"]}</b></div>'
|
||||||
|
# 原图列:显示输入原图
|
||||||
|
orig_cell = (f'<td class="cell-orig"><div class="row-label-cell">{label}</div>'
|
||||||
|
f'<img class="orig-img" src="{orig_src}"></td>')
|
||||||
|
cells = [orig_cell]
|
||||||
|
for c in r["cells"]:
|
||||||
|
src = img_src(c.get("grown_path"), bench_num) if c.get("ok") else None
|
||||||
|
if src:
|
||||||
|
t = c.get("total_ms", 0)
|
||||||
|
cells.append(f'<td class="cell-result"><img class="result-img" src="{src}" loading="lazy">'
|
||||||
|
f'<div class="cell-time">{t/1000:.1f}s</div></td>')
|
||||||
|
else:
|
||||||
|
cells.append(f'<td class="cell-result"><div class="na">⚠</div></td>')
|
||||||
|
body_rows.append(f'<tr>{"".join(cells)}</tr>')
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>重绘分辨率对比({title_suffix})</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: -apple-system, sans-serif; background: #f5f5f5; padding: 16px; }}
|
||||||
|
h1 {{ font-size: 20px; margin-bottom: 4px; }}
|
||||||
|
.subtitle {{ color: #888; font-size: 12px; margin-bottom: 12px; }}
|
||||||
|
.legend {{ background: #fff; border-radius: 8px; padding: 10px 16px; margin-bottom: 12px; font-size: 12px; color: #555; }}
|
||||||
|
.scroll-wrap {{ overflow-x: auto; }}
|
||||||
|
table {{ border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,.06); }}
|
||||||
|
th, td {{ border: 1px solid #eee; padding: 6px; vertical-align: top; text-align: center; }}
|
||||||
|
th {{ background: #f9fafb; position: sticky; top: 0; }}
|
||||||
|
.col-label {{ min-width: 130px; max-width: 150px; }}
|
||||||
|
.col-title {{ font-size: 12px; font-weight: 700; color: #374151; }}
|
||||||
|
.col-stat {{ font-size: 10px; color: #9ca3af; margin-top: 2px; }}
|
||||||
|
.row-label {{ font-size: 11px; color: #6b7280; }}
|
||||||
|
.row-label b {{ color: #1f2937; }}
|
||||||
|
.row-label-cell {{ font-size: 11px; color: #6b7280; margin-bottom: 4px; }}
|
||||||
|
.row-label-cell b {{ color: #1f2937; font-size: 13px; }}
|
||||||
|
img {{ border-radius: 4px; max-width: 130px; max-height: 160px; object-fit: contain; background: #f3f4f6; }}
|
||||||
|
.orig-img {{ border: 2px solid #d1d5db; }}
|
||||||
|
.cell-time {{ font-size: 10px; color: #9ca3af; margin-top: 2px; }}
|
||||||
|
.na {{ color: #d1d5db; font-size: 12px; padding: 40px 10px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📊 重绘分辨率对比(提示词:{title_suffix})</h1>
|
||||||
|
<p class="subtitle">4图×5发型=20行 · 每行原图+4分辨率 · steps=15 · 提示词="{prompt}" · 80/80成功 · 0 OOM</p>
|
||||||
|
<div class="legend">最左列为输入原图。列标题下为平均总耗时。横向滚动查看。</div>
|
||||||
|
<div class="scroll-wrap">
|
||||||
|
<table>
|
||||||
|
<tr>{"".join(headers)}</tr>
|
||||||
|
{"".join(body_rows)}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
deploy = REPOS_ROOT / "static" / f"bench{bench_num}_report.html"
|
||||||
|
deploy.write_text(html, encoding="utf-8")
|
||||||
|
print(f" ✓ bench{bench_num} ({title_suffix}): {deploy.name}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("重新生成报告(加原图列):")
|
||||||
|
for num, suffix in BENCHES:
|
||||||
|
gen_report(num, suffix)
|
||||||
|
print("完成")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 170 KiB |