Give it a video, several videos, or a set of images; get one labelled sheet to look at
instead of dozens of separate frames. Every label is printed by Pillow, so it works
whatever the local ffmpeg was built with. For a strip that finds where a take's action
starts at a frame's precision, ffmpeg-frame-exact-trim (Picking a late start) is the
quicker one-liner; for the frames of a compositor spec rather than a rendered file,
take stills with hn4a-compositor and sheet them with images.
Inputs
- A video (
videoseeks to each frame it shows, so length costs little; given a still image it sheets that image), several videos forrows, or image files. - What you need to see: the whole thing at even steps, given moments, or the first, middle and last frame of each take.
Run it
Write references/contact_sheet.md's script into the job's scripts/ as
contact_sheet.py, unchanged:
exec("python", ["scripts/contact_sheet.py", "video", "work/sheets/take1.jpg", "source/take1.mp4", "--every", "6", "--cols", "5"], cwd="<job>")
| Command | Gives | Use it to |
|---|---|---|
video OUT V [--count 20] | 20 frames first to last, labelled mm:ss.ss fN | see a clip or a render at a glance |
video OUT V --every 6 | a frame every 6 s | watch a long recording; pick moments |
video OUT V --at 2.5,8,11.5 | the frames at those times | check cues, a scene per time |
video OUT V --fractions 0,.33,.66,1 | frames at fractions of the length, the last one the last frame | check a generated shot's start, middle and end |
video OUT V --at 4.2 --cols 1 --width 0 --grid 50 | one full-size frame with a labelled 50 px grid | read a box, a zoom target or four corners off a frame |
video OUT shot.png --cols 1 --width 0 --grid 50 | a still image (a screenshot, a picked still) at full size with the grid | read boxes off a screenshot, with no movie made first |
rows OUT V1 V2 … [--count 3] | one row per video, frames first to last by frame number | compare takes, models or shots side by side |
images OUT I1 I2 … [--cols 4] | the images, labelled with name and size, see-through ones over grey | check stills for baked words; review a site's images and logo sizes |
--cols and --width (px per tile; 0 keeps the source size) shape the sheet. Write
sheets to work/sheets/ and look at every one you make: a sheet is for you to read,
not to send.
What you get
A PNG (or a JPEG for a .jpg name), and its size printed. Tiles keep their aspect;
labels sit above each tile, frame numbers in yellow on rows.
Tips and traps
- A seek returns the first frame at or after its time, so a frame taken "at" 0.77 s in 25 fps footage is frame 20 (0.80 s), not 19, and a seek at a clip's very end returns nothing. The script seeks half a frame early and labels the frame it actually shows; times you pass are snapped to the nearest frame.
rowspicks frames by number, decoding the whole file: exact, but read a long recording withvideo --everyinstead.- Read boxes off a full-size gridded frame, never a thumbnail: on a screen recording, highlight boxes measured on a contact sheet came out a row too high.
- See-through images (logos, cutouts) are put on grey, so white and black art both show; the size in the label is the file's own, which is how to tell a 120 px logo from a usable one.
- Variable-frame-rate screen recordings: the frame number is the nominal one (time × the stream's rate); trust the time.
Check it
Look at the sheet. On a clip with burned-in frame numbers, every tile's label matched the number drawn in its frame.


