burns.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.
Two layers, usable together or apart:
salient_box()— a zero-dependency (numpy + Pillow, already required) estimate of the busy / high-detail region of an image, so motion follows the subject instead of blank sky or plain ground. Good default when you have no detector.content_aware_path()— the pure geometry: given the image size and a keep-region (the union of face boxes if any, else a subject box), build aBurnsPathwhose start/end windows keep that region fullyframed while zooming toward or away from it. This is the reusable, testable core; it touches no pixels.
Detection is injected, not built in: faces come from a caller-supplied
faces_detector (an LLM, an ONNX/cv2 model, or manual boxes), so this module
stays dependency-light and deterministic. Boxes everywhere are normalized
(x, y, w, h) in [0, 1] with a top-left origin — the same convention as
Rect.
The geometry content_aware_path() is built from is exported rather than
inlined — axis_maxima(), keep_window(), fit_zoom() and
pad_box(). burns.moves builds its static and drift moves out of the
same four, so “a window of this zoom, at this output aspect, centred here” has
one implementation rather than one per move.
Module Attributes
given an image, return normalized face boxes (empty if none). |
|
Where the keep-region defaults to when nothing is known about the picture: a generous centered box. |
|
Fractional breathing room around the keep-region. |
Functions
|
Open / convert |
|
The largest |
|
A |
|
Convenience: derive the subject (via |
|
The largest zoom at which |
|
The window of |
|
Grow |
|
Estimate the salient (high-detail) region of |
- burns.content.DFLT_KEEP_BOX: tuple[float, float, float, float] = (0.15, 0.15, 0.7, 0.7)#
Where the keep-region defaults to when nothing is known about the picture: a generous centered box. Not the full frame — that would give the framing nothing to hold on to and every zoom would be capped to a standstill.
- burns.content.DFLT_KEEP_PAD: float = 0.18#
Fractional breathing room around the keep-region. Framing a subject with its own bounding box hard against the window edge reads as a crop, not a frame.
- burns.content.FacesDetector#
given an image, return normalized face boxes (empty if none).
- Type:
A detector
alias of
Callable[[Any],Sequence[tuple[float,float,float,float]]]
- burns.content.as_pil(image)[source]#
Open / convert
image(path,PIL.Image, or ndarray) to a PIL image.The single entry point for “the caller gave me some kind of image”. Used by every function here that needs pixels or a size, so a new accepted input type is added in one place — and so a path is opened once per call rather than once per function that wants it.
- burns.content.axis_maxima(img_w, img_h, output_aspect=None)[source]#
The largest
(w, h)window, in image units, matchingoutput_aspect.At zoom
1.0this is the window the render fills; one of the two is always1.0(the axis that limits) and the other is what the output aspect leaves of the opposite one.output_aspect=Nonemeans “match the image”, so both are1.0.Examples
>>> axis_maxima(1600, 900) # aspect follows the image (1.0, 1.0) >>> [round(v, 4) for v in axis_maxima(1600, 1200, 16 / 9)] # 4:3 -> 16:9 [1.0, 0.75] >>> [round(v, 4) for v in axis_maxima(1080, 1920, 16 / 9)] # portrait [1.0, 0.3164]
- burns.content.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
BurnsPaththat keeps the subject/faces framed while zooming toward or away from them — the content-aware counterpart ofken_burns_path().The keep-region is the union of
facesif any, elsesubject(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. fromsalient_box()). Optional.faces (
Sequence[tuple[float,float,float,float]]) – normalized face boxes; when present they are the keep-region and take priority oversubject.index (
int) – sequence position; inmode="auto"odd indices push in, even pull out, giving a sequence rhythm (mirrorsken_burns_path).output_aspect (
Optional[float]) – the render’swidth/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:
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.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 (fromfacesorfaces_detector(image)) straight fromimage, then callcontent_aware_path().imagemay be a path, PIL image, or ndarray.Keeps burns dependency-light: pass an LLM/cv2/manual
faces_detectorwhen you want face-aware framing; omit it for saliency-only (sky-avoiding) motion.subjectis 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**kwargsbecause those are forwarded tocontent_aware_path(), which already takessubject— so passing it that way raisedTypeError: got multiple valuestwo 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:
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.content.fit_zoom(img_w, img_h, *, keep, output_aspect=None)[source]#
The largest zoom at which
keepstill fits inside the window.keepis taken as given — pad it withpad_box()first if you want the subject framed rather than touching the edges.- Return type:
Examples
>>> round(fit_zoom(1000, 1000, keep=(0.25, 0.25, 0.5, 0.5)), 4) 2.0 >>> round(fit_zoom(1000, 1000, keep=(0.0, 0.0, 1.0, 1.0)), 4) # fills it 1.0
- burns.content.keep_window(img_w, img_h, *, center, zoom, output_aspect=None)[source]#
The window of
zoom, atoutput_aspect, centred oncenter.Clamped inside the image by sliding, never by resizing (see
clamped()) — shrinking would change the window’s aspect and make the rendered frame breathe.- Return type:
Examples
>>> keep_window(1000, 1000, center=(0.5, 0.5), zoom=2.0) Rect(x=0.25, y=0.25, w=0.5, h=0.5) >>> keep_window(1000, 1000, center=(0.95, 0.5), zoom=2.0) # rides the wall Rect(x=0.5, y=0.25, w=0.5, h=0.5)
- burns.content.pad_box(box, pad=0.18)[source]#
Grow
boxbypadof its own size on every side, clipped to the image.Examples
>>> [round(v, 4) for v in pad_box((0.4, 0.4, 0.2, 0.2), 0.25)] [0.35, 0.35, 0.3, 0.3] >>> pad_box((0.0, 0.0, 1.0, 1.0), 0.5) # already the whole image (0.0, 0.0, 1.0, 1.0)
- burns.content.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
imageas 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.
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