audit full semantic extent and AOI visuals
This commit is contained in:
225
navsea_full_aoi_visual_audit.py
Normal file
225
navsea_full_aoi_visual_audit.py
Normal 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()
|
||||
Reference in New Issue
Block a user