- test_interface2.html: 新增「重绘分辨率」下拉(默认896/1024/768/640/不缩图0), 仅 female 生效,append redraw_max_side 字段;male 时自动隐藏 - 新增分辨率对比测试工具 (image/compare_test, image/res_test): - batch_test.py: 串行批量测试脚本,支持断点续跑 - gen_report.py: 生成自包含 HTML 对比报告(速度色阶+缩略图+点击放大) - 两轮测试结果: - v1: 4图×5发型×2档=40次 (compare_test) - v2: 5图×3发型×5档=75次 (res_test) - 结论: 大图(长边>1600)原图直送比896档慢~3倍; 中小图各档差异小 - .gitignore: 忽略测试结果图/原图副本/运行日志(out/, *.log, progress.json)
245 lines
11 KiB
Python
245 lines
11 KiB
Python
#!/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}")
|