Files
pbf/navsea_full_aoi_visual_audit.py
2026-04-18 16:48:13 +08:00

228 lines
8.4 KiB
Python
Raw Permalink 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
from __future__ import annotations
import argparse
import json
import subprocess
from pathlib import Path
from typing import Any
from urllib.parse import urlencode
from PIL import Image, ImageChops, ImageStat
DEFAULT_COMPARE_URL = "http://192.168.200.184/newpec/navsea-compare-full-audit.html"
DEFAULT_AOI_JSON = Path("/root/sourceserver/pbf/tasks/pbf/NavSea_Full_AOI_Visual_Audit_2026-04-18.json")
DEFAULT_OUTPUT_DIR = Path("/root/sourceserver/pbf/report/full_aoi_visual_audit_2026-04-18")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="对全国 full / semantic 做 AOI 截图 + 图像 diff 审计。")
parser.add_argument("--compare-url", default=DEFAULT_COMPARE_URL)
parser.add_argument("--aoi-json", type=Path, default=DEFAULT_AOI_JSON)
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
parser.add_argument("--width", type=int, default=1280)
parser.add_argument("--height", type=int, default=720)
parser.add_argument("--toolbar-height", type=int, default=120)
parser.add_argument("--bottom-trim", type=int, default=30)
parser.add_argument("--timeout-sec", type=int, default=120)
parser.add_argument("--virtual-time-budget-ms", type=int, default=30000)
parser.add_argument("--variant", action="append", choices=("full", "semantic"), help="只跑指定 variant可重复传入")
return parser.parse_args()
def run_browser_screenshot(
compare_url: str,
output_png: Path,
width: int,
height: int,
timeout_sec: int,
virtual_time_budget_ms: int,
) -> None:
cmd = [
"timeout",
f"{timeout_sec}s",
"google-chrome",
"--headless=new",
"--disable-gpu",
"--enable-unsafe-swiftshader",
"--no-sandbox",
f"--virtual-time-budget={virtual_time_budget_ms}",
f"--window-size={width},{height}",
f"--screenshot={output_png}",
compare_url,
]
subprocess.run(cmd, check=True, capture_output=True, text=True)
def compute_visual_metrics(left_img: Image.Image, right_img: Image.Image) -> dict[str, Any]:
diff = ImageChops.difference(left_img, right_img)
diff_rgb = diff.convert("RGB")
diff_gray = diff.convert("L")
stat = ImageStat.Stat(diff_rgb)
mean_abs = [round(v, 4) for v in stat.mean]
rms = [round(v, 4) for v in stat.rms]
bbox = diff_gray.getbbox()
nonzero_pixels = 0
if bbox:
mask = diff_gray.point(lambda value: 255 if value else 0)
histogram = mask.histogram()
nonzero_pixels = histogram[255] if len(histogram) > 255 else 0
total_pixels = left_img.size[0] * left_img.size[1]
changed_ratio = round(nonzero_pixels / total_pixels, 6) if total_pixels else 0.0
return {
"changed_pixels": nonzero_pixels,
"total_pixels": total_pixels,
"changed_ratio": changed_ratio,
"mean_abs_rgb": mean_abs,
"rms_rgb": rms,
"diff_bbox": list(bbox) if bbox else None,
"diff_image": diff,
}
def crop_compare_panes(full_png: Path, left_png: Path, right_png: Path, diff_png: Path, toolbar_height: int, bottom_trim: int) -> dict[str, Any]:
image = Image.open(full_png).convert("RGBA")
usable_top = toolbar_height
usable_bottom = max(usable_top + 1, image.height - bottom_trim)
mid_x = image.width // 2
left = image.crop((0, usable_top, mid_x, usable_bottom))
right = image.crop((mid_x, usable_top, image.width, usable_bottom))
left.save(left_png)
right.save(right_png)
metrics = compute_visual_metrics(left, right)
diff_image = metrics.pop("diff_image")
diff_image.convert("RGB").point(lambda value: min(255, value * 4)).save(diff_png)
metrics["left_crop"] = [0, usable_top, mid_x, usable_bottom]
metrics["right_crop"] = [mid_x, usable_top, image.width, usable_bottom]
return metrics
def build_case_url(compare_url: str, variant: str, center: list[float], zoom: int | float) -> str:
query = urlencode(
{
"variant": variant,
"audit": "1",
"center": f"{center[0]},{center[1]}",
"zoom": zoom,
}
)
separator = "&" if "?" in compare_url else "?"
return f"{compare_url}{separator}{query}"
def write_report(output_dir: Path, summary: dict[str, Any]) -> tuple[Path, Path]:
report_json = output_dir / "full_aoi_visual_audit.json"
report_md = output_dir / "full_aoi_visual_audit.md"
report_json.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
lines = [
"# 全国 AOI 截图对比审计",
"",
"## 总览",
"",
f"- compare page: `{summary['compare_url']}`",
f"- cases: `{len(summary['cases'])}`",
"",
"## 差异排序",
"",
]
for case in summary["cases"]:
lines.extend(
[
f"### {case['label']} · {case['variant']} · z{case['zoom']}",
"",
f"- changed ratio: `{case['changed_ratio']}`",
f"- changed pixels: `{case['changed_pixels']}` / `{case['total_pixels']}`",
f"- mean abs rgb: `{case['mean_abs_rgb']}`",
f"- compare url: `{case['compare_url']}`",
f"- left image: `{case['left_image']}`",
f"- right image: `{case['right_image']}`",
f"- diff image: `{case['diff_image']}`",
"",
]
)
report_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
return report_md, report_json
def main() -> None:
args = parse_args()
output_dir = args.output_dir
output_dir.mkdir(parents=True, exist_ok=True)
aois = json.loads(args.aoi_json.read_text(encoding="utf-8"))
cases: list[dict[str, Any]] = []
variants = args.variant or ["full", "semantic"]
for variant in variants:
for aoi in aois:
for zoom in aoi["zoom_levels"]:
case_id = f"{aoi['id']}_{variant}_z{zoom}"
case_dir = output_dir / case_id
case_dir.mkdir(parents=True, exist_ok=True)
compare_url = build_case_url(args.compare_url, variant, aoi["center"], zoom)
full_png = case_dir / "compare_full.png"
left_png = case_dir / "compare_left.png"
right_png = case_dir / "compare_right.png"
diff_png = case_dir / "compare_diff.png"
run_browser_screenshot(
compare_url=compare_url,
output_png=full_png,
width=args.width,
height=args.height,
timeout_sec=args.timeout_sec,
virtual_time_budget_ms=args.virtual_time_budget_ms,
)
metrics = crop_compare_panes(
full_png=full_png,
left_png=left_png,
right_png=right_png,
diff_png=diff_png,
toolbar_height=args.toolbar_height,
bottom_trim=args.bottom_trim,
)
cases.append(
{
"id": case_id,
"label": aoi["label"],
"variant": variant,
"zoom": zoom,
"center": aoi["center"],
"radius_nm": aoi["radius_nm"],
"compare_url": compare_url,
"changed_pixels": metrics["changed_pixels"],
"total_pixels": metrics["total_pixels"],
"changed_ratio": metrics["changed_ratio"],
"mean_abs_rgb": metrics["mean_abs_rgb"],
"rms_rgb": metrics["rms_rgb"],
"diff_bbox": metrics["diff_bbox"],
"left_image": str(left_png),
"right_image": str(right_png),
"diff_image": str(diff_png),
}
)
cases.sort(key=lambda item: item["changed_ratio"], reverse=True)
summary = {
"compare_url": args.compare_url,
"cases": cases,
}
report_md, report_json = write_report(output_dir, summary)
print(
json.dumps(
{
"report_md": str(report_md),
"report_json": str(report_json),
"top_cases": cases[:5],
},
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
main()