Files
hair/image/compare_test/gen_report.py
T
xsl 5101bb5f6b feat(接口2): 测试页暴露重绘分辨率选项 + 分辨率对比测试套件
- 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)
2026-07-26 22:06:27 +08:00

245 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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}")