0.7 regression where ContentDetector discards cuts that fall short of `min_scene_len` at 29.97/59.94 fps
PySceneDetect: In a 29.97/59.94 fps video, ContentDetector discards cuts that are separated by a number of frames equal to min_scene_len.
Link: https://github.com/Breakthrough/PySceneDetect/issues/577
- Repo: Breakthrough/PySceneDetect (approx. 5.2k stars, default branch
main, HEAD81c414c, version 0.7.1) - Type: Issue (bug report, "Python API" template). Since it has fewer than 10,000 stars, I did not prepare a pull request, and there are no code changes in the clone.
Background
worker-prep-vision splits broadcast video (29.97/59.94 fps) into shots using ContentDetector(threshold, min_scene_len=round(min_sec * fps)). If prep_min_sec=1.0, then min_scene_len is 30 (29.97 fps) or 60 (59.94 fps) frames.
Starting with version 0.7, cuts that fall “exactly min_scene_len frames after the previous cut” based on these values are discarded. The API default of 15 also falls under this category. This behavior was normal in version 0.6.7.
Cause
scenedetect/detector.py’s FlashFilter (lines 179–181, 194–196).
- During the VFR support work in version 0.7 (#540),
FlashFilterwas changed to compare time (in seconds) instead of the number of frames. - The integer frame duration is converted to seconds using
self._filter_length / float(frame_rate), then compared to the elapsed time(timecode - self._last_above)as a float. - For NTSC-based systems,
float(frame_rate)is not precise, so the quotient may be 1 ulp larger than the true value. At 30,000/1,001 fps, 15 frames have an elapsed time of0.5005and a reference value of0.5005000000000001, so>=evaluates to False. - Based on the exact PTS (PyAV) standard, N = 3, 6, 12, 15, 24, 25, 30, 39, 48, 50, 59, and 60 always apply (Verified for N ≤ 60; 29.97 and 59.94 are identical).
- Since the OpenCV backend rounds positions to the nearest microsecond, if the frame duration is not an integer multiple of the frame rate (30, 23.976, 29.97, 59.94) where the frame rate is not an integer multiple of 3, the same issue occurs because the frame is 1 µs short depending on the starting position. Example: At 29.97 fps, OpenCV discards the clip 17 frames after
min_scene_len=17, while PyAV retains it. HistogramDetector,HashDetector,AdaptiveDetector, andThresholdDetectorhave no impact when comparing frame numbers withFrameTimecode >= int.
Reproduction Method
This can be reproduced without video (repro_min.py). Create a stream at 30000/1001 fps that exactly matches the position returned by the PyAV backend, and insert a cut signal every 15 frames at FlashFilter(length=15).
gap.frame_num : 15
gap.seconds : 0.5005
15 / float(fps) : 0.5005000000000001
gap >= 15 / float(fps) : False <- what FlashFilter evaluates
gap >= 15 : True <- what the other detectors evaluate
MERGE cuts every 15 frames -> []
SUPPRESS cuts every 15 frames -> [30, 60, 90, 120, 150, 180, 210, 240, 270]
The expected values for both modes are 15, 30, 45, ..., 285, and version 0.6.7 of FlashFilter returns these values correctly.
I also verified this using actual files (repro.py). I used ffmpeg to create 150-frame clips with shot lengths of exactly 15 frames at five different frame rates and detected them using min_scene_len=15.
| Frame Rate | ContentDetector (HEAD) | HistogramDetector / AdaptiveDetector (HEAD) | ContentDetector (0.6.7) |
|---|---|---|---|
| 25, 30, 24000/1001 | 9 cuts | 9 cuts | 9 cuts |
| 30000/1001 | 0 cuts | 9 cuts | 9 cuts |
| 60,000/1001 | 0 frames | 9 frames | 9 frames |
The HEAD version yields the same results for both the OpenCV and PyAV backends. The 0.7 and 0.6.7 versions on PyPI were run only with the OpenCV backend, and version 0.7 produces the same results as the HEAD version. The HistogramDetector in version 0.6.7 does not output the first frame (15), resulting in 8 frames, but this is unrelated to the current issue.
Verification
- The same script was run in three environments: HEAD
81c414c(editable installation), PyPIscenedetect==0.7, andscenedetect==0.6.7. The original output isrepro.txt. - I generated a list of N values affected by frame rate and backend using
repro_timing.py. - Items not tested: the entire pytest suite in the repo (requires test video resources),
TransNetV2Detector(though it likely behaves the same asFlashFilter, it was not executed). - The proposed fix was summarized in a single paragraph in the main text (calculate integer length precisely using
Fractionor compare by frame count; allow for sub-frame tolerance in OpenCV’s microsecond rounding). Since #540 intentionally changed the unit to time, the maintainer’s judgment is required.
Duplicate Check
- #540 (merged): A PR that changed
FlashFilterto a time unit. This issue concerns the float conversion problem introduced at that time. - #408, #571 (merged), #557 (closed): An issue where
AdaptiveDetectorwas checkingmin_scene_lenbased on the current frame. Different detector, different cause. - #569 (Open): Issue where
frame_numis an approximate value in VFR. This case involves CFR. - #477 (Open): Question regarding the difference between the CLI default of 0.6s and the API default of 15 frames.
- #539 (Open): Issue in 0.6.7 where the last scene is 1 frame long.
- Search terms: min_scene_len, min-scene-len, FlashFilter, filter_secs, floating point, off-by-one, 29.97, NTSC, 30000/1001, time-based units, ContentDetector regression, missing cuts, fewer scenes 0.7