cutan.impacts¶
Synthetic impact clips with exact ground truth, for scoring sub-frame timing.
Structured animations of simple objects — a stick or a ball — striking a surface, or striking “the air” (a stroke that reverses with no contact), on a known tempo grid. Each clip ships a sidecar that keeps three times apart:
the intended grid time of every event (
t_grid),the executed impact time in continuous seconds (
t_impact— the grid plus controllable humanisation, never snapped to a frame),and what the frames show: every frame’s exposure interval and sample instants (
an.frame_clock.FrameClock: frame rate, shutter, capture jitter), the 2D keypoints at each frame, and which frames bracket each impact.
The clips are ordinary an scenes — props moved by tweens — rendered by the
cutout backend, and the ground truth is read back from the same compiled
document the renderer draws.
Quick start:
from cutan.impacts import ImpactClipSpec, write_impact_clip, write_impact_set
write_impact_clip(ImpactClipSpec(kind="air", fps=30, exposure=0.5,
jitter_sd=0.01), "~/clips")
write_impact_set("~/clips/set") # 24 clips: objects x kinds x fps x shutter
Or from the shell: an impacts clip OUT_DIR / an impacts clip-set OUT_DIR.
>>> plan = plan_impact_clip(ImpactClipSpec(kind="air", beats=2, tempo=60, jitter_sd=0))
>>> [(e.t_grid, e.t_impact) for e in plan.events]
[(0.5, 0.5), (1.5, 1.5)]
Functions
|
A ball moving onto a floor whose top is at |
|
Chain rise / hold / fall segments through every executed impact. |
|
Offsets from an AR(1) Gaussian process: |
|
Build a registered object by name. |
|
The cartesian product of the given axes over |
|
The impacts of |
|
Resolve |
|
A drumstick rotating about |
|
Coerce a bpm, a |
|
Write one clip into |
|
Write every clip in |
Classes
|
One output frame: when its exposure opened and closed, and what it saw. |
|
A camera's timing, as data. |
|
Everything that determines a clip. |
|
One impact: where the grid put it and when it was executed. |
|
One striking object, its surface, and how the stroke moves it. |
|
A clip before it touches disk: events, motion, camera, and the Scene IR. |
|
The whole curve, plus the kinematics of every impact on it. |
|
|
|
Tempo as a function of beat: piecewise-linear BPM between |
Exceptions
The ground truth and the thing it describes disagree. |
- class cutan.impacts.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.
- class cutan.impacts.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
- class cutan.impacts.ImpactClipSpec(object='stick', kind='surface', tempo=100.0, beats=16, subdivision=1, pattern=(1.0,), lead_in=0.5, tail=0.5, jitter_sd=0.008, jitter_rho=0.0, jitter_bias=0.0, rise=0.18, fall=0.18, brake=0.03, rise_sd=0.0, fall_sd=0.0, brake_sd=0.0, arc_radius=None, show_surface=None, fps=30.0, exposure=0.0, exposure_samples=None, timestamp_jitter_sd=0.0, timestamp_noise_sd=0.0, phase=0.0, timestamps='nominal', width=640, height=360, trajectory_hz=1000.0, seed=0)[source]¶
Bases:
objectEverything that determines a clip. Defaults: a stick hitting a table at 100 BPM.
Performance:
tempo(a bpm, or[(beat, bpm), ...]for a tempo change),beats,subdivision,pattern(per-step stroke heights,0= rest),lead_in,tail, and the humanisationjitter_sd/jitter_rho/jitter_bias(seconds; seecutan.impacts.performance.gaussian_humanizer()). Humanised by default (jitter_sd=DEFAULT_JITTER_SD): on a perfect grid every impact of a round tempo lands exactly on a frame at common rates, which is the one case a sub-frame estimator cannot be scored on. Set it to 0 for a metronome.Motion:
object("stick"or"ball"),kind("surface"or"air"), the stroke timingsrise/fall/brake, andshow_surface(None: drawn for surface impacts only). Stroke-shape variability (cutan#27):rise_sd/fall_sd/brake_sd(seconds) draw each stroke’s own timings around those means from the seed’s stroke stream, never belowMIN_TIMING_FRACTIONof the mean;truth.jsonrecords each event’s actualrise,fallandbrake.arc_radius(scene px, the ball only) swings the ball on a circle about a pivot that far above its contact point, instead of a straight fall: the contact is the arc’s lowest point andimpact_xyis where it lands, as before.Camera (
an.frame_clock.FrameClock):fps,exposure,exposure_samples,timestamp_jitter_sd(when frames are really taken),timestamp_noise_sd(noise on the reported timestamp only),phase,timestamps.seeddrives two independent streams — the performance’s and the camera’s — so clips that differ only in camera settings share the exact same performance.- property clip_id: str¶
A readable, deterministic directory name.
>>> ImpactClipSpec().clip_id[:-8] 'stick-surface-30fps-e0-' >>> ImpactClipSpec().clip_id == ImpactClipSpec().clip_id True
- class cutan.impacts.ImpactEvent(index, beat, t_grid, t_impact, amplitude)[source]¶
Bases:
objectOne impact: where the grid put it and when it was executed.
- class cutan.impacts.ImpactObject(name, art, at, channels, keypoints, impact_keypoint, keypoint_nodes=dict(), surface_art=None, surface_at=None, params=dict())[source]¶
Bases:
objectOne striking object, its surface, and how the stroke moves it.
channelsare the properties the stroke drives — one for a stick or a ball, two for a forearm-plus-stick limb (each affine in the SAMEh, so the motion stays exact).keypointsare local points by name; each lives on the nodekeypoint_nodes[name]names, the entity itself by default.
- class cutan.impacts.ImpactPlan(spec, events, obj, stroke, frames, scene)[source]¶
Bases:
objectA clip before it touches disk: events, motion, camera, and the Scene IR.
- class cutan.impacts.Stroke(kind, duration, segments, kinematics)[source]¶
Bases:
objectThe whole curve, plus the kinematics of every impact on it.
- class cutan.impacts.StrokeSegment(t0, t1, h0, h1, easing)[source]¶
Bases:
objecthgoes fromh0toh1over[t0, t1]undereasing.
- class cutan.impacts.TempoMap(points)[source]¶
Bases:
objectTempo as a function of beat: piecewise-linear BPM between
(beat, bpm)points.Constant before the first point and after the last. Linear in BEATS, not in seconds, which is how a score writes an accelerando (“speed up over these four bars”); the time of a beat is the exact integral of
60 / bpm.>>> TempoMap(((0, 120),)).time_of(3) 1.5 >>> m = TempoMap(((0, 60), (4, 120))) >>> round(m.time_of(4), 6) # 4 * 60/(120-60) * ln(120/60) 2.772589 >>> m.bpm_at(2), m.bpm_at(10) (90.0, 120.0)
- exception cutan.impacts.TruthMismatch[source]¶
Bases:
RuntimeErrorThe ground truth and the thing it describes disagree.
- cutan.impacts.ball(*, radius=18.0, x=0.0, floor_y=90.0, drop=170.0, arc_radius=None, color='#111827', surface_color='#9ca3af', surface_size=(160.0, 24.0))[source]¶
A ball moving onto a floor whose top is at
floor_y.A full stroke lifts it
droppixels. Keypoints:centerandbottom(its contact point).Straight by default. With
arc_radius(cutan#27, a wide stick arc: a hard hit) it hangs on a circle of that radius about a pivot straight above its contact point, and the stroke ROTATES the pivot: the ball swings up to one side and falls back along the arc, its contact the arc’s lowest point. The angle that lifts itdroppixels isacos(1 - drop / arc_radius); rotation is the one channel, affine inh, so the curve stays exact.>>> b = ball(arc_radius=300.0) >>> round(b.params["arc_sweep"], 4), b.at == (0.0, b.params["pivot_y"]) (1.1226, True)- Return type:
- cutan.impacts.build_stroke(events, *, kind='surface', duration, rise=0.18, fall=0.18, brake=0.03, rest_height=1.0, timings=None)[source]¶
Chain rise / hold / fall segments through every executed impact.
The object starts and ends at
rest_height; before impactkit is raised toevents[k].amplitude(a bigger preparation, a harder hit). Segments tile[0, duration]exactly, and every impact is a segment boundary at preciselyt_impact.timingsgives each impact its own rise, fall and brake (one per event, cutan#27); without it every stroke takesrise,fallandbrake.- Return type:
- cutan.impacts.gaussian_humanizer(sd=0.0, *, rho=0.0, bias=0.0)[source]¶
Offsets from an AR(1) Gaussian process:
o[k] = bias + rho*(o[k-1]-bias) + e.sdis the STATIONARY standard deviation of the offsets (seconds) — the innovation is scaled bysqrt(1 - rho**2)so changingrhochanges how the timing wanders, not how far.rho = 0is independent jitter;rhonear 1 is a player who drifts ahead or behind for several beats.biasis a constant lead (negative) or lag (positive).>>> import numpy as np >>> h = gaussian_humanizer(0.01, rho=0.5) >>> offs = h([0.0] * 20000, np.random.default_rng(0)) >>> round(float(np.std(offs)), 3) 0.01 >>> gaussian_humanizer()([0.0, 1.0], None) [0.0, 0.0]
- cutan.impacts.impact_object(name, **kwargs)[source]¶
Build a registered object by name.
>>> impact_object("ball").name 'ball' >>> impact_object("hammer") Traceback (most recent call last): ... KeyError: "no impact object 'hammer'; known: ['ball', 'stick']"- Return type:
- cutan.impacts.impact_set_specs(*, base=ImpactClipSpec(object='stick', kind='surface', tempo=((0.0, 96.0), (24.0, 132.0)), beats=24, subdivision=1, pattern=(1.0, 0.6, 0.8, 0.6), lead_in=0.5, tail=0.5, jitter_sd=0.012, jitter_rho=0.3, jitter_bias=0.0, rise=0.18, fall=0.18, brake=0.03, rise_sd=0.0, fall_sd=0.0, brake_sd=0.0, arc_radius=None, show_surface=None, fps=30.0, exposure=0.0, exposure_samples=None, timestamp_jitter_sd=0.0, timestamp_noise_sd=0.0, phase=0.0, timestamps='nominal', width=640, height=360, trajectory_hz=1000.0, seed=0), objects=('stick', 'ball'), kinds=('surface', 'air'), fps=(24, 30, 60), exposures=(0.0, 0.5), timestamp_jitter_sds=(0.0,), seeds=(0,))[source]¶
The cartesian product of the given axes over
base.>>> len(impact_set_specs()) 24 >>> {s.seed for s in impact_set_specs(seeds=(0, 1), fps=(30,))} {0, 1}- Return type:
- cutan.impacts.perform(tempo=100.0, *, beats=16, subdivision=1, pattern=(1.0,), lead_in=0.5, humanizer=None, seed=0)[source]¶
The impacts of
beatsbeats ofpattern, ontempo’s grid.subdivisiongrid steps per beat;patternis cycled over the steps, one value per step:0is a rest, anything in(0, 1]is a hit of that relative stroke height (an accent is a bigger stroke).lead_inshifts beat 0 to that many seconds into the clip.humanizerturns grid times into offsets (default: none); each offset is then clamped toMAX_OFFSET_FRACTIONof the gap to either neighbour, and the clamped value is what the event records — the ground truth is what was executed.>>> [e.beat for e in perform(120, beats=2, subdivision=2, pattern=(1, 0))] [0.0, 1.0] >>> [e.amplitude for e in perform(120, beats=1, subdivision=4, pattern=(1, .5))] [1.0, 0.5, 1.0, 0.5]- Return type:
- cutan.impacts.plan_impact_clip(spec)[source]¶
Resolve
specinto events, a stroke, frames and a Scene IR. No I/O.- Return type:
- cutan.impacts.stick(*, length=200.0, thickness=12.0, pivot=(-120.0, -30.0), contact_angle=0.35, swing=0.95, color='#111827', surface_color='#9ca3af', surface_size=(140.0, 24.0))[source]¶
A drumstick rotating about
pivot(its butt — the hand).At contact it points
contact_angleradians below horizontal; a full stroke raises it byswingradians. Keypoints:pivotandtip(the end of its axis). The surface’s top meets the lowest point of its rounded end.- Return type:
- cutan.impacts.tempo_map(tempo)[source]¶
Coerce a bpm, a
[(beat, bpm), ...]list, or aTempoMap.>>> tempo_map(90).points ((0.0, 90.0),) >>> tempo_map([(0, 90), (16, 120)]).points ((0.0, 90.0), (16.0, 120.0))- Return type:
- cutan.impacts.write_impact_clip(spec, out_dir, *, render=True, clip_dir=None)[source]¶
Write one clip into
out_dir / (clip_dir or spec.clip_id); return that dir.With
render=Falseeverything butclip.mp4is written, and no browser is needed: the keypoints and the truth come from the compiled document, not from the pixels.- Return type:
- cutan.impacts.write_impact_set(out_dir, specs=None, *, render=True, progress=None)[source]¶
Write every clip in
specs(default:impact_set_specs()) plusindex.json.progress, if given, is called with(i, n, clip_dir)after each clip.- Return type:
Modules
|
|
One impact clip, end to end: spec -> scene -> ground truth -> (video) -> files. |
|
The things that strike: a stick and a ball, as ordinary |
|
The performance: a tempo grid, and when each impact was intended and executed. |
|
The stroke: impact events -> one continuous height curve |
|
Ground truth: what was intended, what was executed, and what each frame shows. |