burns#

burns — Ken Burns pan/zoom video effects.

Turn a still image (or a sequence of stills) into a cinematic pan/zoom film, driven by one render-agnostic motion spec so the same path feeds the Python renderer here and the in-browser TypeScript port (kenburnz, in ts/).

The core abstraction is a pure, time-parameterized spec:

  • Rect — a normalized (x, y, w, h) viewport over the image (top-left origin, window-fraction zoom).

  • BurnsPath — keyframes + easing, with evaluate(t) -> Rect (pure, deterministic, frame-count-free) and JSON to_dict / from_dict.

  • ken_burns_path() — build a cohesive, deterministic path per sequence index from a little intent (style / zoom / pan / easing).

  • content_aware_path_for() — the content-aware counterpart: keep the subject (via salient_box()) and any injected face boxes framed.

  • MOVES + resolve_move() — the named-move vocabulary, for callers that want to store a camera move as an authored intent and resolve it against whatever still is in the slot at render time. A caller that caches the render puts RESOLVER_IMPL_VERSION in its key, since the pixels an unchanged intent becomes are decided here.

Two renderers consume a path plus a render-time duration:

  • ken_burns_video() — one image -> one mp4 (pluggable backend).

  • ken_burns_film() — a sequence of (image, path, duration) panels -> one continuous mp4 (single encode pass, no seams), with optional audio.

Quickstart:

>>> from burns import ken_burns_video, ken_burns_path
>>> ken_burns_video("photo.jpg")  # 2s push-in
>>> ken_burns_video(  # path per sequence index
...     "photo.jpg", ken_burns_path(1, style="push"), duration=5.0
... )

Functions

ken_burns_path(index, *[, style, zoom, pan, ...])

A deterministic BurnsPath for the index-th image of a sequence.

content_aware_path(img_w, img_h, *[, ...])

A BurnsPath that keeps the subject/faces framed while zooming toward or away from them — the content-aware counterpart of ken_burns_path().

content_aware_path_for(image, *[, subject, ...])

Convenience: derive the subject (via salient_box()) and faces (from faces or faces_detector(image)) straight from image, then call content_aware_path().

salient_box(image, *[, downscale, ...])

Estimate the salient (high-detail) region of image as a normalized box.

choose_move(seed)

Which concrete move "auto" resolves to for seed.

move_kind(move)

How move is grouped: "zoom", "drift", "static", "select".

resolve_move(move, *, image, aspect[, zoom, ...])

Resolve an authored camera intent against image into a path.

ken_burns_video(image[, path, duration, ...])

Render one image into a pan/zoom video from a BurnsPath.

ken_burns_film(panels, *, saveas[, fps, ...])

Render an N-panel Ken Burns film in a single pass.

parse_easing([spec])

Resolve an easing spec to a callable f: [0, 1] -> [0, 1].

cubic_bezier(x1, y1, x2, y2)

A CSS-style cubic-bezier easing f: [0, 1] -> [0, 1].

register_backend(name, backend)

Register a render backend under name (open-closed extension point).

get_backend([name])

Look up a registered backend by name.

Classes

Rect(x, y, w, h)

A normalized (x, y, w, h) viewport over a source image.

BurnsPath(keyframes[, easing, interp, ...])

A time-parameterized pan/zoom motion over a still image.

RenderBackend(*args, **kwargs)

Encode one image + BurnsPath into a video file at output.

Exceptions

MoveError

A move that cannot be resolved into a path.

class burns.BurnsPath(keyframes, easing='ease-in-out', interp='linear', output_aspect=None, version=1)[source]#

Bases: object

A time-parameterized pan/zoom motion over a still image.

Construct it directly from keyframes, or via from_start_end() / push_in() for the common cases, or via ken_burns_path() for deterministic per-index motion across a sequence.

Parameters:
  • keyframes (tuple[tuple[float, Rect], ...]) – a sequence of (t, Rect) waypoints, t in [0, 1], strictly increasing in t. Must have at least one entry; the first t should be 0.0 and the last 1.0 for the whole clock to be covered (out-of-range t clamps to the ends).

  • easing (Union[str, Callable[[float], float], Sequence[float]]) – a CSS timing-function spec (name / cubic-bezier(...) / 4-tuple) or a callable [0,1] -> [0,1]. Default "ease-in-out".

  • interp (str) – geometry interpolation between keyframes. Only "linear" is implemented; the field exists so the spec can carry richer schemes ("catmull-rom", "bezier") without a format change.

  • output_aspect (Optional[float]) – the aspect ratio (width / height) the render should fill. None means “match the source image”.

  • version (int) – spec schema version (for forward-compatible serialization).

Examples

>>> p = BurnsPath.from_start_end(
...     Rect(0, 0, 1, 1), Rect.from_center_zoom(0.5, 0.5, 1.3)
... )
>>> p.evaluate(0.0)
Rect(x=0.0, y=0.0, w=1.0, h=1.0)
>>> round(p.evaluate(1.0).zoom, 4)
1.3
>>> p.duration_keyframes  # number of waypoints
2
property duration_keyframes: int#

Number of keyframe waypoints (2 for the canonical Start/End).

evaluate(t)[source]#

The viewport Rect at normalized clock time t in [0, 1].

Applies easing to the clock, then linearly interpolates the geometry at the eased progress. Pure and deterministic: no image, no I/O, no frame count. t outside [0, 1] clamps to the nearest end.

Return type:

Rect

Examples

>>> p = BurnsPath.from_start_end(
...     Rect(0, 0, 1, 1), Rect(0, 0, 0.5, 0.5), easing="linear"
... )
>>> p.evaluate(0.5)
Rect(x=0.0, y=0.0, w=0.75, h=0.75)
classmethod from_dict(d)[source]#

Rebuild a BurnsPath from to_dict() output.

Return type:

BurnsPath

Examples

>>> p = BurnsPath.from_start_end(Rect(0, 0, 1, 1), Rect(0, 0, .5, .5))
>>> BurnsPath.from_dict(p.to_dict()) == p
True
classmethod from_start_end(start, end, *, easing='ease-in-out', output_aspect=None)[source]#

The canonical two-rectangle Ken Burns case (Start frame -> End frame).

Return type:

BurnsPath

Examples

>>> BurnsPath.from_start_end(Rect(0, 0, 1, 1), Rect(0, 0, .5, .5))
...
BurnsPath(keyframes=((0.0, Rect(...)), (1.0, Rect(...))), ...)
classmethod push_in(zoom=1.3, *, to=(0.5, 0.5), easing='ease-in-out', output_aspect=None)[source]#

The 90%-case constructor: a slow push from the full image toward to at zoom.

Return type:

BurnsPath

Examples

>>> round(BurnsPath.push_in().evaluate(1.0).zoom, 4)
1.3
reversed()[source]#

Swap start and end (the NLE “Swap Start and End Areas” button).

Mirrors every keyframe’s time about 0.5 and re-sorts, so the motion plays back-to-front. Easing/interp/output_aspect are preserved.

Return type:

BurnsPath

Examples

>>> p = BurnsPath.from_start_end(Rect(0, 0, 1, 1), Rect(0, 0, .5, .5))
>>> p.reversed().evaluate(0.0)
Rect(x=0.0, y=0.0, w=0.5, h=0.5)
to_dict()[source]#

Serialize to the versioned JSON wire format (the cross-language SSOT).

Easing is stored as the original CSS string / 4-tuple. A callable easing cannot be serialized and raises — use a CSS spec for paths that travel.

Return type:

dict[str, Any]

Examples

>>> p = BurnsPath.from_start_end(Rect(0, 0, 1, 1), Rect(0, 0, .5, .5))
>>> d = p.to_dict()
>>> d["version"], d["easing"], len(d["keyframes"])
(1, 'ease-in-out', 2)
exception burns.MoveError[source]#

Bases: ValueError

A move that cannot be resolved into a path.

A plain ValueError subclass, so a consumer that already catches ValueError around its authoring layer keeps working, and so nothing downstream needs to import burns.moves to handle it.

class burns.Rect(x, y, w, h)[source]#

Bases: object

A normalized (x, y, w, h) viewport over a source image.

All four components are fractions of the image in [0, 1] with a top-left origin. (x, y) is the top-left corner of the window; w and h are its width and height. Rect(0, 0, 1, 1) is the whole image.

Examples

>>> full = Rect(0.0, 0.0, 1.0, 1.0)
>>> full.center
(0.5, 0.5)
>>> full.zoom
1.0
>>> Rect.from_center_zoom(0.5, 0.5, 2.0)
Rect(x=0.25, y=0.25, w=0.5, h=0.5)
property aspect: float#

Aspect ratio of the window in normalized image units (w / h).

Note this is not the rendered-pixel aspect ratio unless the image is square — the pixel AR is (w * img_w) / (h * img_h). The render path reconciles the window with the desired output AR via a cover-crop.

property center: tuple[float, float]#

The window’s center (cx, cy) in [0, 1] image units.

clamped()[source]#

Slide the window inside the image without resizing it.

Clamps the top-left corner so the window “rides the wall” at an edge rather than shrinking — shrinking the box would change its aspect ratio and make the rendered frame breathe/stretch. Windows larger than the image in a dimension are centered in that dimension.

Return type:

Rect

Examples

>>> Rect(0.8, 0.0, 0.5, 0.5).clamped()
Rect(x=0.5, y=0.0, w=0.5, h=0.5)
>>> Rect(-0.2, 0.3, 0.4, 0.4).clamped()
Rect(x=0.0, y=0.3, w=0.4, h=0.4)
classmethod from_center_zoom(cx, cy, zoom=1.0, *, aspect=1.0)[source]#

Build a rect from a pan center, a zoom, and a window aspect ratio.

This is the bridge from the older center+magnification mental model to the (x, y, w, h) representation. zoom is window-fraction magnification (1.0 = full image, > 1.0 = zoomed in). aspect is the window’s normalized w / h; the default 1.0 yields an isotropic window (w == h) whose rendered AR equals the source image’s — reproducing the legacy behavior exactly. The result is clamped to stay inside the image.

Return type:

Rect

Examples

>>> Rect.from_center_zoom(0.5, 0.5, 1.0)
Rect(x=0.0, y=0.0, w=1.0, h=1.0)
>>> Rect.from_center_zoom(0.75, 0.5, 2.0)  # off-center zoom-in
Rect(x=0.5, y=0.25, w=0.5, h=0.5)
is_contained()[source]#

True when the window lies wholly inside the image.

Return type:

bool

Examples

>>> Rect(0.1, 0.1, 0.5, 0.5).is_contained()
True
>>> Rect(0.8, 0.0, 0.5, 0.5).is_contained()
False
lerp(other, t)[source]#

Linearly interpolate toward other by t in [0, 1].

Return type:

Rect

Examples

>>> Rect(0, 0, 1, 1).lerp(Rect(0.25, 0.25, 0.5, 0.5), 0.5)
Rect(x=0.125, y=0.125, w=0.75, h=0.75)
to_pixels(img_w, img_h)[source]#

Map the (clamped) window to an integer pixel box (x0, y0, x1, y1).

Clamps first so the box is always inside the raster, then rounds (not truncates) for symmetric integer conversion. The returned box is a half-open crop region suitable for ndarray[y0:y1, x0:x1].

Return type:

tuple[int, int, int, int]

Examples

>>> Rect(0.0, 0.0, 1.0, 1.0).to_pixels(100, 80)
(0, 0, 100, 80)
>>> Rect.from_center_zoom(0.5, 0.5, 2.0).to_pixels(100, 80)
(25, 20, 75, 60)
property zoom: float#

Magnification implied by the window, 1 / max(w, h).

1.0 = the full image; > 1.0 = zoomed in. This is the derived value an FFmpeg zoompan backend consumes; the window-fraction (w, h) is the authoritative, user-facing representation.

class burns.RenderBackend(*args, **kwargs)[source]#

Bases: Protocol

Encode one image + BurnsPath into a video file at output.

Args mirror burns.ken_burns_video() after argument resolution: the image is already a decoded (H, W, C) uint8 array with its pixel size, and the output frame size (out_w, out_h) is already even-snapped. Returns the written path.

burns.choose_move(seed)[source]#

Which concrete move "auto" resolves to for seed.

Public because “auto” is a decision a person may want to see (a UI showing auto → drift_left) and to pin (liking what auto chose and storing that name instead, so it survives a change to the weights).

A pure function of seed alone — not of the image, and not of any position in a sequence. That is what makes a panel’s move survive both a reorder and a swapped still.

Return type:

str

Examples

>>> choose_move(7) == choose_move(7)
True
>>> choose_move(7) in MOVES and choose_move(7) != 'auto'
True
>>> len({choose_move(s) for s in range(60)}) > 1   # not one move forever
True
burns.content_aware_path(img_w, img_h, *, subject=None, faces=(), index=0, output_aspect=None, zoom=1.3, min_zoom=1.05, keep_pad=0.18, mode='auto', easing='ease-in-out')[source]#

A BurnsPath that keeps the subject/faces framed while zooming toward or away from them — the content-aware counterpart of ken_burns_path().

The keep-region is the union of faces if any, else subject (else a centered default). Both start and end crop windows are built to contain that region fully, centered on it; the end zoom is capped so the region never leaves the frame. Windows carry the output pixel-aspect so the render’s cover-crop is a no-op and what you frame is what shows.

Parameters:
  • img_w (int) – source image pixel size (the subject box lives in this space).

  • img_h (int) – source image pixel size (the subject box lives in this space).

  • subject (Optional[tuple[float, float, float, float]]) – normalized (x,y,w,h) of the main subject (e.g. from salient_box()). Optional.

  • faces (Sequence[tuple[float, float, float, float]]) – normalized face boxes; when present they are the keep-region and take priority over subject.

  • index (int) – sequence position; in mode="auto" odd indices push in, even pull out, giving a sequence rhythm (mirrors ken_burns_path).

  • output_aspect (Optional[float]) – the render’s width/height; defaults to the image’s.

  • zoom (float) – target end magnification (capped to keep the region framed).

  • min_zoom (float) – floor magnification for the non-zoomed end.

  • keep_pad (float) – fractional padding around the keep-region.

  • mode (str) – "auto" (alternate by index) · "in" (always toward) · "out" (always away).

Return type:

BurnsPath

Examples

>>> p = content_aware_path(1600, 900, subject=(0.6, 0.55, 0.2, 0.25), index=1)
>>> r0, r1 = p.evaluate(0.0), p.evaluate(1.0)
>>> r1.zoom >= r0.zoom            # index 1 -> push in
True
>>> content_aware_path(100, 100, subject=(0,0,1,1)) == content_aware_path(100, 100, subject=(0,0,1,1))
True
burns.content_aware_path_for(image, *, subject=None, faces=(), faces_detector=None, index=0, output_aspect=None, **kwargs)[source]#

Convenience: derive the subject (via salient_box()) and faces (from faces or faces_detector(image)) straight from image, then call content_aware_path(). image may be a path, PIL image, or ndarray.

Keeps burns dependency-light: pass an LLM/cv2/manual faces_detector when you want face-aware framing; omit it for saliency-only (sky-avoiding) motion.

subject is an explicit keep-region that replaces the saliency estimate — for a caller who already knows what the picture is about (a hand-drawn crop, an upstream detector, a stored authoring decision). It is a named parameter rather than one more key in **kwargs because those are forwarded to content_aware_path(), which already takes subject — so passing it that way raised TypeError: got multiple values two frames down. When given, salient_box() is not called: the override is total, and computing an estimate only to discard it is how a partial override quietly lets saliency back in.

Return type:

BurnsPath

Examples

>>> import numpy as np
>>> a = np.zeros((600, 800, 3), dtype='uint8'); a[80:200, 60:200] = 220
>>> p = content_aware_path_for(a, subject=(0.7, 0.7, 0.2, 0.2), index=1)
>>> r = p.evaluate(1.0)                     # framed on the override,
>>> r.x + r.w / 2 > 0.5 and r.y + r.h / 2 > 0.5   # not the bright corner
True
burns.cubic_bezier(x1, y1, x2, y2)[source]#

A CSS-style cubic-bezier easing f: [0, 1] -> [0, 1].

The curve runs from (0, 0) to (1, 1) with control points (x1, y1) and (x2, y2). Evaluation inverts x(t) for the curve parameter (bisection — robust for any monotone-x curve) then returns y(t).

Return type:

Callable[[float], float]

Examples

>>> linear = cubic_bezier(0.0, 0.0, 1.0, 1.0)
>>> round(linear(0.5), 6)
0.5
>>> round(cubic_bezier(0.42, 0.0, 0.58, 1.0)(0.5), 6)  # ease-in-out
0.5
burns.get_backend(name='pillow')[source]#

Look up a registered backend by name.

Return type:

Callable[..., Path]

Examples

>>> get_backend() is get_backend("pillow")
True
burns.ken_burns_film(panels, *, saveas, fps=30, audio_path=None, codec='libx264', audio_codec='aac', **write_kwargs)[source]#

Render an N-panel Ken Burns film in a single pass.

Each panel is an (image, path, duration_s) triple (burns.PanelInput): image is a path / PIL.Image / np.ndarray, path a BurnsPath, duration_s how long the panel occupies the film. Panels play back-to-back; the camera cuts at panel boundaries but motion never pauses on a static frame within a panel.

A single VideoClip (rather than per-panel render + concatenate) avoids the concat re-encode seam and per-panel tail freeze, and keeps frame generation lazy via one global make_frame(t) closure.

Parameters:
  • panels – iterable of (image, path, duration_s) triples.

  • saveas (str) – output mp4 path (required — a film has no single source image).

  • fps (int) – frame rate.

  • audio_path – optional pre-built track (already matching the film duration); muxed in when supplied. Per-panel audio assembly is the caller’s job — the renderer stays pure visual.

  • codec (str) – forwarded to write_videofile.

  • audio_codec (str) – forwarded to write_videofile.

  • **write_kwargs – forwarded to write_videofile.

Return type:

Path

Returns:

The written mp4 Path.

burns.ken_burns_path(index, *, style='push', zoom=1.1, pan=0.03, easing='ease-in-out', output_aspect=None)[source]#

A deterministic BurnsPath for the index-th image of a sequence.

Maps intent to geometry so a sequence of images gets cohesive, non-repetitive motion without hand-authoring rectangles. Per-index deterministic: identical args always return an identical path. Duration is not part of the path — it is a render-time parameter (pass it to burns.ken_burns_video() / burns.ken_burns_film()).

Two styles:

  • style="push" (default) — the “cinematic push”: one slow zoom toward an off-center focal point. Odd indices push in, even indices pull out, so a sequence has visual rhythm without changing direction within a shot. The focal direction rotates through compass octants per index.

  • style="drift" — pure horizontal pan at a constant zoom, alternating direction per index (odd drifts right, even left). zoom is ignored; drift derives its own zoom from pan so the slide is visible.

Parameters:
  • index (int) – the image’s 1-based position in the sequence.

  • style (str) – "push" (default) or "drift".

  • zoom (float) – the zoomed-end magnification (> 1.0) for "push".

  • pan (float) – focal-point offset ("push") or total horizontal travel ("drift"), in [0, 1] image units.

  • easing (Union[str, Callable[[float], float], Sequence[float]]) – CSS timing function or callable. Default "ease-in-out" — the cinematic slow-in/slow-out. Pass "linear" for constant velocity.

  • output_aspect (Optional[float]) – aspect ratio the render should fill (None = match image).

Return type:

BurnsPath

Returns:

A BurnsPath (two keyframes — Start and End).

Examples

>>> ken_burns_path(1).evaluate(0.0)  # odd: push in, starts full
Rect(x=0.0, y=0.0, w=1.0, h=1.0)
>>> ken_burns_path(2).evaluate(1.0)  # even: pull out, ends full
Rect(x=0.0, y=0.0, w=1.0, h=1.0)
>>> ken_burns_path(3) == ken_burns_path(3)  # deterministic
True
burns.ken_burns_video(image, path=BurnsPath(keyframes=((0.0, Rect(x=0.0, y=0.0, w=1.0, h=1.0)), (1.0, Rect(x=0.11538461538461542, y=0.11538461538461542, w=0.7692307692307692, h=0.7692307692307692))), easing='ease-in-out', interp='linear', output_aspect=None, version=1), *, duration=2.0, fps=30, saveas=None, output_size=None, backend='pillow', codec='libx264', audio_codec='aac', **write_kwargs)[source]#

Render one image into a pan/zoom video from a BurnsPath.

Parameters:
  • image – path / PIL.Image / np.ndarray.

  • path (BurnsPath) – the motion spec. Default is a 2-second standard push-in.

  • duration (float) – clip length in seconds (the path’s clock is normalized, so duration is supplied here, not baked into the path).

  • fps (int) – frames per second.

  • saveas (str | None) – output path. Default: the source image’s name with _kenburns appended (auto-uniquified so an existing file is not overwritten). Required-ish when image has no source path — falls back to a temp file.

  • output_size (tuple[int, int] | None) – explicit (width, height) (even-snapped). If omitted and path.output_aspect is set, the size is derived from it; otherwise the source image’s size is used.

  • backend (str) – registered render backend name (default "pillow").

  • codec (str) – forwarded to write_videofile.

  • audio_codec (str) – forwarded to write_videofile.

  • **write_kwargs – forwarded to write_videofile.

Return type:

Path

Returns:

The output Path.

Examples

>>> ken_burns_video("photo.jpg")  # 2s push-in
>>> from burns import ken_burns_path
>>> ken_burns_video(
...     "photo.jpg", ken_burns_path(1), duration=5.0, saveas="out.mp4"
... )
burns.move_kind(move)[source]#

How move is grouped: "zoom", "drift", "static", "select".

For a UI that wants a compass of directions separately from the rest, read off the same table resolve_move() dispatches on.

Return type:

str

Examples

>>> move_kind("drift_left"), move_kind("hold"), move_kind("auto")
('drift', 'static', 'select')
>>> sorted(m for m in MOVES if move_kind(m) == 'drift')
['drift_down', 'drift_left', 'drift_right', 'drift_up']
burns.parse_easing(spec='ease-in-out')[source]#

Resolve an easing spec to a callable f: [0, 1] -> [0, 1].

Accepts:

  • a CSS name ("linear", "ease", "ease-in", "ease-out", "ease-in-out");

  • a CSS "cubic-bezier(x1, y1, x2, y2)" string;

  • a 4-element sequence (x1, y1, x2, y2);

  • any callable, returned unchanged.

Examples

>>> parse_easing("linear")(0.3)
0.3
>>> round(parse_easing("cubic-bezier(0,0,1,1)")(0.7), 6)
0.7
>>> parse_easing(lambda t: t * t)(0.5)
0.25
Return type:

Callable[[float], float]

burns.register_backend(name, backend)[source]#

Register a render backend under name (open-closed extension point).

Return type:

None

burns.resolve_move(move, *, image, aspect, zoom=1.18, focus=None, seed=0, easing='ease-in-out', on_aspect_mismatch='raise', content_box=None)[source]#

Resolve an authored camera intent against image into a path.

Parameters:
  • move (Union[str, BurnsPath, Mapping[str, Any]]) – a name from MOVES, or an explicit BurnsPath / its to_dict() payload. The two are the two front doors on this one path: a stored panel that carries both a named move and a hand-corrected override passes the override here and the choice is made in one place.

  • image (Any) – the still the move is framed against — a path, PIL.Image or ndarray. Required, and read every time: the framing depends on what is in the picture, so swapping the still must re-frame. Do not cache the returned path against the intent.

  • aspect (Optional[float]) – the delivered width / height. None means “match the image”. Required rather than defaulted, because a cut has a delivery size and quietly framing for the image’s aspect instead is a cover-crop nobody asked for.

  • zoom (float) –

    the end magnification. A pure function of the arguments — never jittered by seed or by a position — but **bounded at both ends**, so the number you pass is a request:

    • capped so the padded keep-region stays framed. A focus filling the frame therefore yields a nearly static move; the fix is a tighter focus, not a bigger zoom. A keep-region that saliency found covering DIFFUSE_KEEP_AREA or more of the picture is only a centre hint and does not cap: on a detailed photograph it covers nearly everything, and capping on it made every zoom render the same ~1.07.

    • floored. push_in / pull_out inherit content_aware_path’s min_zoom + 0.02 (about 1.07), so a barely-there 1.02 push renders as 1.07; hold has no such floor and honours 1.02 exactly. A drift is floored to whatever leaves DRIFT_MIN_ROOM of travel.

  • focus (Any) – an explicit keep-region overriding the saliency estimate. A Rect, a normalized (x, y, w, h) tuple, or any object with .x/.y/.w/.h. When given, salient_box() is not called at all.

  • seed (int) – chooses which move "auto" becomes, and nothing else. Mint it once per panel and store it; never derive it from a position, which is the defect this parameter exists to remove.

  • easing (Union[str, Callable[[float], float], Sequence[float]]) – CSS timing function or callable. A callable is fine here but cannot be serialized — see BurnsPath.to_dict(). Applies to a named move; an explicit path carries its own.

  • content_box (Any) – the part of image that is picture, as a normalized (x, y, w, h) (same forms as focus). Saliency runs inside it, so the blurred fill or bars a caller composited around a still do not read as subject. Ignored when focus is given.

  • on_aspect_mismatch (str) – what to do when an explicit path was authored for a different output_aspect than the one being rendered. "raise" (default) refuses, because honouring the stored rectangles at another shape cover-crops a framing somebody chose by hand. "refit" rebuilds each keyframe at the new aspect, keeping what the author actually chose — where the camera looks at each instant, and how far in it is — and changing only the window shape. A cut in a second aspect is a real workflow (a vertical edit of a landscape film), and a panel carries one path, not one per delivery, so “author a second path” is not a remedy a caller can take; "refit" is.

Return type:

BurnsPath

Returns:

A BurnsPath. Duration is not part of it; pass that to the renderer.

Raises:

MoveError – an unknown move name, a malformed focus, a non-positive zoom or aspect, or — under the default on_aspect_mismatch="raise" — an explicit path whose output_aspect contradicts aspect.

Note

image is opened for every named move. For an explicit path it is read only when refitting, since resolved geometry needs no picture.

Examples

>>> import numpy as np
>>> img = np.zeros((600, 800, 3), dtype='uint8'); img[300:460, 500:700] = 230

A named move is a decision, so the seed does not touch it:

>>> resolve_move("push_in", image=img, aspect=16 / 9, seed=1) == \
...     resolve_move("push_in", image=img, aspect=16 / 9, seed=999)
True

"auto" is where the seed does its one job:

>>> a = resolve_move("auto", image=img, aspect=16 / 9, seed=3)
>>> a == resolve_move("auto", image=img, aspect=16 / 9, seed=3)
True

A drift travels — and drift_right ends further right:

>>> d = resolve_move("drift_right", image=img, aspect=16 / 9)
>>> d.evaluate(1.0).x > d.evaluate(0.0).x
True

An explicit path is the other front door, returned as authored:

>>> from burns import BurnsPath, Rect
>>> hand = BurnsPath.from_start_end(
...     Rect(0, 0, 1, 1), Rect(0.1, 0.1, 0.6, 0.6), output_aspect=16 / 9
... )
>>> resolve_move(hand.to_dict(), image=img, aspect=16 / 9) == hand
True

Rendering it at another delivery refuses by default, and "refit" keeps the authored look while changing the window shape:

>>> resolve_move(hand, image=img, aspect=9 / 16)
Traceback (most recent call last):
    ...
burns.moves.MoveError: ...
>>> v = resolve_move(hand, image=img, aspect=9 / 16,
...                  on_aspect_mismatch="refit")
>>> v.output_aspect == 9 / 16
True
>>> before = [round(c, 4) for c in hand.evaluate(1.0).center]
>>> after = [round(c, 4) for c in v.evaluate(1.0).center]
>>> before == after          # the author's framing, at the new shape
True
burns.salient_box(image, *, downscale=320, threshold_pct=72.0, trim_pct=4.0, pad=0.05, min_size=0.35)[source]#

Estimate the salient (high-detail) region of image as a normalized box.

Uses gradient magnitude: flat regions (sky, walls, water) have low gradient and fall away, so the bounding box of the high-gradient pixels tracks the subject. Robust to outliers via percentile trimming. Falls back to a centered box when the image is too uniform to decide.

Return type:

tuple[float, float, float, float]

Examples

>>> import numpy as np
>>> a = np.zeros((100, 100), dtype='uint8'); a[60:90, 40:70] = 255
>>> x, y, w, h = salient_box(a, min_size=0.0, pad=0.0)
>>> 0.3 < x < 0.45 and 0.55 < y < 0.65   # box around the bright square
True

Modules

backends

Pluggable single-clip render backends — the open-closed seam.

content

Content-aware Ken Burns: choose crop windows that keep the subject (and any detected faces) framed, and avoid drifting over empty regions like sky.

easing

Timing functions (easing) for Ken Burns motion.

moves

The named camera moves, and the one resolver that expands one into a path.

path

The render-agnostic Ken Burns motion spec: BurnsPath.

rect

The Ken Burns viewport rectangle — the render-agnostic geometric atom.

render

Ken Burns renderers — turn still images into pan/zoom video.