loupe / validate_page_measures.py
anxuanng's picture
Loupe research preview: audit code, read weights, benchmark reports
84c3565 verified
Raw History Blame Contribute Delete
3.25 kB
"""Compare what Loupe reads from a page's pixels with what the browser computed, block by block.
Pages are collected in truth mode (collect_page.cjs ... truth). The audit never reads those truth fields;
this script does, to report how far font size, line height and contrast are from the browser's values.
Usage: validate_page_measures.py <collected page directory> [more directories]
"""
import json
import re
import sys
from pathlib import Path
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
PACKAGE = Path(__file__).resolve().parent
sys.path.insert(0, str(PACKAGE))
import numpy
from PIL import Image
import block_measure
def colour(text, behind=None):
"""An rgb() or rgba() string as RGB; a translucent colour is composited over the colour behind it."""
numbers = [float(value) for value in re.findall(r"[\d.]+", text)]
alpha = numbers[3] if len(numbers) > 3 and behind is not None else 1.0
return [alpha * numbers[channel] + (1 - alpha) * (behind[channel] if behind else 0.0) for channel in range(3)]
def page_errors(directory, maps):
layout = json.loads((directory / "layout.json").read_text(encoding="utf-8"))
with Image.open(directory / "page.png") as image:
pixels = numpy.asarray(image.convert("RGB"))
errors = {"font_px": [], "line_height_px": [], "contrast": []}
for text in layout["texts"]:
truth = text["truth"]
measured = block_measure.measure_block(pixels, text["box"], maps)
errors["font_px"].append(abs(measured["font_px"] / truth["font_px"] - 1))
if measured["lines"] > 1 and truth["line_height_px"] is not None:
errors["line_height_px"].append(abs(measured["line_height_px"] / truth["line_height_px"] - 1))
behind = colour(truth["background"])
declared = block_measure.contrast_ratio(behind, colour(truth["color"], behind))
errors["contrast"].append(abs(measured["contrast"] / declared - 1))
return errors
def summary(values):
values = numpy.asarray(values)
return {"blocks": int(len(values)), "median_relative_error": float(numpy.median(values)),
"within_10_percent": float((values <= 0.10).mean()), "within_20_percent": float((values <= 0.20).mean())}
def main():
maps = block_measure.load_maps()
pooled = {"font_px": [], "line_height_px": [], "contrast": []}
report = {"pages": {}}
for argument in sys.argv[1:]:
directory = Path(argument)
errors = page_errors(directory, maps)
report["pages"][directory.name] = {kind: summary(values) for kind, values in errors.items() if values}
for kind, values in errors.items():
pooled[kind].extend(values)
report["pooled"] = {kind: summary(values) for kind, values in pooled.items() if values}
(PACKAGE / "page_measure_validation.json").write_text(json.dumps(report, indent=1), encoding="utf-8")
for name, entry in {**report["pages"], "POOLED": report["pooled"]}.items():
print(name, {kind: (value["blocks"], round(value["median_relative_error"], 3), round(value["within_10_percent"], 2))
for kind, value in entry.items()})
if __name__ == "__main__":
main()