Skip to content
Merged
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
30 changes: 30 additions & 0 deletions scenedetect/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,34 @@ def detect_threshold_command(ctx, threshold, min_scene_len, fade_bias, add_last_
threshold=threshold, min_scene_len=min_scene_len, fade_bias=fade_bias,
add_final_scene=add_last_scene, min_percent=min_percent, block_size=block_size))


@click.command('export-html', add_help_option=False)
@click.option(
'--filename', '-f', metavar='NAME', default='$VIDEO_NAME-Scenes.html',
type=click.STRING, show_default=True, help=
'Filename format to use for the scene list html file. You can use the'
' $VIDEO_NAME macro in the file name.')
Comment thread
Breakthrough marked this conversation as resolved.
@click.option(
'--no-images', is_flag=True, flag_value=True, help=
'Export the scene list including or excluding the saved images.')
@click.option(
'--image-width', '-w', metavar='pixels',
type=click.INT, help=
'Width in pixels of the images in the resulting html table.')
@click.option(
'--image-height', '-h', metavar='pixels',
type=click.INT, help=
'Height in pixels of the images in the resulting html table.')
@click.pass_context
def export_html_command(ctx, filename, no_images, image_width, image_height):
""" Exports scene list to a html file. Can also include scene images."""
if not ctx.obj.save_images and not no_images:
raise click.BadParameter("save-images isn't enabled")
ctx.obj.export_html_command(filename, no_images, image_width, image_height)
ctx.obj.export_html = True



@click.command('list-scenes', add_help_option=False)
@click.option(
'--output', '-o', metavar='DIR',
Expand Down Expand Up @@ -716,3 +744,5 @@ def colors_command(ctx):
add_cli_command(scenedetect_cli, save_images_command)
add_cli_command(scenedetect_cli, split_video_command)

add_cli_command(scenedetect_cli, export_html_command)

61 changes: 54 additions & 7 deletions scenedetect/cli/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

from scenedetect.scene_manager import SceneManager
from scenedetect.scene_manager import write_scene_list
from scenedetect.scene_manager import write_scene_list_html

from scenedetect.stats_manager import StatsManager
from scenedetect.stats_manager import StatsFileCorrupt
Expand Down Expand Up @@ -130,6 +131,13 @@ def __init__(self):
self.scene_list_name_format = None # list-scenes -f/--filename
self.scene_list_output = False # list-scenes -n/--no-output

self.export_html = False # export-html command
self.html_name_format = None # export-html -f/--filename
self.html_include_images = True # export-html --no-images
self.image_filenames = None # export-html used for embedding images
self.image_width = None # export-html -w/--image-width
self.image_height = None # export-html -h/--image-height


def cleanup(self):
# type: () -> None
Expand Down Expand Up @@ -181,9 +189,11 @@ def _generate_images(self, scene_list, video_name,
image_num_format += str(math.floor(math.log(self.num_images, 10)) + 2) + 'd'

timecode_list = dict()
self.image_filenames = dict()

for i in range(len(scene_list)):
timecode_list[i] = []
self.image_filenames[i] = []

if self.num_images == 1:
for i, (start_time, end_time) in enumerate(scene_list):
Expand Down Expand Up @@ -212,14 +222,16 @@ def _generate_images(self, scene_list, video_name,
self.video_manager.grab()
ret_val, frame_im = self.video_manager.retrieve()
if ret_val:
file_path = '%s.%s' % (filename_template.safe_substitute(
VIDEO_NAME=video_name,
SCENE_NUMBER=scene_num_format % (i + 1),
IMAGE_NUMBER=image_num_format % (j + 1)),
self.image_extension)
self.image_filenames[i].append(file_path)
cv2.imwrite(
self.get_output_file_path(
'%s.%s' % (filename_template.safe_substitute(
VIDEO_NAME=video_name,
SCENE_NUMBER=scene_num_format % (i+1),
IMAGE_NUMBER=image_num_format % (j+1)
), self.image_extension),
output_dir=output_dir), frame_im, imwrite_param)
self.get_output_file_path(file_path,
output_dir=output_dir),
frame_im, imwrite_param)
else:
completed = False
break
Expand Down Expand Up @@ -399,6 +411,22 @@ def process_input(self):
image_name_template=self.image_name_format,
output_dir=self.image_directory)

# Handle export-html command.
if self.export_html:
html_filename = Template(self.html_name_format).safe_substitute(
VIDEO_NAME=video_name)
if not html_filename.lower().endswith('.html'):
html_filename += '.html'
html_path = self.get_output_file_path(
html_filename, self.image_directory)
logging.info('Exporting to html file:\n %s:', html_path)
if not self.html_include_images:
self.image_filenames = None
write_scene_list_html(html_path, scene_list, cut_list,
image_filenames=self.image_filenames,
image_width=self.image_width,
image_height=self.image_height)

# Handle split-video command.
if self.split_video:
# Add proper extension to filename template if required.
Expand Down Expand Up @@ -618,6 +646,25 @@ def list_scenes_command(self, output_path, filename_format, no_output_mode, quie
logging.info('Scene list output directory set:\n %s', self.scene_list_directory)


def export_html_command(self, filename, no_images, image_width, image_height):
# type: (str, bool) -> None
"""Export HTML command: Parses all options/arguments passed to the export-html command,
or with respect to the CLI, this function processes [export-html] options when calling:
scenedetect [global options] export-html [export-html options] [other commands...].

Raises:
click.BadParameter
"""
self.check_input_open()

self.html_name_format = filename
if self.html_name_format is not None:
logging.info('Scene list html file name format:\n %s', self.html_name_format)
self.html_include_images = False if no_images else True
self.image_width = image_width
self.image_height = image_height


def save_images_command(self, num_images, output, name_format, jpeg, webp, quality,
png, compression):
# type: (int, str, str, bool, bool, int, bool, int) -> None
Expand Down
94 changes: 94 additions & 0 deletions scenedetect/scene_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
from scenedetect.platform import get_csv_writer
from scenedetect.stats_manager import FrameMetricRegistered

from scenedetect.simpletable import SimpleTableCell, SimpleTableImage
from scenedetect.simpletable import SimpleTableRow, SimpleTable, HTMLPage



##
## SceneManager Helper Functions
Expand Down Expand Up @@ -136,6 +140,96 @@ def write_scene_list(output_csv_file, scene_list, cut_list=None):
'%d' % duration.get_frames(), duration.get_timecode(), '%.3f' % duration.get_seconds()])


def write_scene_list_html(output_html_filename, scene_list, cut_list=None, css=None, css_class='mytable',

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would like to have this function in it's own .py file, just because it has nothing to do with the SceneManager class itself. That being said, this is something I can tackle myself - just a small comment :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I put it there because this is also where the write_scene_list function is that writes the csv file. I basically tried to make these two functions the same. One minor thing is that the write_scene_list function takes a file handle, but the html version just takes a filepath. This is because the simpletable code does the opening of the file and I thought it easier to have this discrepancy than rewriting the simpletable code at the time. If these two functions get consolidated, it might make sense to standardize the file handling as well.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points @wjs018, I'll make a note to see if I can modify write_scene_list to also just take the output file path as a string, instead of a file handle, to make it consistent. Also good point in that write_scene_list and write_scene_list_html belong together - perhaps I will move them into a new sub-module (e.g. export.py or scene_export.py - I'm not sure which is better, or if there's a better name - feel free to choose or suggest one!).

image_filenames=None, image_width=None, image_height=None):
"""Writes the given list of scenes to an output file handle in html format.

Arguments:
output_html_filename: filename of output html file
scene_list: List of pairs of FrameTimecodes denoting each scene's start/end FrameTimecode.
cut_list: Optional list of FrameTimecode objects denoting the cut list (i.e. the frames
in the video that need to be split to generate individual scenes). If not passed,
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_width: Optional desired width of images in table in pixels
image_height: Optional desired height of images in table in pixels
"""
if not css:
css = """
table.mytable {
font-family: times;
font-size:12px;
color:#000000;
border-width: 1px;
border-color: #eeeeee;
border-collapse: collapse;
background-color: #ffffff;
width=100%;
max-width:550px;
table-layout:fixed;
}
table.mytable th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #eeeeee;
background-color: #e6eed6;
color:#000000;
}
table.mytable td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #eeeeee;
}
#code {
display:inline;
font-family: courier;
color: #3d9400;
}
#string {
display:inline;
font-weight: bold;
}
"""

# Output Timecode list
timecode_table = SimpleTable([["Timecode List:"] +
(cut_list if cut_list else [start.get_timecode() for start, _ in scene_list[1:]])],
css_class=css_class)

# Output list of scenes
header_row = ["Scene Number", "Start Frame", "Start Timecode", "Start Time (seconds)","End Frame",
"End Timecode", "End Time (seconds)", "Length (frames)", "Length (timecode)", "Length (seconds)"]
for i, (start, end) in enumerate(scene_list):
duration = end - start

row = SimpleTableRow([
'%d' % (i+1),
'%d' % start.get_frames(), start.get_timecode(), '%.3f' % start.get_seconds(),
'%d' % end.get_frames(), end.get_timecode(), '%.3f' % end.get_seconds(),
'%d' % duration.get_frames(), duration.get_timecode(), '%.3f' % duration.get_seconds()])

if image_filenames:
for image in image_filenames[i]:
row.add_cell(SimpleTableCell(SimpleTableImage(image, width=image_width, height=image_height)))

if i == 0:
scene_table = SimpleTable(rows=[row], header_row=header_row, css_class=css_class)
else:
scene_table.add_row(row=row)

# Write html file
page = HTMLPage()
page.add_table(timecode_table)
page.add_table(scene_table)
page.css = css
page.save(output_html_filename)


##
## SceneManager Class Implementation
##
Expand Down
Loading