figure_compare

utils.figure_compare

Measure how a rendered figure differs in LAYOUT from a reference image.

A replication’s figure is meant to land on the published one’s layout: same aspect, same panel grid, panels in the same places at the same relative sizes. Judging that by eye does not scale and does not produce a number you can put in a report, so this module reduces both images to their panel geometry and reports the differences.

Panels are found by recursive XY-cut — the classic document-layout decomposition: project the ink onto each axis, split at runs of blank, recurse. It needs no knowledge of either figure’s provenance, which is the point: the reference is a bitmap from a PDF and ours comes from a mosaic spec, and they are compared on equal terms.

Classes

Name Description
Box A content block in fractional page coordinates (0-1, origin top-left).
Pane One side of an A/B row.

Box

utils.figure_compare.Box(x0, y0, x1, y1)

A content block in fractional page coordinates (0-1, origin top-left).

Attributes

Name Description
area Fraction of the page the block covers.
h Height as a fraction of the page.
w Width as a fraction of the page.

Methods

Name Description
iou Intersection over union — 1.0 when the two blocks coincide exactly.
iou
utils.figure_compare.Box.iou(other)

Intersection over union — 1.0 when the two blocks coincide exactly.

Pane

utils.figure_compare.Pane()

One side of an A/B row.

Parameters

Name Type Description Default
images The image to draw — a path, several paths stacked vertically (a paper that splits one quantity over separate scans), or None. required
title Heading above the pane. required
fallback Drawn in place of a missing image, so the pane still holds its slot in the layout instead of the row silently collapsing to one side. required

Functions

Name Description
compare Layout comparison of two figure images.
content_blocks Recursive XY-cut of an ink mask into content blocks, in pixel coordinates.
image_row A one-row figure holding panes at a COMMON height, widths following their aspect.
match_boxes Pair our panels with theirs by best overlap, leaving unmatched panels as None.
overlay Write a side-by-side of both images with their detected panels outlined.
page_boxes Panel boxes of one image in fractional page coordinates, plus its pixel size.
report_table The per-panel comparison as a markdown table, built through md_table.
side_by_side Write panes as one row at a common height — the A/B composite a report embeds.

compare

utils.figure_compare.compare(ours, theirs, **kwargs)

Layout comparison of two figure images.

Returns

Name Type Description
dict A dict with the page geometry of each, the matched panel pairs and their
dict offsets in percent of page, and summary statistics (mean/max offset, IoU).

content_blocks

utils.figure_compare.content_blocks(
    mask,
    *,
    min_gap_frac=0.012,
    min_size_frac=0.03,
    depth=4,
)

Recursive XY-cut of an ink mask into content blocks, in pixel coordinates.

Parameters

Name Type Description Default
mask np.ndarray Boolean ink mask, (rows, cols). required
min_gap_frac float Blank run, as a fraction of the page’s smaller side, that counts as a gutter. Below this, adjacent panels merge into one block. 0.012
min_size_frac float Blocks smaller than this fraction of the page in BOTH axes are dropped as decorations (tick labels, a stray legend). 0.03
depth int Maximum alternating cut depth. 4

Returns

Name Type Description
list[tuple[int, int, int, int]] (x0, y0, x1, y1) per block, in reading order.

image_row

utils.figure_compare.image_row(panes, width=6.7, fontsize=8)

A one-row figure holding panes at a COMMON height, widths following their aspect.

Equal heights with aspect-proportional widths is what makes an A/B honest: neither side is stretched to match the other, and the row fills width inches exactly, so the pair lands on a report’s text block without letterboxing. Returns (fig, axes) so a caller can annotate before saving.

Built on matplotlib.figure.Figure rather than pyplot, so calling this from a notebook (a Quarto report is one) neither switches the global backend nor leaks a figure into pyplot’s registry.

fontsize=0 drops the labels rather than drawing them at zero size, which FreeType rejects outright; it is how a caller measures the row’s pure image geometry.

match_boxes

utils.figure_compare.match_boxes(ours, theirs)

Pair our panels with theirs by best overlap, leaving unmatched panels as None.

Greedy on IoU rather than an assignment solve: when the layouts already broadly agree the two are identical, and when they do not, a greedy pairing degrades into obvious None rows instead of an inscrutable global optimum.

overlay

utils.figure_compare.overlay(result, outfile, titles=('ours', 'reference'))

Write a side-by-side of both images with their detected panels outlined.

The numbers say how far off the layout is; this says which panel drifted.

page_boxes

utils.figure_compare.page_boxes(path, **kwargs)

Panel boxes of one image in fractional page coordinates, plus its pixel size.

report_table

utils.figure_compare.report_table(result)

The per-panel comparison as a markdown table, built through md_table.

side_by_side

utils.figure_compare.side_by_side(
    panes,
    outfile,
    width=6.7,
    fontsize=6,
    dpi=300,
)

Write panes as one row at a common height — the A/B composite a report embeds.

A replication report sets the published figure beside its reproduction. Composing that pair at render time, rather than shipping a rendered composite, is what keeps a copyrighted original out of every artifact but the one the caller names here.