├── README.md ├── __init__.py └── shot_detect_yt.png /README.md: -------------------------------------------------------------------------------- 1 | [](https://discord.gg/HMYpnPzbTm) ![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/tintwotin) 2 | # Shot Detection in the Blender Video Sequence Editor 3 | 4 | [![IMAGE ALT TEXT HERE](https://github.com/tin2tin/shot_detection/blob/main/shot_detect_yt.png?raw=true)](https://www.youtube.com/watch?v=zWNQMII-IAE) 5 | 6 | ![alt text](https://blender.chat/file-upload/AJKEtutaWrs527Wwv/Cam.gif) 7 | 8 | Detected video: 'Caminandes : Llamigos' by Pablo Vazquez, Blender Studio 9 | 10 | ## Usage 11 | When installed, the operator, "Detect Shots & Split Strips", is exposed in the bottom of the Strip Menu. 12 | The active strip with white outline will be used for detection, and all selected strips will be split at the same points. 13 | Ex. use the movie strip as the active selected strip and the audio strip as selected strip, then both will be split at the same points. 14 | 15 | ## Installation 16 | Install the add-on as any Blender add-on. 17 | It is depenadant on this python lib: https://github.com/Breakthrough/PySceneDetect by Brandon Castellano. It should be installed automatically. If not, then this add-on on can be used to install it: https://github.com/amb/blender_pip 18 | 19 | ## Join Through Splits 20 | Handy add-on for joining strips: https://github.com/tin2tin/join_through_splits 21 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Detect Shots and Split Strips", 21 | "author": "Tintwotin, Brandon Castellano(PySceneDetect-module)", 22 | "version": (1, 0), 23 | "blender": (2, 90, 0), 24 | "location": "Sequencer > Strip Menu or Context Menu", 25 | "description": "Detect shots in active strip and split all selected strips accordingly.", 26 | "warning": "", 27 | "doc_url": "", 28 | "category": "Sequencer", 29 | } 30 | 31 | import bpy, subprocess, os, sys 32 | from bpy.types import Operator 33 | from bpy.props import ( 34 | IntProperty, 35 | BoolProperty, 36 | EnumProperty, 37 | StringProperty, 38 | FloatProperty, 39 | ) 40 | #import site 41 | #app_path = site.USER_SITE 42 | #if app_path not in sys.path: 43 | # sys.path.append(app_path) 44 | 45 | def find_scenes(video_path, threshold, start, end): 46 | pybin = sys.executable # bpy.app.binary_path_python # Use for 2.83 47 | try: 48 | subprocess.call([pybin, "-m", "ensurepip"]) 49 | except ImportError: 50 | pass 51 | try: 52 | from scenedetect import open_video#, detect 53 | from scenedetect import SceneManager 54 | from scenedetect.detectors import ContentDetector 55 | except ImportError: 56 | subprocess.check_call([pybin, "-m", "pip", "install", "scenedetect[opencv]"]) 57 | from scenedetect import open_video#, detect 58 | from scenedetect import SceneManager 59 | from scenedetect.detectors import ContentDetector 60 | 61 | render = bpy.context.scene.render 62 | fps = round((render.fps / render.fps_base), 3) 63 | video = open_video(video_path,framerate=fps) 64 | scene_manager = SceneManager() 65 | scene_manager.add_detector(ContentDetector(threshold=threshold)) 66 | video.seek((start/fps)) 67 | scene_manager.detect_scenes(video, end_time=(end/fps)) 68 | 69 | return scene_manager.get_scene_list() 70 | 71 | 72 | class SEQUENCER_OT_split_selected(bpy.types.Operator): 73 | """Split Unlocked Un/Seleted Strips Soft""" 74 | 75 | bl_idname = "sequencer.split_selected" 76 | bl_label = "Split Selected" 77 | bl_options = {"REGISTER", "UNDO"} 78 | 79 | type: EnumProperty( 80 | name="Type", 81 | description="Split Type", 82 | items=( 83 | ("SOFT", "Soft", "Split Soft"), 84 | ("HARD", "Hard", "Split Hard"), 85 | ), 86 | ) 87 | 88 | @classmethod 89 | def poll(cls, context): 90 | if context.sequences: 91 | return True 92 | return False 93 | 94 | def execute(self, context): 95 | selection = context.selected_sequences 96 | sequences = bpy.context.scene.sequence_editor.sequences_all 97 | cf = bpy.context.scene.frame_current 98 | at_cursor = [] 99 | cut_selected = False 100 | 101 | # find unlocked strips at cursor 102 | for s in sequences: 103 | if s.frame_final_start <= cf and s.frame_final_end > cf: 104 | if s.lock == False: 105 | at_cursor.append(s) 106 | if s.select == True: 107 | cut_selected = True 108 | for s in at_cursor: 109 | if cut_selected: 110 | if s.select: # only cut selected 111 | bpy.ops.sequencer.select_all(action="DESELECT") 112 | s.select = True 113 | bpy.ops.sequencer.split( 114 | frame=bpy.context.scene.frame_current, 115 | type=self.type, 116 | side="RIGHT", 117 | ) 118 | 119 | # add new strip to selection 120 | for i in bpy.context.scene.sequence_editor.sequences_all: 121 | if i.select: 122 | selection.append(i) 123 | bpy.ops.sequencer.select_all(action="DESELECT") 124 | for s in selection: 125 | s.select = True 126 | return {"FINISHED"} 127 | 128 | 129 | class SEQUENCER_OT_detect_shots(Operator): 130 | """Detect shots in active strip and split all selected strips accordingly""" 131 | 132 | bl_idname = "sequencer.detect_shots" 133 | bl_label = "Detect Shots & Split Strips" 134 | bl_options = {"REGISTER", "UNDO"} 135 | 136 | @classmethod 137 | def poll(cls, context): 138 | if ( 139 | context.scene 140 | and context.scene.sequence_editor 141 | and context.scene.sequence_editor.active_strip 142 | ): 143 | return context.scene.sequence_editor.active_strip.type == "MOVIE" 144 | else: 145 | return False 146 | 147 | def execute(self, context): 148 | scene = context.scene 149 | sequencer = bpy.ops.sequencer 150 | cf = context.scene.frame_current 151 | path = context.scene.sequence_editor.active_strip.filepath 152 | path = (os.path.realpath(bpy.path.abspath(path))).replace("\\", "\\\\") 153 | 154 | msg = "Please wait. Detecting shots in "+str(path)+"." 155 | self.report({'INFO'}, msg) 156 | 157 | path = path.replace("\\", "\\\\") 158 | active = context.scene.sequence_editor.active_strip 159 | start_time = active.frame_offset_start 160 | end_time = active.frame_duration - active.frame_offset_end 161 | scenes = find_scenes(path, 32, start_time, end_time) 162 | for i, scene in enumerate(scenes): 163 | context.scene.frame_current = int(scene[1].get_frames()+active.frame_start) 164 | sequencer.split_selected() 165 | 166 | context.scene.frame_current = cf 167 | 168 | msg = "Finished: Shot detection and strip splitting." 169 | self.report({'INFO'}, msg) 170 | return {'FINISHED'} 171 | 172 | 173 | def menu_detect_shots(self, context): 174 | self.layout.separator() 175 | self.layout.operator("sequencer.detect_shots") 176 | 177 | 178 | classes = ( 179 | SEQUENCER_OT_detect_shots, 180 | SEQUENCER_OT_split_selected, 181 | ) 182 | 183 | 184 | def register(): 185 | for cls in classes: 186 | bpy.utils.register_class(cls) 187 | bpy.types.SEQUENCER_MT_context_menu.append(menu_detect_shots) 188 | bpy.types.SEQUENCER_MT_strip.append(menu_detect_shots) 189 | 190 | 191 | def unregister(): 192 | for cls in reversed(classes): 193 | bpy.utils.unregister_class(cls) 194 | bpy.types.SEQUENCER_MT_context_menu.remove(menu_detect_shots) 195 | bpy.types.SEQUENCER_MT_strip.remove(menu_detect_shots) 196 | 197 | 198 | if __name__ == "__main__": 199 | register() 200 | -------------------------------------------------------------------------------- /shot_detect_yt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tin2tin/Shot_Detection/c8787a6f5bc9db46b882171f9f352ecab80aecfb/shot_detect_yt.png --------------------------------------------------------------------------------