Refacto of the scene cut detection, make it generic and add another algorythm as an option

This commit is contained in:
2025-01-10 16:24:52 +01:00
parent de16df05ef
commit 425c842bfd
8 changed files with 248 additions and 155 deletions
+68 -38
View File
@@ -11,7 +11,7 @@ from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels
get_channel_index, new_text_strip, get_strip_at, get_channel_name,
create_shot_strip)
from vse_toolbox import auto_splitter
from vse_toolbox.scene_cut_detection import detect_scene_change
from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings, get_addon_prefs
from shutil import copy2
@@ -176,30 +176,47 @@ class VSETB_OT_set_sequencer(Operator):
return {"FINISHED"}
class VSETB_OT_auto_split(Operator):
class VSETB_OT_scene_cut_detection(Operator):
"""Launch subprocess with ffmpeg and python to find and create each
shots strips from video source"""
bl_idname = "vse_toolbox.auto_split"
bl_label = "Auto Split"
bl_description = "Generate shots strips"
bl_idname = "vse_toolbox.scene_cut_detection"
bl_label = "Scene Cut Detection"
bl_description = "Detect scene change and create strips on the destination channel, use crop to restric detection"
bl_options = {"REGISTER", "UNDO"}
threshold: FloatProperty(name="Threshold", default=0.6, min=0, max=1)
frame_first: IntProperty(name='Start Split')
frame_last: IntProperty(name='End Split')
movie_channel_name: EnumProperty(
animated_threshold: FloatProperty(name="Threshold", default=0.5, min=0, max=1,
description='Probability for the current frame to introduce a new scene')
still_threshold: FloatProperty(name="Threshold", default=0.001, min=0, max=1, precision=4,
description="Noise tolerance, difference ratio between 0 and 1")
frame_start: IntProperty(name='Start Split')
frame_end: IntProperty(name='End Split')
source_channel_name: EnumProperty(
items=lambda self, ctx: ((c.name, c.name, '') for c in ctx.scene.sequence_editor.channels),
name='Movie Channel')
name='Source Channel')
destination_channel_name: EnumProperty(
items=lambda self, ctx: ((c.name, c.name, '') for c in ctx.scene.sequence_editor.channels),
name='Destination Channel')
movie_type: EnumProperty(
items=[('ANIMATED', 'Animated', 'Use select filter from ffmpeg, best for animated frame'),
('STILL', 'Still', 'Use freezedetect filter from ffmpeg, best for board or text')],
name='Movie Type')
def invoke(self, context, event):
self.frame_first = context.scene.frame_start
self.frame_last = context.scene.frame_end
# Select active channel by default
if strip := context.active_sequence_strip:
self.source_channel_name = get_channel_name(strip)
if context.scene.sequence_editor.channels.get('Shots'):
self.destination_channel_name = 'Shots'
self.frame_start = context.scene.frame_start
self.frame_end = context.scene.frame_end
if context.selected_sequences:
self.frame_first = min([s.frame_final_start for s in context.selected_sequences])
self.frame_last = max([s.frame_final_end for s in context.selected_sequences])
self.frame_start = min([s.frame_final_start for s in context.selected_sequences])
self.frame_end = max([s.frame_final_end for s in context.selected_sequences])
return context.window_manager.invoke_props_dialog(self)
@@ -210,54 +227,66 @@ class VSETB_OT_auto_split(Operator):
col.use_property_split = True
col.use_property_decorate = False
col.prop(self, 'threshold')
col.prop(self, 'movie_channel_name')
row = col.row(align=True)
row.prop(self, 'movie_type', expand=True)
if self.movie_type == 'ANIMATED':
col.prop(self, 'animated_threshold')
else:
col.prop(self, 'still_threshold')
col.prop(self, 'source_channel_name')
col.prop(self, 'destination_channel_name')
split_col = col.column(align=True)
split_col.prop(self, 'frame_first', text='Frame Split First')
split_col.prop(self, 'frame_last', text='Last')
split_col.prop(self, 'frame_start', text='Frame Split First')
split_col.prop(self, 'frame_end', text='Last')
def execute(self, context):
if self.source_channel_name == self.destination_channel_name:
self.report({"ERROR"}, 'Source and Destination cannot be the same channel')
return {'CANCELLED'}
context.window_manager.modal_handler_add(self)
return {'PASS_THROUGH'}
def modal(self, context, event):
strips = get_strips(channel=self.movie_channel_name)
scn = context.scene
strips = get_strips(channel=self.source_channel_name)
i = 1
frame_start = self.frame_first
frame_start = self.frame_start
for strip in strips:
if strip.type != 'MOVIE':
continue
# Skip strip outside the frame range to create shot from.
if strip.frame_final_start >= self.frame_last or strip.frame_final_end <= self.frame_first:
if strip.frame_final_start >= self.frame_end or strip.frame_final_end <= self.frame_start:
continue
threshold = self.animated_threshold if self.movie_type == 'ANIMATED' else self.still_threshold
process = auto_splitter.launch_split(strip, self.threshold, frame_start=self.frame_first, frame_end=self.frame_last)
for line in process.stdout:
# Get frame split from the movie timeline (not from blender strips timeline)
frame_end = auto_splitter.get_split_time(line, fps=24)
params = dict(strip=strip, movie_type= self.movie_type, threshold=threshold,
frame_start=self.frame_start, frame_end=self.frame_end,
crop=(strip.crop.min_x, strip.crop.max_x, strip.crop.min_y, strip.crop.max_y))
for frame_end in detect_scene_change(**params):
if not frame_end:
continue
# Convert movie frame to strips frame
if frame_start+int(strip.frame_start) < self.frame_first:
frame_start = self.frame_first
if frame_start+int(strip.frame_start) < self.frame_start:
frame_start = self.frame_start
frame_end += int(strip.frame_final_start)
if frame_end > self.frame_last:
frame_end = self.frame_last
if frame_end > self.frame_end:
frame_end = self.frame_end
create_shot_strip(
f'tmp_shot_{str(i).zfill(3)}',
start=frame_start,
end=frame_end
end=frame_end,
channel=self.destination_channel_name
)
i += 1
@@ -265,14 +294,15 @@ class VSETB_OT_auto_split(Operator):
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
process.wait()
#process.wait()
# Last strip:
if frame_start < self.frame_last:
if frame_start < self.frame_end:
create_shot_strip(
f'tmp_shot_{str(i).zfill(3)}',
start=frame_start,
end=self.frame_last
end=self.frame_end,
channel=self.destination_channel_name
)
return {'FINISHED'}
@@ -705,7 +735,7 @@ class VSETB_OT_update_media(Operator):
@classmethod
def poll(cls, context):
if context.active_sequence_strip .type not in ('MOVIE', 'SOUND'):
if not (strip := context.active_sequence_strip) or strip.type not in ('MOVIE', 'SOUND'):
cls.poll_message_set('No active AUDIO or MOVIE strips')
return
@@ -761,7 +791,7 @@ def unregister_keymaps():
classes = (
VSETB_OT_rename,
VSETB_OT_set_sequencer,
VSETB_OT_auto_split,
VSETB_OT_scene_cut_detection,
VSETB_OT_set_stamps,
VSETB_OT_show_waveform,
VSETB_OT_previous_shot,