Skip to content

Commit 81c414c

Browse files
committed
[backends] Add workaround for #575 to VideoStreamCv2
1 parent b0afeca commit 81c414c

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

‎scenedetect/backends/opencv.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@
5454
)
5555

5656

57+
def _get_capture_api(path: str) -> int:
58+
"""API preference to use when opening `path` with `cv2.VideoCapture`. This exists as a
59+
workaround for https://scenedetect.com/issue/575, where certain versions of the opencv-python
60+
package cause intermittent segfaults on exit. The issue only happens on Windows when opening
61+
PNG image sequences.
62+
63+
By default OpenCV resolves any capture backend by probing for compatibility, and tries FFmpeg
64+
first. Usually this fails, and it falls back gracefully to CAP_IMAGES. However, this also seems
65+
to cause issues on interpreter exit, as the stack trace implicates something in the ffmpeg DLL.
66+
"""
67+
is_local = not any(identifier in path for identifier in ("://", " ! "))
68+
if os.name == "nt" and is_local and path.lower().endswith(".png"):
69+
return cv2.CAP_IMAGES
70+
return cv2.CAP_ANY
71+
72+
5773
def _get_aspect_ratio(cap: cv2.VideoCapture, epsilon: float = 0.0001) -> float:
5874
"""Display/pixel aspect ratio of the VideoCapture as a float (1.0 represents square pixels)."""
5975
# Versions of OpenCV < 3.4.1 do not support this, so we fall back to 1.0.
@@ -326,8 +342,10 @@ def _open_capture(self, frame_rate: FrameRate | None = None):
326342
if self._path_or_device < 0:
327343
raise ValueError("Invalid/negative device ID specified.")
328344
input_is_video_file = False
345+
capture_api = cv2.CAP_ANY
329346
else:
330347
assert isinstance(self._path_or_device, str)
348+
capture_api = _get_capture_api(self._path_or_device)
331349
input_is_video_file = not any(
332350
identifier in self._path_or_device
333351
for identifier in NON_VIDEO_FILE_INPUT_IDENTIFIERS
@@ -337,7 +355,7 @@ def _open_capture(self, frame_rate: FrameRate | None = None):
337355
if input_is_video_file and not os.path.exists(self._path_or_device):
338356
raise OSError("Video file not found.")
339357

340-
cap = cv2.VideoCapture(self._path_or_device)
358+
cap = cv2.VideoCapture(self._path_or_device, capture_api)
341359
if not cap.isOpened():
342360
raise VideoOpenFailure(
343361
"Ensure file is valid video and system dependencies are up to date.\n"

‎tests/test_backend_opencv.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
For VideoStream tests that validate conformance, see test_video_stream.py.
1818
"""
1919

20+
import os
21+
2022
import cv2
2123
import pytest
2224

@@ -39,6 +41,27 @@ def test_open_image_sequence(test_image_sequence: str):
3941
assert sequence.position == 29
4042

4143

44+
@pytest.mark.skipif(os.name != "nt", reason="Workaround only applies on Windows.")
45+
def test_open_png_sequence_skips_ffmpeg_probe(test_image_sequence: str, monkeypatch):
46+
"""PNG inputs must be opened with `cv2.CAP_IMAGES` on Windows because certain versions of the
47+
opencv-python 5.x package cause intermittent segfaults on exit.
48+
49+
See https://scenedetect.com/issue/575 for details."""
50+
calls = []
51+
real_capture = cv2.VideoCapture
52+
53+
def spy(*args):
54+
calls.append(args)
55+
return real_capture(*args)
56+
57+
monkeypatch.setattr(cv2, "VideoCapture", spy)
58+
sequence = VideoStreamCv2(test_image_sequence, frame_rate=25.0)
59+
sequence.reset()
60+
assert calls == [(test_image_sequence, cv2.CAP_IMAGES)] * 2
61+
assert sequence.capture.getBackendName() == "CV_IMAGES"
62+
assert sequence.read() is not False
63+
64+
4265
def test_capture_adapter(test_movie_clip: str):
4366
"""Test that the VideoCaptureAdapter works with SceneManager."""
4467
cap = cv2.VideoCapture(test_movie_clip)

‎website/pages/changelog.md‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,5 @@ Development
794794
- [api] Scene-list output functions for EDL, FCPXML, FCP7 XML, and OTIO now accept an open text file handle, a string path, or a `pathlib.Path`; paths are opened and closed automatically [#567](https://github.com/Breakthrough/PySceneDetect/issues/567)
795795
- [feature] Add `--min-out-length`/`min_out_length` to `detect-threshold`/`ThresholdDetector`, which ignores fades that stay below the threshold for less than the given duration [#278](https://github.com/Breakthrough/PySceneDetect/issues/278)
796796
- [improvement] `detect-threshold` now ignores fade-outs shorter than `0.1s` by default. Use `--min-out-length 0` to restore the previous behavior. The API default remains `0` [#278](https://github.com/Breakthrough/PySceneDetect/issues/278)
797-
798-
## PySceneDetect 0.8 (TBD)
799-
800797
- [feature] Added `save-keyframes` command to export detected cuts using `# keyframe format v1` for Aegisub-compatible tools [#534](https://github.com/Breakthrough/PySceneDetect/issues/534). Frame numbers are currently approximate for VFR input [#569](https://github.com/Breakthrough/PySceneDetect/issues/569)
798+
- [bugfix] Fix intermittent segfault on exit. Isolated to Windows builds with OpenCV 5.x when opening PNG image sequences with the `VideoStreamCv2` backend. [#575](https://github.com/Breakthrough/PySceneDetect/issues/575)

0 commit comments

Comments
 (0)