audit full semantic extent and AOI visuals

This commit is contained in:
OpenAI Codex
2026-04-18 09:43:42 +08:00
parent 4f96d7be91
commit d50b127eae
9 changed files with 5113 additions and 1 deletions

View File

@@ -0,0 +1,225 @@
#!/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)
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]] = []
for variant in ("full", "semantic"):
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()