Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,8 @@ Examples

``scenedetect -i video.mp4 save-images --num-images 5``

``scenedetect -i video.mp4 save-images --interval 2s --max-images 10``

``scenedetect -i video.mp4 save-images --width 1024``

``scenedetect -i video.mp4 save-images --filename \$SCENE_NUMBER-img\$IMAGE_NUMBER``
Expand All @@ -670,10 +672,26 @@ Options

.. option:: -n N, --num-images N

Number of images to generate per scene. Will always include start/end frame, unless -n 1, in which case the image will be the frame at the mid-point of the scene.
Number of images to generate per scene. Will always include start/end frame, unless -n 1, in which case the image will be the frame at the mid-point of the scene. Cannot be used with :option:`-i/--interval <-i>`.

Default: ``3``

.. option:: -i DURATION, --interval DURATION

Generate one image per DURATION of each scene instead of a fixed number, so longer scenes get more images. Each scene gets floor(length / DURATION) + 1 images, chosen the same way as with :option:`-n/--num-images <-n>`. Use --min-images and --max-images to limit the number of images per scene. DURATION can be specified in frames (-i 24), in seconds with `s` suffix (-i 1.5s), or timecode (-i 00:00:01.500). Cannot be used with :option:`-n/--num-images <-n>`.

.. option:: --min-images N

Minimum number of images per scene. Can only be used with :option:`-i/--interval <-i>`.

Default: ``1``

.. option:: --max-images N

Maximum number of images per scene, or 0 for no limit. Can only be used with :option:`-i/--interval <-i>`.

Default: ``0``

.. option:: -j, --jpeg

Set output format to JPEG (default).
Expand Down
13 changes: 12 additions & 1 deletion scenedetect.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,20 @@
# Image format (jpeg, png, webp).
#format = jpeg

# Number of images to generate for each scene.
# Number of images to generate for each scene. Cannot be used with interval.
#num-images = 3

# Generate one image per DURATION of each scene instead of num-images, so longer
# scenes get more images: floor(length / DURATION) + 1 per scene. DURATION accepts
# frames (24), seconds with `s` suffix (1.5s), or timecode (00:00:01.500).
# Cannot be used with num-images.
#interval = 2s

# Minimum and maximum number of images per scene with interval (ignored without
# interval). Set max-images to 0 for no limit.
#min-images = 1
#max-images = 0

# Image quality (jpeg/webp). Default is 95 for jpeg, 100 for webp
#quality = 95

Expand Down
88 changes: 86 additions & 2 deletions scenedetect/_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
CONFIG_MAP,
DEFAULT_JPG_QUALITY,
DEFAULT_WEBP_QUALITY,
ImageIntervalValue,
RangeValue,
)
from scenedetect._cli.context import USER_CONFIG, CliContext, check_split_video_requirements
Expand Down Expand Up @@ -1400,6 +1401,8 @@ def split_video_command(

{scenedetect_with_video} save-images --num-images 5

{scenedetect_with_video} save-images --interval 2s --max-images 10

{scenedetect_with_video} save-images --width 1024

{scenedetect_with_video} save-images --filename \\$SCENE_NUMBER-img\\$IMAGE_NUMBER
Expand Down Expand Up @@ -1432,10 +1435,38 @@ def split_video_command(
metavar="N",
default=None,
type=click.INT,
help="Number of images to generate per scene. Will always include start/end frame, unless -n 1, in which case the image will be the frame at the mid-point of the scene.{}".format(
help="Number of images to generate per scene. Will always include start/end frame, unless -n 1, in which case the image will be the frame at the mid-point of the scene. Cannot be used with -i/--interval.{}".format(
USER_CONFIG.get_help_string("save-images", "num-images")
),
)
@click.option(
"--interval",
"-i",
metavar="DURATION",
default=None,
type=click.STRING,
help="Generate one image per DURATION of each scene instead of a fixed number, so longer scenes get more images. Each scene gets floor(length / DURATION) + 1 images, chosen the same way as with -n/--num-images. Use --min-images and --max-images to limit the number of images per scene. DURATION can be specified in frames (-i 24), in seconds with `s` suffix (-i 1.5s), or timecode (-i 00:00:01.500). Cannot be used with -n/--num-images.{}".format(
USER_CONFIG.get_help_string("save-images", "interval", show_default=False)
),
)
@click.option(
"--min-images",
metavar="N",
default=None,
type=click.IntRange(min=1),
help="Minimum number of images per scene. Can only be used with -i/--interval.{}".format(
USER_CONFIG.get_help_string("save-images", "min-images")
),
)
@click.option(
"--max-images",
metavar="N",
default=None,
type=click.IntRange(min=0),
help="Maximum number of images per scene, or 0 for no limit. Can only be used with -i/--interval.{}".format(
USER_CONFIG.get_help_string("save-images", "max-images")
),
)
@click.option(
"--jpeg",
"-j",
Expand Down Expand Up @@ -1525,6 +1556,9 @@ def save_images_command(
output: str | None = None,
filename: str | None = None,
num_images: int | None = None,
interval: str | None = None,
min_images: int | None = None,
max_images: int | None = None,
jpeg: bool = False,
webp: bool = False,
quality: int | None = None,
Expand Down Expand Up @@ -1577,14 +1611,64 @@ def save_images_command(
raise click.BadParameter("\n".join(error_strs), param_hint="save-images")
output = ctx.config.get_value("save-images", "output", output)

# -n/--num-images and -i/--interval are mutually exclusive. If neither is set, use the
# config file, where only one of them may be set.
if num_images is not None and interval is not None:
raise click.BadParameter(
"Only one of -n/--num-images or -i/--interval can be set.", param_hint="save-images"
)
if interval is not None:
try:
ImageIntervalValue(interval)
except ValueError as ex:
raise click.BadParameter(str(ex), param_hint="-i/--interval") from ex
elif num_images is None:
if not ctx.config.is_default("save-images", "interval") and not ctx.config.is_default(
"save-images", "num-images"
):
raise click.BadParameter(
"Only one of num-images or interval can be set in the config file.",
param_hint="save-images",
)
interval = ctx.config.get_value("save-images", "interval")
if interval is None:
num_images = ctx.config.get_value("save-images", "num-images")
# --min-images/--max-images only apply with an interval (from the command line or the config
# file). Without one, they are an error on the command line, and ignored in the config file.
if interval is None:
if min_images is not None or max_images is not None:
raise click.BadParameter(
"--min-images and --max-images can only be used with -i/--interval.",
param_hint="save-images",
)
min_images, max_images = 1, None
else:
min_images_value: int = ctx.config.get_value("save-images", "min-images", min_images)
max_images_value: int = ctx.config.get_value("save-images", "max-images", max_images)
# Click checks the range of values from the command line, but not from the config file.
if min_images_value < 1:
raise click.BadParameter(
"min-images in the config file must be at least 1.", param_hint="save-images"
)
if max_images_value != 0 and max_images_value < min_images_value:
raise click.BadParameter(
"max-images must be 0 (no limit) or at least min-images.",
param_hint="save-images",
)
# A max-images of 0 means no limit.
min_images, max_images = min_images_value, max_images_value or None

save_images_args = {
"encoder_param": compression if png else quality,
"frame_margin": ctx.config.get_value("save-images", "frame-margin", frame_margin),
"height": height,
"image_extension": image_extension,
"filename": ctx.config.get_value("save-images", "filename", filename),
"interpolation": scale_method,
"num_images": ctx.config.get_value("save-images", "num-images", num_images),
"interval": interval,
"max_images": max_images,
"min_images": min_images,
"num_images": num_images,
"output": output,
"scale": scale,
"show_progress": not ctx.quiet_mode,
Expand Down
8 changes: 7 additions & 1 deletion scenedetect/_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,10 @@ def save_images(
context: CliContext,
scenes: SceneList,
cuts: CutList,
num_images: int,
num_images: int | None,
interval: str | None,
min_images: int,
max_images: int | None,
frame_margin: int,
image_extension: str,
encoder_param: int,
Expand All @@ -215,6 +218,9 @@ def save_images(
scene_list=scenes,
video=context.video_stream,
num_images=num_images,
interval=interval,
min_images=min_images,
max_images=max_images,
frame_margin=frame_margin,
image_extension=image_extension,
encoder_param=encoder_param,
Expand Down
32 changes: 32 additions & 0 deletions scenedetect/_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,35 @@ def from_config(config_value: str, default: "TimecodeValue") -> "TimecodeValue":
) from ex


class ImageIntervalValue(ValidatedValue):
"""Validator for the save-images interval: a timecode greater than 0, or None if unset.

Stores value in original representation, like `TimecodeValue`."""

def __init__(self, value: str | None = None):
if value is not None:
try:
seconds = FrameTimecode(timecode=value, fps=100.0).seconds
except ValueError as ex:
raise ValueError(
"Interval must be in frames (24), seconds (1.5s), or HH:MM:SS."
) from ex
if seconds <= 0:
raise ValueError("Interval must be greater than 0.")
self._value = value

@property
def value(self) -> str | None:
return self._value

@staticmethod
def from_config(config_value: str, default: "ImageIntervalValue") -> "ImageIntervalValue":
try:
return ImageIntervalValue(config_value)
except ValueError as ex:
raise OptionParseFailure(f"{ex}") from ex


class RangeValue(ValidatedValue):
"""Validator for int/float ranges. `min_val` and `max_val` are inclusive."""

Expand Down Expand Up @@ -442,6 +471,9 @@ class FcpFormat(Enum):
"format": "jpeg",
"frame-margin": TimecodeValue(1),
"height": 0,
"interval": ImageIntervalValue(),
"max-images": 0,
"min-images": 1,
"num-images": 3,
"output": None,
"quality": RangeValue(_PLACEHOLDER, min_val=0, max_val=100),
Expand Down
4 changes: 2 additions & 2 deletions scenedetect/output/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ def write_scene_list_html(
the start times of each scene (besides the 0th scene) is used instead.
css: String containing all the css information for the resulting html page.
css_class: String containing the named css class
image_filenames: dict where key i contains a list with n elements (filenames of
the n saved images from that scene)
image_filenames: dict where key i contains a list of the filenames of the images
saved from that scene (the number of images can differ between scenes)
image_width: Optional desired width of images in table in pixels
image_height: Optional desired height of images in table in pixels
"""
Expand Down
Loading