an.frame_clock#
The frame clock: WHEN each output frame samples scene time.
A render has always sampled frame i at exactly i / fps — one instant,
on a perfect grid. A real camera does neither: its shutter stays open for part
of the frame period (so a moving object smears), and its capture instants
wander around the nominal grid (so the timestamp a frame carries is not quite
when it was taken). Anything that wants to measure motion from video — the
sub-frame impact estimators an.impacts exists to score — needs both, and
needs to know exactly what was done.
FrameClock is that model as data. FrameClock.frames() returns one
CapturedFrame per output frame, carrying the exposure interval and the
instants integrated over it; FrameClock.sample_times() is the projection
the renderer consumes through RenderContext.frame_samples. One object feeds
both the render and the ground truth, so the two cannot disagree about when a
frame was taken.
The default clock is the old behaviour exactly — no exposure, no jitter, one
sample at i / fps:
>>> FrameClock(fps=4).sample_times(1.0)
((0.0,), (0.25,), (0.5,), (0.75,))
A 180-degree shutter (exposure=0.5) integrates the first half of each frame
period, sampled at the midpoints of samples equal sub-intervals:
>>> FrameClock(fps=4, exposure=0.5, samples=2).sample_times(0.5)
((0.03125, 0.09375), (0.28125, 0.34375))
>>> f = FrameClock(fps=4, exposure=0.5, samples=2).frames(0.5)[1]
>>> (f.t_nominal, f.t_open, f.t_close, f.t_mid)
(0.25, 0.25, 0.375, 0.3125)
Capture jitter moves each frame’s exposure as a unit, never out of order:
>>> clock = FrameClock(fps=30, jitter_sd=0.004, seed=7)
>>> opens = [f.t_open for f in clock.frames(2.0)]
>>> all(b > a for a, b in zip(opens, opens[1:]))
True
>>> clock.frames(2.0) == clock.frames(2.0) # seeded: the same clock twice
True
Module Attributes
Sub-samples integrated per frame when the shutter is open and the caller did not say how many. |
|
A frame's capture offset is clamped to this fraction of the slack between one exposure closing and the next opening, on each side. |
|
Report noise is clamped to this fraction of the frame period, each side, so two neighbouring reported timestamps can never cross on a regular grid. |
Functions
|
Frames in a render of |
Classes
|
One output frame: when its exposure opened and closed, and what it saw. |
|
A camera's timing, as data. |
Exceptions
A frame clock that cannot describe a camera. |
- class an.frame_clock.CapturedFrame(index, t_nominal, t_open, t_close, samples, t_reported)[source]#
Bases:
objectOne output frame: when its exposure opened and closed, and what it saw.
t_nominalisindex / fps— what a constant-rate container (an mp4) says the frame’s time is.samplesare the instants actually rendered and averaged into it;t_midis the middle of the exposure, the single best instant to attribute a blurred frame to.t_reportedis the timestamp a capture pipeline hands downstream: nominal or actual, per the clock’stimestampssetting.
- an.frame_clock.DEFAULT_EXPOSURE_SAMPLES: int = 8#
Sub-samples integrated per frame when the shutter is open and the caller did not say how many. Eight midpoint samples put the smear of an object crossing 100 px in one exposure at steps of ~12 px — continuous to a tracker, cheap to render (each sample is one screenshot).
- class an.frame_clock.FrameClock(fps=30.0, exposure=0.0, samples=None, jitter_sd=0.0, phase=0.0, timestamps='nominal', report_noise_sd=0.0, seed=0)[source]#
Bases:
objectA camera’s timing, as data. Every field defaults to the ideal camera.
fps: nominal frame rate (need not be an integer: 29.97 is a camera).exposure: fraction of the frame period the shutter is open, in[0, 1]; 0 is an instantaneous sample, 0.5 a 180-degree shutter.samples: instants integrated per open exposure (midpoint rule);Nonemeans 1 whenexposure == 0andDEFAULT_EXPOSURE_SAMPLESotherwise.jitter_sd: standard deviation, in seconds, of each frame’s CAPTURE offset from the nominal grid — the frame really is taken early or late. Gaussian, clamped atmax_jitterso frames never overlap; ajitter_sdabove half that clamp is refused rather than silently shrunk (atexposure=1there is no room for any).phase: seconds added to every capture instant — the camera clock’s sub-frame offset from scene time, in(-1/fps, 1/fps).timestamps: the base ofCapturedFrame.t_reported—"nominal"(index / fps, what a naive tick loop or an mp4 reports) or"actual"(t_open, what a capture API with real timestamps reports).report_noise_sd: Gaussian noise, in seconds, added to the REPORTED timestamp only — regular capture, noisy clock (a browser frame callback). Clamped atMAX_REPORT_NOISE_FRACTIONof a frame period, with the same refuse-rather-than-shrink rule asjitter_sd, and reported timestamps must still increase: a real frame clock never runs backwards.seed: the random streams (capture jitter and report noise are independent draws from it).Every instant is clipped into
[0, duration], because a render cannot sample a scene outside its own timeline; the clipped values are whatframes()returns and what gets rendered.- frames(duration)[source]#
One
CapturedFrameper output frame of adurationrender.- Return type:
- property max_jitter: float#
The clamp on a frame’s capture offset, in seconds.
>>> round(FrameClock(fps=10, exposure=0.5).max_jitter, 9) 0.0245
- exception an.frame_clock.FrameClockError[source]#
Bases:
ValueErrorA frame clock that cannot describe a camera.
- an.frame_clock.MAX_JITTER_FRACTION: float = 0.49#
A frame’s capture offset is clamped to this fraction of the slack between one exposure closing and the next opening, on each side. Below one half, so two neighbouring frames can each move toward the other by the full clamp and still not overlap: the clock never reorders frames, whatever
jitter_sdis.