Skip to content

Commit a9f012e

Browse files
committed
[save-images] Add new config option for setting threading mode
1 parent 55dec7f commit a9f012e

File tree

8 files changed

+124
-84
lines changed

8 files changed

+124
-84
lines changed

scenedetect.cfg

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,22 +220,25 @@
220220
# Image quality (jpeg/webp). Default is 95 for jpeg, 100 for webp
221221
#quality = 95
222222

223-
# Compression amount for png images (0 to 9). Does not affect quality.
223+
# Compression amount for png images (0 to 9). Only affects size, not quality.
224224
#compression = 3
225225

226-
# Number of frames to skip at beginning/end of scene.
226+
# Number of frames to ignore around each scene cut when selecting frames.
227227
#frame-margin = 1
228228

229-
# Factor to resize images by (0.5 = half, 1.0 = same, 2.0 = double).
229+
# Resize by scale factor (0.5 = half, 1.0 = same, 2.0 = double).
230230
#scale = 1.0
231231

232-
# Override image height and/or width. Mutually exclusive with scale.
232+
# Resize to specified height, width, or both. Mutually exclusive with scale.
233233
#height = 0
234234
#width = 0
235235

236-
# Method to use for image scaling (nearest, linear, cubic, area, lanczos4).
236+
# Method to use for scaling (nearest, linear, cubic, area, lanczos4).
237237
#scale-method = linear
238238

239+
# Use separate threads for encoding and disk IO. Can improve performance.
240+
#threading = yes
241+
239242

240243
[export-html]
241244
# Filename format of created HTML file. Can use $VIDEO_NAME in the name.

scenedetect/__init__.py

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,37 @@
2222
# need to support both opencv-python and opencv-python-headless. Include some additional
2323
# context with the exception if this is the case.
2424
try:
25-
import cv2
25+
import cv2 as _
2626
except ModuleNotFoundError as ex:
2727
raise ModuleNotFoundError(
2828
"OpenCV could not be found, try installing opencv-python:\n\npip install opencv-python",
2929
name="cv2",
3030
) from ex
31-
import numpy as np
3231

33-
from scenedetect.backends import (
34-
AVAILABLE_BACKENDS,
35-
VideoCaptureAdapter,
36-
VideoStreamAv,
37-
VideoStreamCv2,
38-
VideoStreamMoviePy,
39-
)
32+
# Commonly used classes/functions exported under the `scenedetect` namespace for brevity.
33+
# Note that order of importants is important!
34+
from scenedetect.platform import init_logger # noqa: I001
35+
from scenedetect.frame_timecode import FrameTimecode
36+
from scenedetect.video_stream import VideoStream, VideoOpenFailure
37+
from scenedetect.video_splitter import split_video_ffmpeg, split_video_mkvmerge
38+
from scenedetect.scene_detector import SceneDetector
4039
from scenedetect.detectors import (
41-
AdaptiveDetector,
4240
ContentDetector,
43-
HashDetector,
44-
HistogramDetector,
41+
AdaptiveDetector,
4542
ThresholdDetector,
43+
HistogramDetector,
44+
HashDetector,
4645
)
47-
from scenedetect.frame_timecode import FrameTimecode
48-
49-
# Commonly used classes/functions exported under the `scenedetect` namespace for brevity.
50-
from scenedetect.platform import ( # noqa: I001
51-
get_and_create_path,
52-
get_cv2_imwrite_params,
53-
init_logger,
54-
tqdm,
46+
from scenedetect.backends import (
47+
AVAILABLE_BACKENDS,
48+
VideoStreamCv2,
49+
VideoStreamAv,
50+
VideoStreamMoviePy,
51+
VideoCaptureAdapter,
5552
)
56-
from scenedetect.scene_detector import SceneDetector
57-
from scenedetect.scene_manager import Interpolation, SceneList, SceneManager, save_images
58-
from scenedetect.stats_manager import StatsFileCorrupt, StatsManager
53+
from scenedetect.stats_manager import StatsManager, StatsFileCorrupt
54+
from scenedetect.scene_manager import SceneManager, save_images, SceneList, CutList, Interpolation
5955
from scenedetect.video_manager import VideoManager # [DEPRECATED] DO NOT USE.
60-
from scenedetect.video_splitter import split_video_ffmpeg, split_video_mkvmerge
61-
from scenedetect.video_stream import VideoOpenFailure, VideoStream
6256

6357
# Used for module identification and when printing version & about info
6458
# (e.g. calling `scenedetect version` or `scenedetect about`).

scenedetect/_cli/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,7 @@ def save_images_command(
14941494
"output_dir": output,
14951495
"scale": scale,
14961496
"show_progress": not ctx.quiet_mode,
1497+
"threading": ctx.config.get_value("save-images", "threading"),
14971498
"width": width,
14981499
}
14991500
ctx.add_command(cli_commands.save_images, save_images_args)

scenedetect/_cli/commands.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ def save_images(
177177
height: int,
178178
width: int,
179179
interpolation: Interpolation,
180+
threading: bool,
180181
):
181182
"""Handles the `save-images` command."""
182183
del cuts # save-images only uses scenes.
@@ -195,6 +196,7 @@ def save_images(
195196
height=height,
196197
width=width,
197198
interpolation=interpolation,
199+
threading=threading,
198200
)
199201
# Save the result for use by `export-html` if required.
200202
context.save_images_result = (images, output_dir)

scenedetect/_cli/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ def format(self, timecode: FrameTimecode) -> str:
361361
"quality": RangeValue(_PLACEHOLDER, min_val=0, max_val=100),
362362
"scale": 1.0,
363363
"scale-method": Interpolation.LINEAR,
364+
"threading": True,
364365
"width": 0,
365366
},
366367
"save-qp": {

scenedetect/scene_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def get_scenes_from_cuts(
213213
return scene_list
214214

215215

216-
# TODO(v1.0): Move post-processing functionality into separate submodule.
216+
# TODO(#463): Move post-processing functionality into separate submodule.
217217

218218

219219
def write_scene_list(

tests/test_scene_manager.py

Lines changed: 86 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import glob
1919
import os
2020
import os.path
21+
from pathlib import Path
2122
from typing import List
2223

2324
from scenedetect.backends.opencv import VideoStreamCv2
@@ -84,7 +85,7 @@ def test_get_scene_list_start_in_scene(test_video_file):
8485
assert scene_list[0][1] == end_time
8586

8687

87-
def test_save_images(test_video_file):
88+
def test_save_images(test_video_file, tmp_path: Path):
8889
"""Test scenedetect.scene_manager.save_images function."""
8990
video = VideoStreamCv2(test_video_file)
9091
sm = SceneManager()
@@ -97,66 +98,101 @@ def test_save_images(test_video_file):
9798
"$TIMESTAMP_MS.$TIMECODE"
9899
)
99100

100-
try:
101-
video_fps = video.frame_rate
102-
scene_list = [
103-
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
104-
for start, end in [(0, 100), (200, 300), (300, 400)]
105-
]
101+
video_fps = video.frame_rate
102+
scene_list = [
103+
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
104+
for start, end in [(0, 100), (200, 300), (300, 400)]
105+
]
106+
107+
image_filenames = save_images(
108+
scene_list=scene_list,
109+
output_dir=tmp_path,
110+
video=video,
111+
num_images=3,
112+
image_extension="jpg",
113+
image_name_template=image_name_template,
114+
threading=False,
115+
)
106116

107-
image_filenames = save_images(
108-
scene_list=scene_list,
109-
video=video,
110-
num_images=3,
111-
image_extension="jpg",
112-
image_name_template=image_name_template,
113-
)
117+
# Ensure images got created, and the proper number got created.
118+
total_images = 0
119+
for scene_number in image_filenames:
120+
for path in image_filenames[scene_number]:
121+
assert tmp_path.joinpath(path).exists(), f"expected {path} to exist"
122+
total_images += 1
114123

115-
# Ensure images got created, and the proper number got created.
116-
total_images = 0
117-
for scene_number in image_filenames:
118-
for path in image_filenames[scene_number]:
119-
assert os.path.exists(path), f"expected {path} to exist"
120-
total_images += 1
124+
assert total_images == len([path for path in tmp_path.glob(image_name_glob)])
121125

122-
assert total_images == len(glob.glob(image_name_glob))
123126

124-
finally:
125-
for path in glob.glob(image_name_glob):
126-
os.remove(path)
127+
def test_save_images_singlethreaded(test_video_file, tmp_path: Path):
128+
"""Test scenedetect.scene_manager.save_images function."""
129+
video = VideoStreamCv2(test_video_file)
130+
sm = SceneManager()
131+
sm.add_detector(ContentDetector())
132+
133+
image_name_glob = "scenedetect.tempfile.*.jpg"
134+
image_name_template = (
135+
"scenedetect.tempfile."
136+
"$SCENE_NUMBER.$IMAGE_NUMBER.$FRAME_NUMBER."
137+
"$TIMESTAMP_MS.$TIMECODE"
138+
)
139+
140+
video_fps = video.frame_rate
141+
scene_list = [
142+
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
143+
for start, end in [(0, 100), (200, 300), (300, 400)]
144+
]
145+
146+
image_filenames = save_images(
147+
scene_list=scene_list,
148+
output_dir=tmp_path,
149+
video=video,
150+
num_images=3,
151+
image_extension="jpg",
152+
image_name_template=image_name_template,
153+
threading=True,
154+
)
155+
156+
# Ensure images got created, and the proper number got created.
157+
total_images = 0
158+
for scene_number in image_filenames:
159+
for path in image_filenames[scene_number]:
160+
assert tmp_path.joinpath(path).exists(), f"expected {path} to exist"
161+
total_images += 1
162+
163+
assert total_images == len([path for path in tmp_path.glob(image_name_glob)])
127164

128165

129166
# TODO: Test other functionality against zero width scenes.
130-
def test_save_images_zero_width_scene(test_video_file):
167+
def test_save_images_zero_width_scene(test_video_file, tmp_path: Path):
131168
"""Test scenedetect.scene_manager.save_images guards against zero width scenes."""
132169
video = VideoStreamCv2(test_video_file)
133170
image_name_glob = "scenedetect.tempfile.*.jpg"
134171
image_name_template = "scenedetect.tempfile.$SCENE_NUMBER.$IMAGE_NUMBER"
135-
try:
136-
video_fps = video.frame_rate
137-
scene_list = [
138-
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
139-
for start, end in [(0, 0), (1, 1), (2, 3)]
140-
]
141-
NUM_IMAGES = 10
142-
image_filenames = save_images(
143-
scene_list=scene_list,
144-
video=video,
145-
num_images=10,
146-
image_extension="jpg",
147-
image_name_template=image_name_template,
148-
)
149-
assert len(image_filenames) == 3
150-
assert all(len(image_filenames[scene]) == NUM_IMAGES for scene in image_filenames)
151-
total_images = 0
152-
for scene_number in image_filenames:
153-
for path in image_filenames[scene_number]:
154-
assert os.path.exists(path), f"expected {path} to exist"
155-
total_images += 1
156-
assert total_images == len(glob.glob(image_name_glob))
157-
finally:
158-
for path in glob.glob(image_name_glob):
159-
os.remove(path)
172+
173+
video_fps = video.frame_rate
174+
scene_list = [
175+
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
176+
for start, end in [(0, 0), (1, 1), (2, 3)]
177+
]
178+
NUM_IMAGES = 10
179+
image_filenames = save_images(
180+
scene_list=scene_list,
181+
output_dir=tmp_path,
182+
video=video,
183+
num_images=10,
184+
image_extension="jpg",
185+
image_name_template=image_name_template,
186+
)
187+
assert len(image_filenames) == 3
188+
assert all(len(image_filenames[scene]) == NUM_IMAGES for scene in image_filenames)
189+
total_images = 0
190+
for scene_number in image_filenames:
191+
for path in image_filenames[scene_number]:
192+
assert tmp_path.joinpath(path).exists(), f"expected {path} to exist"
193+
total_images += 1
194+
195+
assert total_images == len([path for path in tmp_path.glob(image_name_glob)])
160196

161197

162198
# TODO: This would be more readable if the callbacks were defined within the test case, e.g.

website/pages/changelog.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -588,15 +588,18 @@ Development
588588
- [feature] Add ability to configure CSV separators for rows/columns in config file [#423](https://github.com/Breakthrough/PySceneDetect/issues/423)
589589
- [feature] Add new `--show` flag to `export-html` command to launch browser after processing [#442](https://github.com/Breakthrough/PySceneDetect/issues/442)
590590
- [general] Timecodes of the form `MM:SS[.nnn]` are now processed correctly [#443](https://github.com/Breakthrough/PySceneDetect/issues/443)
591-
- [bugfix] Fix `save-images`/`save_images()` not working correctly with UTF-8 paths [#450](https://github.com/Breakthrough/PySceneDetect/issues/455)
591+
- [bugfix] Fix `save-images`/`save_images()` not working correctly with UTF-8 paths [#450](https://github.com/Breakthrough/PySceneDetect/issues/450)
592+
- [improvement] Add new `threading` option to `save-images`/`save_images()` [#456](https://github.com/Breakthrough/PySceneDetect/issues/456)
593+
- Enabled by default, offloads image encoding and disk IO to separate threads
594+
- Improves performance by up to 50% in some cases
592595
- [bugfix] Fix crash when using `save-images`/`save_images()` with OpenCV backend [#455](https://github.com/Breakthrough/PySceneDetect/issues/455)
593596
- [bugfix] Fix new detectors not working with `default-detector` config option
594597
- [improvement] The `export-html` command now implicitly invokes `save-images` with default parameters
595-
- The output of the `export-html` command will always use the result of the `save-images` command that *precedes* it
598+
- The output of the `export-html` command will always use the result of the `save-images` command that *precedes* it
596599
- [general] Updates to Windows distributions:
597600
- The MoviePy backend is now included with Windows distributions
598601
- Bundled Python interpreter is now Python 3.13
599602
- Updated PyAV 10 -> 13.1.0 and OpenCV 4.10.0.82 -> 4.10.0.84
600603
- [improvement] `save_to_csv` now works with paths from `pathlib`
601604
- [api] The `save_to_csv` function now works correctly with paths from the `pathlib` module
602-
- [api] Add `col_separator` and `row_separator` args to `write_scene_list` function in `scenedetect.scene_manager`
605+
- [api] Add `col_separator` and `row_separator` args to `write_scene_list` function in `scenedetect.scene_manager`

0 commit comments

Comments
 (0)