Find scene cuts
Give it a video. You get the times and frames of its cuts, any softer transitions worth a look, a shot table (first and last frame per shot) and a contact sheet of every shot's opening frame. The scene score is ffmpeg's measure of how different a frame is from the one before it. Hard cuts score high. Dissolves and cuts between similar shots score low and need a low threshold. Read the scores. A fixed threshold alone is not enough.
Inputs
The video, and its frame rate and exact frame count from
ffprobe-inspect-media. The shot table is in the video's own frames.
Run it
1. Score the frames. Every frame's score is written to a text file in the project folder. Scaling to 320 px first makes it fast and ignores fine noise:
exec ffmpeg -hide_banner -v error -i <video> -an -vf "scale=320:-2,select='gte(scene,0)',metadata=print:file=scores.txt" -f null -
When you only need the hard cuts, print just those:
exec ffmpeg -hide_banner -v error -i <video> -an -vf "scale=320:-2,select='gt(scene,0.15)',metadata=print:file=-" -f null -
Each hit is a pts_time:<seconds> line (the first frame of the new shot)
followed by lavfi.scene_score=<score>. To scan only a window, put -ss <start> -t <length> before -i. The times then start at 0, so add the start back.
2. Read the scores into a shot table. Write
references/scene-scores.md's script into the project folder as
scene_scores.py and run it:
exec python scene_scores.py scores.txt --fps 24 --frames 484
exec python scene_scores.py scores.txt --fps 24 --frames 484 --hard 0.3
It prints cuts (over --hard, default 0.15), soft (scores between 0.015 and
--hard, the dissolve candidates) and shots. Cuts under 0.4 s apart count
once, as a flash.
3. Look. Make a contact sheet of every shot's opening frame and check it:
exec ffmpeg -hide_banner -v error -i <video> -an -vf "select='eq(n\,0)+gt(scene\,0.15)',scale=320:-2,tile=6x4:padding=4" -fps_mode vfr cuts_sheet_%02d.png
Look at the soft candidates the same way, with frames taken either side of them.
Reading the scores
| Score | What it usually is |
|---|---|
| 0.4–0.8 | A hard cut. Real cuts on a 24 fps spot all scored above ~0.39. |
| 0.15–0.4 | Usually a cut between similar shots, but sometimes fast motion inside a shot. One 0.15 hit on a spot was motion, not a cut. Look at it. |
| 0.02–0.06 | Soft cuts and dissolves. A scene > 0.25 pass misses them. |
| ~0.03–0.05 | Also a camera move or a flicker on a screen in frame. These overlap with dissolves, so only looking at the frames separates them. |
- 0.15 is the threshold for cutting a short spot into shots. Confirm every hit on the sheet.
- 0.3 is the threshold for cutting a long video into shots for a script. Fewer false hits, and anything it misses usually doesn't change the story.
- 0.015, read by eye, is for a finished cut where titles or graphics must land on every shot change, dissolves included.
Then
- Pull the first and last stills of a shot by frame number:
exec ffmpeg -v error -i <video> -vf "select=eq(n\,<frame>)" -frames:v 1 <shot>_first.png. - Write the shot table (shot, first frame, last frame) into the job's notes, so every later step uses the same frame numbers.
- Cut exact-length shot clips with
ffmpeg-frame-exact-trim, not with-tfrom these times.
Check it
The shots run without gaps from frame 0 to the last frame. Their lengths add up to the frame count. Every shot on the contact sheet is a different picture from the one before.


