Skip to content

Commit a42aeba

Browse files
committed
[bugfix] Ensure ThresholdDetector produces deterministic boundary rounding
1 parent 2726346 commit a42aeba

3 files changed

Lines changed: 13 additions & 6 deletions

File tree

scenedetect/detectors/threshold_detector.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,17 @@ def process_frame(
145145
# Only add the scene if min_scene_len frames have passed.
146146
if (timecode - self.last_scene_cut) >= self.min_scene_len:
147147
# Just faded into a new scene, compute timecode for the scene
148-
# split based on the fade bias.
148+
# split based on the fade bias. Use frame-number arithmetic so the
149+
# result is identical across backends — float seconds + framerate
150+
# multiplication can land on a .5 rounding boundary and tip the
151+
# frame number by 1 between PyAV (sub-microsecond PTS) and OpenCV
152+
# (millisecond-truncated CAP_PROP_POS_MSEC).
149153
f_out = self.last_fade["frame"]
150-
duration = (timecode - f_out).seconds
151-
split_seconds = f_out.seconds + (duration * (1.0 + self.fade_bias)) / 2.0
152-
cuts.append(FrameTimecode(split_seconds, fps=timecode))
154+
duration_frames = timecode.frame_num - f_out.frame_num
155+
split_frame_num = f_out.frame_num + round(
156+
duration_frames * (1.0 + self.fade_bias) / 2.0
157+
)
158+
cuts.append(FrameTimecode(split_frame_num, fps=timecode))
153159
self.last_scene_cut = timecode
154160
self.last_fade["type"] = "in"
155161
self.last_fade["frame"] = timecode

tests/test_detectors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def get_fade_in_out_test_cases():
139139
detector=ThresholdDetector(),
140140
start_time=0,
141141
end_time=500,
142-
scene_boundaries=[0, 15, 198, 376],
142+
scene_boundaries=[0, 15, 198, 377],
143143
),
144144
id="threshold_testvideo_default",
145145
),
@@ -177,7 +177,7 @@ def get_fade_in_out_test_cases():
177177
),
178178
start_time=0,
179179
end_time=250,
180-
scene_boundaries=[0, 42, 125, 209],
180+
scene_boundaries=[0, 42, 126, 209],
181181
),
182182
id="threshold_fades_ceil",
183183
),

website/pages/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,7 @@ Although there have been minimal changes to most API examples, there are several
681681
- [feature] `save-edl` accepts a new `--start-timecode`/`-s` flag (SMPTE `HH:MM:SS:FF` or 8-digit `HHMMSSFF`) to stamp every event with a custom start timecode so generated EDLs align with the source media's on-screen timecode [#515](https://github.com/Breakthrough/PySceneDetect/issues/515)
682682
- [bugfix] Fix floating-point precision error in `save-otio` output where frame values near integer boundaries (e.g. `90.00000000000001`) were serialized with spurious precision
683683
- [bugfix] Add mitigation for transient `OSError` in the MoviePy backend as it is susceptible to subprocess pipe races on slow or heavily loaded systems [#496](https://github.com/Breakthrough/PySceneDetect/issues/496)
684+
- [bugfix] `detect-threshold` cut frame numbers are now backend-deterministic; previously the cut could differ by 1 frame between PyAV and OpenCV when the fade midpoint landed on a `.5` rounding boundary (PyAV uses sub-microsecond PTS, OpenCV uses millisecond-truncated `CAP_PROP_POS_MSEC`)
684685
- [refactor] Remove deprecated `-d`/`--min-delta-hsv` option from `detect-adaptive` command
685686

686687
### API Changes

0 commit comments

Comments
 (0)