Skip to content

Commit

Permalink
[save-images] Add new config option for setting threading mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Breakthrough committed Nov 24, 2024
1 parent 55dec7f commit a9f012e
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 84 deletions.
13 changes: 8 additions & 5 deletions scenedetect.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -220,22 +220,25 @@
# Image quality (jpeg/webp). Default is 95 for jpeg, 100 for webp
#quality = 95

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

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

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

# Override image height and/or width. Mutually exclusive with scale.
# Resize to specified height, width, or both. Mutually exclusive with scale.
#height = 0
#width = 0

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

# Use separate threads for encoding and disk IO. Can improve performance.
#threading = yes


[export-html]
# Filename format of created HTML file. Can use $VIDEO_NAME in the name.
Expand Down
44 changes: 19 additions & 25 deletions scenedetect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,43 +22,37 @@
# need to support both opencv-python and opencv-python-headless. Include some additional
# context with the exception if this is the case.
try:
import cv2
import cv2 as _
except ModuleNotFoundError as ex:
raise ModuleNotFoundError(
"OpenCV could not be found, try installing opencv-python:\n\npip install opencv-python",
name="cv2",
) from ex
import numpy as np

from scenedetect.backends import (
AVAILABLE_BACKENDS,
VideoCaptureAdapter,
VideoStreamAv,
VideoStreamCv2,
VideoStreamMoviePy,
)
# Commonly used classes/functions exported under the `scenedetect` namespace for brevity.
# Note that order of importants is important!
from scenedetect.platform import init_logger # noqa: I001
from scenedetect.frame_timecode import FrameTimecode
from scenedetect.video_stream import VideoStream, VideoOpenFailure
from scenedetect.video_splitter import split_video_ffmpeg, split_video_mkvmerge
from scenedetect.scene_detector import SceneDetector
from scenedetect.detectors import (
AdaptiveDetector,
ContentDetector,
HashDetector,
HistogramDetector,
AdaptiveDetector,
ThresholdDetector,
HistogramDetector,
HashDetector,
)
from scenedetect.frame_timecode import FrameTimecode

# Commonly used classes/functions exported under the `scenedetect` namespace for brevity.
from scenedetect.platform import ( # noqa: I001
get_and_create_path,
get_cv2_imwrite_params,
init_logger,
tqdm,
from scenedetect.backends import (
AVAILABLE_BACKENDS,
VideoStreamCv2,
VideoStreamAv,
VideoStreamMoviePy,
VideoCaptureAdapter,
)
from scenedetect.scene_detector import SceneDetector
from scenedetect.scene_manager import Interpolation, SceneList, SceneManager, save_images
from scenedetect.stats_manager import StatsFileCorrupt, StatsManager
from scenedetect.stats_manager import StatsManager, StatsFileCorrupt
from scenedetect.scene_manager import SceneManager, save_images, SceneList, CutList, Interpolation
from scenedetect.video_manager import VideoManager # [DEPRECATED] DO NOT USE.
from scenedetect.video_splitter import split_video_ffmpeg, split_video_mkvmerge
from scenedetect.video_stream import VideoOpenFailure, VideoStream

# Used for module identification and when printing version & about info
# (e.g. calling `scenedetect version` or `scenedetect about`).
Expand Down
1 change: 1 addition & 0 deletions scenedetect/_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,7 @@ def save_images_command(
"output_dir": output,
"scale": scale,
"show_progress": not ctx.quiet_mode,
"threading": ctx.config.get_value("save-images", "threading"),
"width": width,
}
ctx.add_command(cli_commands.save_images, save_images_args)
Expand Down
2 changes: 2 additions & 0 deletions scenedetect/_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def save_images(
height: int,
width: int,
interpolation: Interpolation,
threading: bool,
):
"""Handles the `save-images` command."""
del cuts # save-images only uses scenes.
Expand All @@ -195,6 +196,7 @@ def save_images(
height=height,
width=width,
interpolation=interpolation,
threading=threading,
)
# Save the result for use by `export-html` if required.
context.save_images_result = (images, output_dir)
Expand Down
1 change: 1 addition & 0 deletions scenedetect/_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ def format(self, timecode: FrameTimecode) -> str:
"quality": RangeValue(_PLACEHOLDER, min_val=0, max_val=100),
"scale": 1.0,
"scale-method": Interpolation.LINEAR,
"threading": True,
"width": 0,
},
"save-qp": {
Expand Down
2 changes: 1 addition & 1 deletion scenedetect/scene_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def get_scenes_from_cuts(
return scene_list


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


def write_scene_list(
Expand Down
136 changes: 86 additions & 50 deletions tests/test_scene_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import glob
import os
import os.path
from pathlib import Path
from typing import List

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


def test_save_images(test_video_file):
def test_save_images(test_video_file, tmp_path: Path):
"""Test scenedetect.scene_manager.save_images function."""
video = VideoStreamCv2(test_video_file)
sm = SceneManager()
Expand All @@ -97,66 +98,101 @@ def test_save_images(test_video_file):
"$TIMESTAMP_MS.$TIMECODE"
)

try:
video_fps = video.frame_rate
scene_list = [
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
for start, end in [(0, 100), (200, 300), (300, 400)]
]
video_fps = video.frame_rate
scene_list = [
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
for start, end in [(0, 100), (200, 300), (300, 400)]
]

image_filenames = save_images(
scene_list=scene_list,
output_dir=tmp_path,
video=video,
num_images=3,
image_extension="jpg",
image_name_template=image_name_template,
threading=False,
)

image_filenames = save_images(
scene_list=scene_list,
video=video,
num_images=3,
image_extension="jpg",
image_name_template=image_name_template,
)
# Ensure images got created, and the proper number got created.
total_images = 0
for scene_number in image_filenames:
for path in image_filenames[scene_number]:
assert tmp_path.joinpath(path).exists(), f"expected {path} to exist"
total_images += 1

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

assert total_images == len(glob.glob(image_name_glob))

finally:
for path in glob.glob(image_name_glob):
os.remove(path)
def test_save_images_singlethreaded(test_video_file, tmp_path: Path):
"""Test scenedetect.scene_manager.save_images function."""
video = VideoStreamCv2(test_video_file)
sm = SceneManager()
sm.add_detector(ContentDetector())

image_name_glob = "scenedetect.tempfile.*.jpg"
image_name_template = (
"scenedetect.tempfile."
"$SCENE_NUMBER.$IMAGE_NUMBER.$FRAME_NUMBER."
"$TIMESTAMP_MS.$TIMECODE"
)

video_fps = video.frame_rate
scene_list = [
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
for start, end in [(0, 100), (200, 300), (300, 400)]
]

image_filenames = save_images(
scene_list=scene_list,
output_dir=tmp_path,
video=video,
num_images=3,
image_extension="jpg",
image_name_template=image_name_template,
threading=True,
)

# Ensure images got created, and the proper number got created.
total_images = 0
for scene_number in image_filenames:
for path in image_filenames[scene_number]:
assert tmp_path.joinpath(path).exists(), f"expected {path} to exist"
total_images += 1

assert total_images == len([path for path in tmp_path.glob(image_name_glob)])


# TODO: Test other functionality against zero width scenes.
def test_save_images_zero_width_scene(test_video_file):
def test_save_images_zero_width_scene(test_video_file, tmp_path: Path):
"""Test scenedetect.scene_manager.save_images guards against zero width scenes."""
video = VideoStreamCv2(test_video_file)
image_name_glob = "scenedetect.tempfile.*.jpg"
image_name_template = "scenedetect.tempfile.$SCENE_NUMBER.$IMAGE_NUMBER"
try:
video_fps = video.frame_rate
scene_list = [
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
for start, end in [(0, 0), (1, 1), (2, 3)]
]
NUM_IMAGES = 10
image_filenames = save_images(
scene_list=scene_list,
video=video,
num_images=10,
image_extension="jpg",
image_name_template=image_name_template,
)
assert len(image_filenames) == 3
assert all(len(image_filenames[scene]) == NUM_IMAGES for scene in image_filenames)
total_images = 0
for scene_number in image_filenames:
for path in image_filenames[scene_number]:
assert os.path.exists(path), f"expected {path} to exist"
total_images += 1
assert total_images == len(glob.glob(image_name_glob))
finally:
for path in glob.glob(image_name_glob):
os.remove(path)

video_fps = video.frame_rate
scene_list = [
(FrameTimecode(start, video_fps), FrameTimecode(end, video_fps))
for start, end in [(0, 0), (1, 1), (2, 3)]
]
NUM_IMAGES = 10
image_filenames = save_images(
scene_list=scene_list,
output_dir=tmp_path,
video=video,
num_images=10,
image_extension="jpg",
image_name_template=image_name_template,
)
assert len(image_filenames) == 3
assert all(len(image_filenames[scene]) == NUM_IMAGES for scene in image_filenames)
total_images = 0
for scene_number in image_filenames:
for path in image_filenames[scene_number]:
assert tmp_path.joinpath(path).exists(), f"expected {path} to exist"
total_images += 1

assert total_images == len([path for path in tmp_path.glob(image_name_glob)])


# TODO: This would be more readable if the callbacks were defined within the test case, e.g.
Expand Down
9 changes: 6 additions & 3 deletions website/pages/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,15 +588,18 @@ Development
- [feature] Add ability to configure CSV separators for rows/columns in config file [#423](https://github.com/Breakthrough/PySceneDetect/issues/423)
- [feature] Add new `--show` flag to `export-html` command to launch browser after processing [#442](https://github.com/Breakthrough/PySceneDetect/issues/442)
- [general] Timecodes of the form `MM:SS[.nnn]` are now processed correctly [#443](https://github.com/Breakthrough/PySceneDetect/issues/443)
- [bugfix] Fix `save-images`/`save_images()` not working correctly with UTF-8 paths [#450](https://github.com/Breakthrough/PySceneDetect/issues/455)
- [bugfix] Fix `save-images`/`save_images()` not working correctly with UTF-8 paths [#450](https://github.com/Breakthrough/PySceneDetect/issues/450)
- [improvement] Add new `threading` option to `save-images`/`save_images()` [#456](https://github.com/Breakthrough/PySceneDetect/issues/456)
- Enabled by default, offloads image encoding and disk IO to separate threads
- Improves performance by up to 50% in some cases
- [bugfix] Fix crash when using `save-images`/`save_images()` with OpenCV backend [#455](https://github.com/Breakthrough/PySceneDetect/issues/455)
- [bugfix] Fix new detectors not working with `default-detector` config option
- [improvement] The `export-html` command now implicitly invokes `save-images` with default parameters
- The output of the `export-html` command will always use the result of the `save-images` command that *precedes* it
- The output of the `export-html` command will always use the result of the `save-images` command that *precedes* it
- [general] Updates to Windows distributions:
- The MoviePy backend is now included with Windows distributions
- Bundled Python interpreter is now Python 3.13
- Updated PyAV 10 -> 13.1.0 and OpenCV 4.10.0.82 -> 4.10.0.84
- [improvement] `save_to_csv` now works with paths from `pathlib`
- [api] The `save_to_csv` function now works correctly with paths from the `pathlib` module
- [api] Add `col_separator` and `row_separator` args to `write_scene_list` function in `scenedetect.scene_manager`
- [api] Add `col_separator` and `row_separator` args to `write_scene_list` function in `scenedetect.scene_manager`

0 comments on commit a9f012e

Please sign in to comment.