scene cut detection
This commit is contained in:
+85
-1
@@ -11,7 +11,8 @@ 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, update_text_strips)
|
||||
|
||||
from vse_toolbox.scene_cut_detection import detect_scene_change
|
||||
from vse_toolbox.scene_cut_detection import (detect_scene_change, get_scene_scores,
|
||||
compute_scene_threshold)
|
||||
from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings, get_addon_prefs
|
||||
from shutil import copy2
|
||||
|
||||
@@ -230,6 +231,11 @@ class VSETB_OT_scene_cut_detection(Operator):
|
||||
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")
|
||||
known_cuts: StringProperty(
|
||||
name='Known Cuts',
|
||||
description="Manually checked cut frames (timeline numbers), separated by ','. "
|
||||
"Calibrates the threshold automatically (Animated only), e.g. 680,890,1059",
|
||||
default='')
|
||||
frame_start: IntProperty(name='Start Split')
|
||||
frame_end: IntProperty(name='End Split')
|
||||
source_channel_name: EnumProperty(
|
||||
@@ -288,6 +294,7 @@ class VSETB_OT_scene_cut_detection(Operator):
|
||||
row.prop(self, 'movie_type', expand=True)
|
||||
if self.movie_type == 'ANIMATED':
|
||||
col.prop(self, 'animated_threshold')
|
||||
col.prop(self, 'known_cuts')
|
||||
else:
|
||||
col.prop(self, 'still_threshold')
|
||||
|
||||
@@ -298,11 +305,85 @@ class VSETB_OT_scene_cut_detection(Operator):
|
||||
split_col.prop(self, 'frame_start', text='Frame Split First')
|
||||
split_col.prop(self, 'frame_end', text='Last')
|
||||
|
||||
def _parse_known_cuts(self):
|
||||
"""Return the Known Cuts frames as a list of int, or None on bad input."""
|
||||
try:
|
||||
return [int(f) for f in re.split(r'[:;, ]+', self.known_cuts.strip()) if f]
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _calibrate_threshold(self):
|
||||
"""Compute the threshold from the Known Cuts frames and apply it."""
|
||||
known_frames = self._parse_known_cuts()
|
||||
|
||||
# Score the whole split range, extended to cover every known cut:
|
||||
# known cuts outside the split range are still valid calibration points.
|
||||
scan_start = min([self.frame_start] + known_frames)
|
||||
scan_end = max([self.frame_end] + [frame + 1 for frame in known_frames])
|
||||
|
||||
scene_scores = {}
|
||||
for strip in get_strips(channel=self.source_channel_name):
|
||||
if strip.type != 'MOVIE':
|
||||
continue
|
||||
|
||||
# Skip strip outside the scanned frame range.
|
||||
if strip.frame_final_start >= scan_end or strip.frame_final_end <= scan_start:
|
||||
continue
|
||||
|
||||
scene_scores.update(get_scene_scores(
|
||||
strip,
|
||||
frame_start=scan_start,
|
||||
frame_end=scan_end,
|
||||
crop=(strip.crop.min_x, strip.crop.max_x, strip.crop.min_y, strip.crop.max_y)))
|
||||
|
||||
threshold, missing, weak = compute_scene_threshold(scene_scores, known_frames)
|
||||
|
||||
if missing:
|
||||
self.report({"WARNING"},
|
||||
f"Known Cuts not found in source channel: {', '.join(str(f) for f in missing)}")
|
||||
|
||||
if weak:
|
||||
self.report({"WARNING"},
|
||||
"Known Cuts at noise level, skipped (dissolve or wrong frame?): "
|
||||
+ ', '.join(f"{frame} ({scene_scores[frame]:.4f})" for frame in weak))
|
||||
|
||||
if threshold is None:
|
||||
if not scene_scores:
|
||||
self.report({"ERROR"},
|
||||
f'Known Cuts: no frames scored on channel "{self.source_channel_name}", '
|
||||
f'check that it contains the movie strip')
|
||||
elif weak:
|
||||
self.report({"ERROR"},
|
||||
'Known Cuts: every found frame scores below noise level (0.01), '
|
||||
'threshold not calibrated')
|
||||
else:
|
||||
fps = bpy.context.scene.render.fps
|
||||
first, last = min(scene_scores), max(scene_scores)
|
||||
self.report({"ERROR"},
|
||||
f'Known Cuts: no matching frames (scored {len(scene_scores)} frames, '
|
||||
f'{first} to {last}, scene fps {fps})')
|
||||
return
|
||||
|
||||
self.animated_threshold = round(threshold, 4)
|
||||
cut_count = sum(1 for frame, score in scene_scores.items()
|
||||
if score > threshold and self.frame_start <= frame <= self.frame_end)
|
||||
scores_str = ', '.join(f"{frame}: {scene_scores[frame]:.4f}"
|
||||
for frame in known_frames
|
||||
if frame in scene_scores and frame not in weak)
|
||||
self.report({"INFO"},
|
||||
f"Calibrated threshold: {self.animated_threshold} "
|
||||
f"(known cuts: {scores_str}; {cut_count} cuts detected)")
|
||||
|
||||
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'}
|
||||
|
||||
if self.movie_type == 'ANIMATED' and self.known_cuts.strip():
|
||||
if self._parse_known_cuts() is None:
|
||||
self.report({"ERROR"}, "Known Cuts: expected frame numbers separated by ','")
|
||||
return {'CANCELLED'}
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
@@ -314,6 +395,9 @@ class VSETB_OT_scene_cut_detection(Operator):
|
||||
if old_strip.name.startswith("tmp_shot_"):
|
||||
scn.sequence_editor.strips.remove(old_strip)
|
||||
|
||||
if self.movie_type == 'ANIMATED' and self.known_cuts.strip():
|
||||
self._calibrate_threshold()
|
||||
|
||||
strips = get_strips(channel=self.source_channel_name)
|
||||
|
||||
i = 1
|
||||
|
||||
Reference in New Issue
Block a user