an.bench.metrics#
The metric panel: pure numpy, no I/O, no subprocess.
Every function here is f(arrays, mask) -> float. Loading is
an.bench.imageio’s job. The split is what lets the whole panel run in
the default CI leg — numpy is a hard dependency, ffmpeg and Playwright are not
— which is the only part of this wave that main CI can ever see.
The set is the research’s corrected one, and it is not the epic’s draft. All twelve originally-proposed metrics were refuted. The corrections that are easiest to undo by accident, each guarded by a test:
edge_transition_width’s flatness tolerance must not be 0. With> 0, ±3-LSB dither sends the metric to 255.0.off_palette_pixel_fractionmust not usenp.unique(..., axis=0): 1.833 s/frame at 1080p against 0.019 s for the packed form, 94x, identical result.encode_flicker_on_held_pixelsmust cast before subtracting.np.abs(a - b)on uint8 is the identity on unsigned dtypes, so the literal proposed form measured the sign of the change. It stayed monotone, which is exactly why it would have shipped unnoticed. And it must report a rate, not a mean: the median held-pixel delta is 0 at every CRF.encode_ringing_excessis the difference of two overshoot means, so the source-hardness term cancels. Raw overshoot is a joint function of source hardness and encoder fidelity with one degree of freedom, which is why any move toward crisper outlines raised it under an unchanged encoder.edge_masked_distinct_coloursmust be handed a mask built fromluma_u8(), never fromluma709()directly.luma709returnsfloat in [0,1] and
an.bench.masks.edge_mask()thresholds at **40 on 0-255**, so the float form makes every two-apart gradient <= 1.0 and the mask comes back entirely empty — measured: 0 selected pixels against 4 for the same hard step. The metric would then benanon every scene, which reads as “the check could not run” rather than as a bug (an#55).
One metric the epic named is deliberately absent: mean_adjacent_frame_ssim
moves the wrong way (0.958 at crf18 -> 0.977 at crf51, because a crushed
video is smoother). Its existing use as a frozen-render detector in
an.verify.media_quality is a different and legitimate job.
Module Attributes
Two neighbours within this many code values count as "flat". |
|
Trim fraction for |
|
A flat-field pixel more than this far off is "deviated". |
|
A held pixel that moved by at least this much "flickered". |
|
|
|
The neighbourhood of each threshold counter's OWN free parameters over which its verdict must agree before |
|
How far off the line between two palette colours a pixel may sit and still be called a blend of them. |
|
BT.709 luma coefficients, recorded in the ledger so a future change to the reduction is visible rather than folded into the number. |
Functions
|
Say, for each off-palette colour, whether it is a blend of two declared ones. |
|
Mean distinct colours per frame, counted ONLY on the edge mask. |
|
Average thickness, in pixels, of the fuzzy band between two flat areas. |
|
Fraction of perfectly-held pixels that moved in the delivered video. |
|
|
|
How much more the lossy encode overshoots than a lossless one does. |
|
Fraction of flat-field pixels the encoder moved by more than |
|
|
|
Mean number of distinct colours per frame. |
|
The full-frame identity gate plus its diagnostics. |
|
|
|
|
|
|
|
Fraction of the frame whose colour is not one the compiler declared. |
|
The most frequent off-palette colours, as |
|
Mean positive excursion above the source, over the ring band. |
|
|
|
Windowed SSIM at stride 1, as a per-pixel map. |
|
The label of one cell of a parameter sweep, stable across rows. |
- an.bench.metrics.BLEND_TOLERANCE: int = 3#
How far off the line between two palette colours a pixel may sit and still be called a blend of them. 8-bit channels, so a couple of code values covers rounding in the compositor.
- an.bench.metrics.EDGE_FLAT_TOL: int = 4#
Two neighbours within this many code values count as “flat”. NOT 0 — see the module docstring.
- an.bench.metrics.EDGE_TRIM: float = 0.1#
Trim fraction for
edge_transition_width’s mean, so one pathological run cannot carry the number.
- an.bench.metrics.FLAT_DEV_TOL_SWEEP: tuple[int, ...] = (4, 5, 6, 7, 8)#
The neighbourhood of each threshold counter’s OWN free parameters over which its verdict must agree before
an bench-comparewill call it a direction (an#140).flat_field_deviationis(dev > tol).mean()over a mask eroded byFLAT_DILATE_K; both are free, and measured on one machine and one x264 build its direction under a 4:2:0 -> 4:4:4 change reversed on three of six scenes somewhere on this grid (graded_field+84.2% at tol 6, -81.6% at tol 8). Every shipped value sits inside its own sweep, so a sweep that disagrees is the row saying “somewhere near my own settings, my direction reverses”. The grids are the ones the defect was measured on, not tuned ones; the flicker grid is one-sided below because tol 0 counts every pixel.
- an.bench.metrics.FLICKER_DELTA_TOL: int = 2#
A held pixel that moved by at least this much “flickered”.
- an.bench.metrics.LUMA_709: tuple[float, float, float] = (0.2126, 0.7152, 0.0722)#
BT.709 luma coefficients, recorded in the ledger so a future change to the reduction is visible rather than folded into the number.
- an.bench.metrics.SSIM_RADIUS: int = 3#
ssim_mapwindow radius; 7x7, matched to feature size rather than the global-moment form.
- an.bench.metrics.classify_off_palette(entries, palette)[source]#
Say, for each off-palette colour, whether it is a blend of two declared ones.
This is the difference between a diagnostic and a list of hex codes. The metric’s whole meaning is “not one of the colours the compiler declared”, so a palette that under-collects turns it into a large, plausible number with no error anywhere. Anti-aliasing legitimately produces colours *on the segment between* two declared colours; a missed literal does not.
So each entry gains
blend_of(the two palette colours it sits between, orNone). A row whose top off-palette colours are all blends is reporting anti-aliasing correctly. One that is not is a derivation bug, and now it says so in the ledger rather than waiting for someone to look.>>> classify_off_palette([{"hex": "#808080"}], [0x000000, 0xffffff])[0]["blend_of"] ['#000000', '#ffffff'] >>> classify_off_palette([{"hex": "#ff00ff"}], [0x000000, 0xffffff])[0]["blend_of"]
- an.bench.metrics.edge_masked_distinct_colours(packed, edge)[source]#
Mean distinct colours per frame, counted ONLY on the edge mask.
The half of
frame_distinct_coloursthat is about edges: an interior-only change — a gradient laid into a flat field, a soft shadow — moves the whole-frame count and cannot reach this one at all. The second doctest below is that property, and it is the whole of what the mask buys.**It does NOT make the number blind to a whole-frame blur, and an#55’s premise that it would is refuted.** The mask is recomputed from the frame being measured, and a blur WIDENS the edge band, so the mask grows to admit the new gradation. Measured on the six committed goldens, 3x3 box blur, ratio against k=1:
scene
whole-frame
edge-masked
aa_probe
10.25x
9.50x
graded_field
2.04x
2.35x
multi_shot
7.92x
4.63x
promote_demo
1.25x
0.83x
saturated_outline
1.49x
1.14x
single_character
8.60x
5.70x
Damped on four of six, WORSE on
graded_field, and nowhere near blind. (an#55 quotes “1.8x-9.3x” for the whole-frame column; the real range on these goldens is 1.25x-10.25x, wider at both ends.)**What separates a blur from a supersample is the WIDTH half of the pair, not this one.** The same blur puts
edge_transition_widthat 2.1x-3.4x (aa_probe2.655 -> 5.594 px), while an exact k=2 resolve moves it +2.6% to +8.0% (research §4). So read this metric BESIDEedge_transition_width: colours up with width flat is gradation added; colours up with width doubled is a soft picture.Evidence for a human reader, never a gate. an#41’s criterion counts metrics independently and cannot express a conjunction, so neither half of the pair may be declared as counting on the strength of the other.
edgemust come froman.bench.masks.edge_mask()applied toluma_u8()— not toluma709(), which is float in [0,1] against a threshold of 40 on 0-255 and yields an empty mask every time.Returns
(mean, frames_measured). A frame whose mask is empty is skipped, not averaged in as zero — a zero would drag the mean down and read as exactly the regression this metric exists to notice. With no such frame the answer isnan, which the caller records asunavailable;an.bench.ledger.measured()refuses it.>>> import numpy as np >>> from an.bench.masks import edge_mask >>> c = np.zeros((1, 4, 16, 3), np.uint8); c[0, :, 8:] = 255 >>> edge_masked_distinct_colours(pack_rgb(c), edge_mask(luma_u8(c))) (2.0, 1)
A change entirely inside a flat field moves the whole-frame count and not this one — this, and only this, is what the mask buys:
>>> d = c.copy() >>> for x in range(2, 6): d[0, :, x] = 8 * (x - 1) ... >>> frame_distinct_colours(pack_rgb(d)) 6.0 >>> edge_masked_distinct_colours(pack_rgb(d), edge_mask(luma_u8(d))) (2.0, 1)
An empty mask is not a colour count of zero:
>>> flat = np.full((1, 4, 8, 3), 128, np.uint8) >>> edge_masked_distinct_colours(pack_rgb(flat), edge_mask(luma_u8(flat))) (nan, 0)
- an.bench.metrics.edge_transition_width(rgb, *, tol=4, trim=0.1)[source]#
Average thickness, in pixels, of the fuzzy band between two flat areas.
Under 1 is a jagged staircase; ~1 is clean AA; 3+ means the picture has gone soft. Two-sided, which is the whole reason it replaced a palette cardinality count: cardinality reads AA-off as 4, AA-on as 45 and a 3x3 blur as 416, so “the number went up” means “AA restored” and “the picture went soft” indiscriminately.
Returns
(trimmed_mean, median).>>> import numpy as np >>> a = np.zeros((1, 4, 8, 3), np.uint8); a[0, :, 4:] = 255 >>> round(edge_transition_width(a)[0], 3) # one hard step = a 2px band 2.0
- an.bench.metrics.encode_flicker_on_held_pixels(src_rgb, dec_rgb, *, tol=2)[source]#
Fraction of perfectly-held pixels that moved in the delivered video.
Held-pose “boiling” — the worst artefact for limited-motion animation. Pooled over every frame pair rather than averaged per pair, so a pair with three held pixels does not weigh as much as one with seventy thousand.
- Return type:
>>> import numpy as np >>> s = np.zeros((2, 1, 4, 3), np.uint8) >>> d = s.copy(); d[1, 0, 0] = 5 >>> encode_flicker_on_held_pixels(s, d) 0.25
- an.bench.metrics.encode_flicker_sweep(src_rgb, dec_rgb, *, tols=(1, 2, 3, 4))[source]#
encode_flicker_on_held_pixels’ count at everytolof its sweep.Same shape and same purpose as
flat_field_deviation_sweep(): this is the panel’s other hard-threshold counter, and measured under the same 4:2:0 -> 4:4:4 change its direction reverses between tol 2 and tol 3 onsingle_character(+25% -> -15%), and underhigh_crfbetween tol 1 and tol 2 ongraded_field(-9% -> +29%). Empty when nothing is held.>>> import numpy as np >>> s = np.zeros((2, 1, 4, 3), np.uint8) >>> d = s.copy(); d[1, 0, 0] = 2 >>> encode_flicker_sweep(s, d, tols=(2, 3)) {'tol=2': [1, 4], 'tol=3': [0, 4]}
- an.bench.metrics.encode_ringing_excess(dec_luma, lossless_luma, src_luma, ring)[source]#
How much more the lossy encode overshoots than a lossless one does.
Both legs share the source, so the source-spectrum term cancels: AA-off raises both together (correctly reporting “the encoder did not get worse”), a genuine crispness improvement also raises both (no false regression), and a CRF change raises only the lossy leg.
Provisional.
edge_band_maeis recorded beside it so research open question 4 — “does plain edge-band MAE beat this?” — is answered by the ledger rather than by nobody.- Return type:
>>> import numpy as np >>> s = np.zeros((1, 1, 2), np.uint8) >>> encode_ringing_excess(np.array([[[9, 0]]], np.uint8), ... np.array([[[3, 0]]], np.uint8), s, np.ones((1, 1, 2), bool)) 3.0
- an.bench.metrics.flat_field_deviation(src_rgb, dec_rgb, flat, *, tol=6)[source]#
Fraction of flat-field pixels the encoder moved by more than
tol.The strongest metric in the set and the only one measured genuinely orthogonal to the edge/AA axis: monotone over a 133x span on the CRF ladder (0.0003 -> 0.0399, crf18 -> crf51) and flat under every AA variant.
Returns
(fraction_over_tol, p99_of_the_deviation).>>> import numpy as np >>> s = np.zeros((1, 3, 3, 3), np.uint8) >>> d = s.copy(); d[0, 1, 1] = 20 >>> flat_field_deviation(s, d, np.ones((1, 3, 3), bool))[0] 0.1111111111111111
- an.bench.metrics.flat_field_deviation_sweep(src_rgb, dec_rgb, *, mask_rgb, tols=(4, 5, 6, 7, 8), ks=(1, 3, 5))[source]#
flat_field_deviation’s count at every(dilate_k, tol)of its sweep.{cell_key: [counted, of]}— integers, so a comparison between two rows is exact rather than a comparison of two rounded fractions. The positional pair isflat_field_deviation’s own (reference, decoded);mask_rgb— what the flat mask is derived from, the SOURCE frames inrun.py— is keyword-only so the two roles cannot be swapped positionally. Akwhose mask selects nothing is omitted, not recorded as[0, 0].It is not a new number for the panel. It is what lets
an bench-comparetell a verdict from a threshold accident (an#140): the shipped cell IS the metric, and the others say whether anywhere else on the declared grid of its own two parameters the direction reverses.>>> import numpy as np >>> s = np.zeros((1, 9, 9, 3), np.uint8) >>> d = s.copy(); d[0, 4, 4] = 7 >>> cells = flat_field_deviation_sweep(s, d, mask_rgb=s, tols=(6, 7), ks=(3,)) >>> cells {'dilate_k=3,tol=6': [1, 81], 'dilate_k=3,tol=7': [0, 81]}
- an.bench.metrics.frame_distinct_colours(packed)[source]#
Mean number of distinct colours per frame.
A flatness / palette-discipline guard, with no predicted direction on an AA change. Do not count it alongside
off_palette_pixel_fraction; they are the same family.- Return type:
>>> import numpy as np >>> frame_distinct_colours(np.array([[[1, 1, 2]], [[3, 4, 5]]], np.uint32)) 2.5
- an.bench.metrics.golden_comparison(today_rgb, golden_rgb)[source]#
The full-frame identity gate plus its diagnostics.
Full-frame, not edge-masked: that is what makes the one sentence true, and what catches the flat-interior regressions an edge mask is blind to — and for this look the flat fields are most of the picture.
changed_pxandmax_deltabelong in a failure message, not in the metrics block; they are returned here so the caller can put them there.- Return type:
>>> import numpy as np >>> a = np.zeros((4, 4, 3), np.uint8) >>> golden_comparison(a, a)["identical"] True
- an.bench.metrics.luma709(rgb)[source]#
(H,W,3)uint8 ->(H,W)float in [0,1]. PIL-free.- Return type:
>>> import numpy as np >>> round(float(luma709(np.full((1, 1, 3), 255, np.uint8))[0, 0]), 6) 1.0
- an.bench.metrics.luma_u8(rgb)[source]#
(...,3)uint8 ->(...)uint8 luma, on the 0-255 scale a plane uses.The one conversion between
luma709(), which returns **float in [0,1]**, andan.bench.masks.edge_mask(), whose threshold is **40 on 0-255**. Handing the float straight to the mask is not a wrong number, it is an empty mask: every two-apart gradient is <= 1.0, so nothing is ever an edge and the metric downstream readsunavailableon every scene. Written once, named and tested here rather than open-coded at each call site, because it cost a debugging round the first time (an#55).This is FULL-RANGE luma and the encode-side plane is not.
an.bench.imageio.SOURCE_SCALE_FILTERpinsout_range=tv, so ffmpeg’s Y sits in [16,235] and its gradients are 219/255 of these. At one threshold that makes the render-side mask the wider of the two — measured: a 45-code-value step selects 4 pixels here and 0 there — which is why the row records it under its own operator string rather than reusingan.bench.masks.EDGE_OPERATOR.- Return type:
>>> import numpy as np >>> a = np.zeros((1, 1, 2, 3), np.uint8); a[0, 0, 1] = 255 >>> luma_u8(a).tolist() [[[0, 255]]]
- an.bench.metrics.masked_mean_abs(a, b, mask)[source]#
|a - b|averaged overmask, with the cast that makes it correct.- Return type:
>>> import numpy as np >>> x = np.array([[[10, 200]]], np.uint8); y = np.array([[[12, 190]]], np.uint8) >>> masked_mean_abs(x, y, np.ones((1, 1, 2), bool)) 6.0
- an.bench.metrics.off_palette_pixel_fraction(packed, palette)[source]#
Fraction of the frame whose colour is not one the compiler declared.
- Return type:
>>> import numpy as np >>> p = np.array([[[0x000000, 0xffffff, 0x808080]]], np.uint32) >>> round(off_palette_pixel_fraction(p, [0x000000, 0xffffff]), 4) 0.3333
- an.bench.metrics.off_palette_top_colours(packed, palette, *, top=10)[source]#
The most frequent off-palette colours, as
[{"hex", "count"}].Not decoration: this metric’s whole meaning is “not one of the colours the compiler declared”, so a palette that under-collects turns it into a large plausible number with no error anywhere. If the top entries are exact hex literals from the staged art, the derivation missed them; if they are blends sitting between two palette entries, that is anti-aliasing and the number is right.
>>> import numpy as np >>> p = np.array([[[1, 1, 2]]], np.uint32) >>> off_palette_top_colours(p, [2], top=1) [{'hex': '#000001', 'count': 2}]
- an.bench.metrics.overshoot_mean(dec_luma, src_luma, ring)[source]#
Mean positive excursion above the source, over the ring band.
- Return type:
>>> import numpy as np >>> s = np.zeros((1, 1, 2), np.uint8); d = np.array([[[5, 0]]], np.uint8) >>> overshoot_mean(d, s, np.ones((1, 1, 2), bool)) 2.5
- an.bench.metrics.pack_rgb(rgb)[source]#
(N,H,W,3)uint8 ->(N,H,W)uint32, one integer per colour.- Return type:
>>> import numpy as np >>> hex(int(pack_rgb(np.array([[[[0x12, 0x34, 0x56]]]], np.uint8))[0, 0, 0])) '0x123456'
- an.bench.metrics.ssim_map(a, b, *, r=3)[source]#
Windowed SSIM at stride 1, as a per-pixel map.
Added beside
an.verify.media.ssim(), never replacing it: that function’s global-moment form is whatan.verify.media_quality’s frozen-render threshold was tuned against, andMediaQualityVerifiersits in the default orchestrate chain.Why it exists at all: the metrics survey concluded SSIM should be excluded because whole-frame SSIM scores a total eye-blink at 0.9989. That conclusion was refuted — only the global-moment reduction is blind. With the window matched to feature size, min-over-windows scores the same blink at 0.279 (1080p) and 0.063 (native).
Do not cross-check against ffmpeg’s
ssimfilter: it uses overlapped 8x8 block sums at 4-pixel stride, disagrees by up to 0.0201, and the disagreement grows with degradation.- Return type:
>>> import numpy as np >>> x = np.linspace(0, 1, 64).reshape(8, 8) >>> round(float(ssim_map(x, x).min()), 6) 1.0
- an.bench.metrics.sweep_cell_key(**params)[source]#
The label of one cell of a parameter sweep, stable across rows.
Keyword order is irrelevant — the key is sorted — so two rows written by code that spells the call differently still match cell for cell.
- Return type:
>>> sweep_cell_key(tol=6, dilate_k=3) 'dilate_k=3,tol=6'