an.media.shutter#

The temporal half of the frame stage: average several instants into one frame.

supersample.py resolves a frame SPATIALLY — k x k pixels into one. This module resolves it TEMPORALLY — the screenshots taken at each of a frame’s sample instants (RenderContext.frame_samples, built by an.frame_clock.FrameClock) into the one frame a camera with an open shutter would have recorded. Same place, same rules: the resolve runs in the frame stage, so everything downstream still sees PNGs at the declared size, and the rounding is spelled out rather than inherited.

One sample is free. A frame with a single instant keeps the bytes the spatial path produces (Chromium’s own, at supersample == 1), so a render without an open shutter is byte-identical to one from before this module.

Engine-independent, like the spatial half: moved from an/adapters/cutout/shutter.py (an#247), which re-exports it.

Functions

check_frame_samples(frame_samples, *, ...)

Validate frame_samples against the render it is for; normalise to tuples.

mean_png_bytes(shots, *, factor)

Spatially resolve each screenshot by factor, then average them in time.

temporal_mean(frames)

k equal-shape uint8 frames -> their exact per-pixel mean, uint8.

Exceptions

ShutterError

Frame samples a render cannot honour.

exception an.media.shutter.ShutterError[source]#

Bases: ValueError

Frame samples a render cannot honour.

an.media.shutter.check_frame_samples(frame_samples, *, total_frames, duration)[source]#

Validate frame_samples against the render it is for; normalise to tuples.

Checked before a browser launches, for check_factor’s reason: the render costs minutes and this costs microseconds.

Return type:

tuple[tuple[float, ...], ...] | None

>>> check_frame_samples(None, total_frames=3, duration=0.1) is None
True
>>> check_frame_samples([[0.0], [0.5, 0.6]], total_frames=2, duration=1.0)
((0.0,), (0.5, 0.6))
>>> check_frame_samples([[0.0]], total_frames=2, duration=1.0)
Traceback (most recent call last):
  ...
an.media.shutter.ShutterError: frame_samples has 1 frame(s) but this render has 2; a frame clock must describe every frame, and only those
an.media.shutter.mean_png_bytes(shots, *, factor)[source]#

Spatially resolve each screenshot by factor, then average them in time.

One screenshot takes exactly the spatial path — the same bytes a render without a shutter writes.

Return type:

bytes

an.media.shutter.temporal_mean(frames)[source]#

k equal-shape uint8 frames -> their exact per-pixel mean, uint8.

Rounded half-to-even, the rule block_mean_resolve spells out, so the temporal and spatial resolves agree about every tie. The mean is of the ENCODED (sRGB) values, as the spatial resolve’s is — not of linear light, so a smear’s profile is not exactly a physical sensor’s. The centroid of what is drawn is still the average of the sampled positions, which is the property the impact ground truth relies on.

Return type:

Any

>>> import numpy as np
>>> a, b = np.full((1, 2, 3), 1, np.uint8), np.full((1, 2, 3), 2, np.uint8)
>>> temporal_mean([a, b])[0, 0].tolist()   # 1.5 -> 2 (even)
[2, 2, 2]
>>> temporal_mean([a, a + 2])[0, 0].tolist()   # 2.0 exactly
[2, 2, 2]
>>> temporal_mean([a]) is a
True