Merge pull request 'shot_split_auto' (#6) from shot_split_auto into master

Reviewed-on: #6
Reviewed-by: christophe.seux <christophe@autourdeminuit.com>
master
florentin.luce 2024-05-28 15:27:01 +02:00
commit 8e21dcc6f5
6 changed files with 233 additions and 8 deletions

104
auto_splitter.py Normal file
View File

@ -0,0 +1,104 @@
import os
import re
import subprocess
import bpy
from vse_toolbox import bl_utils
def launch_split(movie_strip, threshold, frame_start=None, frame_end=None):
"""Launch ffmpeg command to detect changing frames from a movie strip.
Args:
movie_strip (bpy.types.Sequence): blender sequence strip to detect changes.
threshold (float): value of the detection factor (from 0 to 1).
frame_start (int, optional): first frame to detect.
Defaults to None.
frame_end (int, optional): last frame to detect.
Defaults to None.
Returns:
str: ffmpeg command log.
"""
path = bl_utils.abspath(movie_strip.filepath)
fps = bpy.context.scene.render.fps
if frame_start is None:
frame_start = 0
if frame_end is None:
frame_end = movie_strip.frame_duration
frame_start = frame_start - movie_strip.frame_start
#frame_start += movie_strip.frame_offset_start
#frame_end -= movie_strip.frame_offset_end
# Launch ffmpeg command to split
ffmpeg_cmd = get_command(str(path), threshold, frame_start, frame_end, fps)
print(ffmpeg_cmd)
process = subprocess.Popen(
ffmpeg_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
return process
def get_command(path, threshold, frame_start, frame_end, fps):
"""Generate the ffmpeg command which detect change from a movie.
Args:
path (_type_): path to detect changes.
threshold (_type_): value of the detection factor (from 0 to 1).
frame_start (_type_): first frame to detect.
frame_end (_type_): last frame to detect.
fps (_type_): framerate of the movie.
Returns:
list: ffmpeg command as list for subprocess module.
"""
start_time = frame_start/fps
end_time = frame_end/fps
return [
'ffmpeg',
'-i',
str(path),
'-vf',
f"trim=start={start_time}:end={end_time}, select='gt(scene, {threshold})',showinfo",
'-f',
'null',
'-'
]
def get_split_time(log, as_frame=True, fps=None):
"""Parse ffmpeg command lines to detect the timecode
Args:
log (str): log to parse.
as_frame (bool, optional): if wanted the timecode as frame number.
Defaults to True.
fps (_type_, optional): framerate of the movie (mandatory if as_frame used).
Defaults to None.
Returns:
_type_: _description_
"""
timecodes = re.findall(r'pts_time:([\d.]+)', log)
if not timecodes:
return
timecode = timecodes[0]
if as_frame:
# convert timecode to frame number
return round(float(timecode) * fps)
return timecode

View File

@ -36,4 +36,4 @@ ASSET_PREVIEWS = bpy.utils.previews.new()
CASTING_BUFFER = CONFIG_DIR / 'casting.json'
SPREADSHEET = []
SPREADSHEET = []

View File

@ -89,9 +89,9 @@ def norm_name(string, separator='_', format=str.lower, padding=0):
return string
def read_file(path):
'''Read a file with an extension in (json, yaml, yml, txt)'''
'''Read a file with an extension in (json, yaml, yml, txt, log)'''
exts = ('.json', '.yaml', '.yml', '.txt')
exts = ('.json', '.yaml', '.yml', '.txt', '.log')
if not path:
print('Try to read empty file')

View File

@ -2,13 +2,15 @@ from os.path import expandvars, abspath
from pathlib import Path
import bpy
from bpy.types import Operator
from bpy.props import (BoolProperty, StringProperty, EnumProperty)
from bpy.props import (BoolProperty, StringProperty, FloatProperty,
IntProperty, EnumProperty)
from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels,
get_channel_index, new_text_strip, get_strip_at, get_channel_name)
get_channel_index, new_text_strip, get_strip_at, get_channel_name,
create_shot_strip)
from vse_toolbox import auto_splitter
from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings
from vse_toolbox.constants import REVIEW_TEMPLATE_BLEND
from shutil import copy2
@ -172,6 +174,108 @@ class VSETB_OT_set_sequencer(Operator):
return {"FINISHED"}
class VSETB_OT_auto_split(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_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(
items=lambda self, ctx: ((c.name, c.name, '') for c in ctx.scene.sequence_editor.channels),
name='Movie Channel')
def invoke(self, context, event):
self.frame_first = context.scene.frame_start
self.frame_last = 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])
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(self, 'threshold')
col.prop(self, 'movie_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')
def execute(self, context):
context.window_manager.modal_handler_add(self)
return {'PASS_THROUGH'}
def modal(self, context, event):
strips = get_strips(channel=self.movie_channel_name)
i = 1
frame_start = self.frame_first
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:
continue
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)
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
frame_end += int(strip.frame_final_start)
if frame_end > self.frame_last:
frame_end = self.frame_last
create_shot_strip(
f'tmp_shot_{str(i).zfill(3)}',
start=frame_start,
end=frame_end
)
i += 1
frame_start = frame_end
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
process.wait()
# Last strip:
if frame_start < self.frame_last:
create_shot_strip(
f'tmp_shot_{str(i).zfill(3)}',
start=frame_start,
end=self.frame_last
)
return {'FINISHED'}
class VSETB_OT_set_stamps(Operator):
bl_idname = "vse_toolbox.set_stamps"
bl_label = "Set Stamps"
@ -625,6 +729,7 @@ def unregister_keymaps():
classes = (
VSETB_OT_rename,
VSETB_OT_set_sequencer,
VSETB_OT_auto_split,
VSETB_OT_set_stamps,
VSETB_OT_show_waveform,
VSETB_OT_previous_shot,

View File

@ -582,4 +582,19 @@ def update_text_strips(scene):
strip['text_pattern'] = strip.text
if 'text_pattern' in strip.keys():
strip.text = strip['text_pattern'].format_map(MissingKey(**format_data))
strip.text = strip['text_pattern'].format_map(MissingKey(**format_data))
def create_shot_strip(name, start, end):
shot_strip = bpy.context.scene.sequence_editor.sequences.new_effect(
name,
'COLOR',
get_channel_index('Shots'),
frame_start=start,
frame_end=end
)
shot_strip.blend_alpha = 0
shot_strip.color = (0.5, 0.5, 0.5)
return shot_strip

View File

@ -179,6 +179,7 @@ class VSETB_PT_sequencer(VSETB_main, Panel):
col = layout.column()
col.operator('vse_toolbox.set_sequencer', text='Set-Up Sequencer', icon='SEQ_SEQUENCER')
col.operator('vse_toolbox.auto_split', text='Auto Split Shots')
col.operator('vse_toolbox.strips_rename', text=f'Rename {channel}', icon='SORTALPHA')
col.operator('vse_toolbox.set_stamps', text='Set Stamps', icon='COLOR')
col.operator("vse_toolbox.collect_files", text='Collect Files', icon='PACKAGE')