DEV Community

snake lil
snake lil

Posted on Edited on

A Screenshot MAE Is Not a Material Accuracy Score

AI assistance disclosure: AI assisted with research, drafting and the numerical analysis reported here. The source images are direct WebGL renders, not AI-generated images.

A screenshot difference can confirm that a rendering changed. Calling the same number a material-accuracy score requires evidence that the comparison does not provide.

Here is a small example: the same pair of retained PNGs gives an RGB mean absolute difference of 6.407536 over the full image and 18.313377 over a shared non-background mask. No material, lighting or pixel value changed between those calculations. Only the averaging domain changed.

This is new analysis of an existing capture set, performed on October 31, 2026. It is not another rendering experiment, a physical-film comparison or a new benchmark of Three.js.

The inputs and the question

The source frames are the procedural-sphere renders from the October 30 update to this material audit. All three PNGs are 640 × 480. Their camera, geometry, lights, environment, exposure and base color were fixed. Base roughness was 0.86 and metalness 0.02.

Download the source PNG copies as A.png, B.png and C.png. On November 1, each download was checked against the retained source: the PNG file bytes differ, but all decoded RGB pixel bytes are identical. That observation does not establish what changed the file encoding. It does make decoded-pixel hashes the useful input check for this calculation.

Frame Clearcoat Clearcoat roughness
A 1 0.04
B 0 0.04
C 1 0.86

A/B changes coating intensity. A/C changes coating roughness. B/C changes both, so it is included as a descriptive image comparison rather than a single-variable intervention.

The original capture sequence returned to A twice, with identical PNG bytes. That helps establish repeatability within that session. It does not supply a physically correct target image: none of A, B or C is a measured real material used as ground truth.

For this analysis, the question is narrower: how much does the definition of the screenshot statistic affect the number we report?

Background pixels participate in the average

For an image domain containing N pixels, the metric used here is:

RGB8 MAE = sum over pixels and RGB channels of |left - right| / (3 × N)
Enter fullscreen mode Exit fullscreen mode

Its units are encoded 8-bit channel values, on a 0–255 scale. Every included pixel contributes to the denominator, even when its difference is zero.

The three images have the same flat background. Every outer-border pixel in every source PNG is exactly (28, 35, 27). A single common mask was constructed by including a pixel if it differed from that background color in any of A, B or C. The same mask was then used for all comparisons.

This mask includes 107,484 of 307,200 pixels, or 34.98828125% of the frame. All pairwise differences lie inside it; the sum of differences outside it is zero.

This is a convenient mask for these synthetic images, not a general foreground-segmentation technique. An object region matching the background could be omitted. Real scenes with gradients, shadows or textured backgrounds need a different, explicitly defined region of interest.

Pair Full-frame RGB8 MAE Common-mask RGB8 MAE
A / B 6.407536 18.313377
A / C 5.679630 16.232949
B / C 4.714180 13.473596

Both columns describe the same original pixels. They answer different questions: average difference over the complete output, or average difference over this particular shared mask. The larger masked value does not indicate a worse renderer, and the smaller full-frame value does not indicate a more faithful material.

There is an even simpler consequence. If identical background were added around both images until the total area was four times larger, the difference sum would stay constant and full-frame MAE would be divided by four. A/B would become 1.601884.

That last value is an algebraic padding calculation. No padded images were created, resized or rendered. It illustrates why scores from differently framed screenshots should not be compared without specifying their averaging domains.

Encoded RGB and decoded display output are different statistics

The render setup used ACES Filmic tone mapping and sRGB output. Three.js distinguishes its linear working space from the nonlinear display encoding; those roles are described in its Color Management manual.

The analysis also decoded each 8-bit channel with the inverse sRGB transfer function before taking absolute differences. For normalized values s in [0, 1], that conversion is:

def decode_srgb(s):
    return s / 12.92 if s <= 0.04045 else ((s + 0.055) / 1.055) ** 2.4
Enter fullscreen mode Exit fullscreen mode

The formula is consistent with the conversion example in CSS Color 4, a work-in-progress specification. Only nonnegative, in-range inputs are needed for these RGB8 files.

Pair Full-frame MAE after inverse-sRGB decoding, scale 0–1
A / B 0.020100281
A / C 0.019102066
B / C 0.012787025

These values are not directly comparable in magnitude to the RGB8 column because their scales and encodings differ. The ordering happens to remain the same in this dataset; no general ordering guarantee follows.

Crucially, inverse-sRGB decoding does not undo the earlier tone mapping, recover the scene's HDR radiance, or undo quantization. These are differences in decoded display output. Neither this calculation nor the encoded-RGB version is ΔE, a measured gloss value or a perceptual fidelity assessment.

Reproduce the domain comparison

Use the original PNGs as A.png, B.png and C.png, with Python and Pillow. Avoid JPEG conversions, resized copies or screenshots of the article page. This compact calculation reproduces the first table and the decoded-output table:

from hashlib import sha256
from itertools import combinations
from PIL import Image

frames = [Image.open(f"{name}.png").convert("RGB") for name in "ABC"]
assert all(frame.size == (640, 480) for frame in frames)
expected = [
    "05727ba66b1b305b38c0f689a76c69191d147a04b5a12bc7b7ea962d6c3d3a7e",
    "8d55da1b08820728d1f0358424216aa9de46823ddfa82d09bc3b990fb55ce313",
    "ca5556ae171ed681114fe0c229f6eead1cdffaf5b47e428836dfcd4a680df1c4",
]
assert [sha256(frame.tobytes()).hexdigest() for frame in frames] == expected
pixels = [list(frame.getdata()) for frame in frames]
n = 640 * 480
background = pixels[0][0]
border = [y * 640 + x for y in range(480) for x in range(640)
          if x in (0, 639) or y in (0, 479)]
assert all(frame[i] == background for frame in pixels for i in border)
mask = [any(frame[i] != background for frame in pixels) for i in range(n)]
m = sum(mask)
assert 0 < m <= n

def decode(v):
    s = v / 255
    return s / 12.92 if s <= 0.04045 else ((s + 0.055) / 1.055) ** 2.4

lut = [decode(v) for v in range(256)]
for left, right in combinations(range(3), 2):
    total = masked = 0
    linear = 0.0
    for i, (a, b) in enumerate(zip(pixels[left], pixels[right])):
        delta = sum(abs(x - y) for x, y in zip(a, b))
        total += delta
        masked += delta if mask[i] else 0
        linear += sum(abs(lut[x] - lut[y]) for x, y in zip(a, b))
    assert total == masked
    print("ABC"[left], "ABC"[right], total / (3 * n),
          masked / (3 * m), linear / (3 * n))
Enter fullscreen mode Exit fullscreen mode

Pillow 12.3.0 was used for the retained-file analysis and the downloaded-copy checks. The hashes in the code are for row-major RGB bytes, after conversion to RGB, not the PNG container bytes. Image dimensions are checked separately.

The full-frame RGB8 results were checked against the earlier independent PNG analysis. The new analysis also checked that every difference was inside the common mask. No source image was modified, and no product code or rendering setting changed for this follow-up.

A useful reporting boundary

For screenshot regression work, record the source files, alignment, output encoding, averaging domain and formula alongside the score. If a region of interest is used, define it before comparing candidates and keep it common across the comparisons.

With those conditions fixed, a difference statistic can help detect a change or an exact return to a baseline. The separate question—whether the rendered material matches a real one—still needs an appropriate physical reference and a defined validation method. A low screenshot MAE alone cannot provide that reference.

Top comments (0)