diff --git a/space_clip_editor_auto_track.py b/space_clip_editor_auto_track.py new file mode 100644 index 000000000..841838b4f --- /dev/null +++ b/space_clip_editor_auto_track.py @@ -0,0 +1,377 @@ +# SPDX-FileCopyrightText: 2017-2022 Blender Foundation +# +# SPDX-License-Identifier: GPL-2.0-or-later + +# Re-write of Miika Puustinen's blender 2.7 autotracker +# https://github.com/miikapuustinen/blender_autotracker + +import time +import math +import bpy +from bpy.types import Operator, Panel, Scene +from bpy.props import ( + IntProperty, + FloatProperty, + EnumProperty +) + +bl_info = { + 'name': 'Auto-track', + 'author': 'mcd1992', + 'license': 'GPL', + 'version': (1, 0, 0), + 'blender': (3, 1, 0), + 'location': 'Movie Clip Editor > Tracking > Clip > Toolbar', + 'description': 'VFX motion tracking automation.', + 'warning': '', + 'doc_url': '{BLENDER_MANUAL_URL}/addons/video_tools/auto_track.html', + 'category': 'Video Tools', +} + + +class CLIP_OT_autotrack_autotrack(Operator): + bl_idname = 'autotrack.auto_track' + bl_label = 'Auto Track' + bl_description = 'Automatically use Detect Features and filtering to motion track the timeline forward' + bl_options = {'REGISTER', 'UNDO', 'BLOCKING', 'PRESET'} + + _frame_changed = False + _frame_redetect = 2**64 + + def _frame_change_event(self, scene, depsgraph): + self._frame_changed = True + + @classmethod + def poll(cls, context): + return (context.area.spaces.active.clip is not None) + + def execute(self, context): + time_start = time.time() + scene = context.scene + # wm = context.window_manager + clip = context.area.spaces.active.clip + tracks = clip.tracking.tracks + current_frame = scene.frame_current + # clip_end = clip.frame_start + clip.frame_duration + # clip_start = clip.frame_start + + # Filter short tracks + context.area.spaces.active.show_disabled = True # Something weird is happening with muted/hidden trackers + filtered_trackers = [] + for track in tracks: + if track.hide or track.lock: + continue + marker = track.markers.find_frame(current_frame - scene.autotrack_rate, exact=True) + if marker and len(track.markers) < scene.autotrack_filter_mintime: + filtered_trackers.append(track) + # print("remove short %s of len %s [%s]" % ( + # track.name, len(track.markers), marker.frame + # )) + bpy.ops.clip.select_all(action='DESELECT') + for track in filtered_trackers: + track.select = True + bpy.ops.clip.delete_track() + print('Filtered %s short trackers in %.4f sec' % (len(filtered_trackers), time.time() - time_start)) + filtered_trackers.clear() + + # Detect new features + bpy.ops.clip.select_all(action='DESELECT') + bpy.ops.clip.detect_features( + threshold=scene.autotrack_detect_threshold, + min_distance=scene.autotrack_detect_distance, + margin=scene.autotrack_detect_margin, + placement=scene.autotrack_detect_placement + ) + + # Store new trackers + new_trackers = [] + for track in tracks: + if track.select: + new_trackers.append(track) + track.frames_limit = scene.autotrack_rate + print('Frame %s detected %s features in %.4f sec' % + (current_frame, len(new_trackers), time.time() - time_start)) + + # Store other, non disabled/hidden/locked, trackers on this frame + bpy.ops.clip.select_all(action='INVERT') + old_trackers = [] + for track in tracks: + if track.select and not (track.hide or track.lock): + marker = track.markers.find_frame(current_frame, exact=True) + if marker: + if marker.mute: # marker is 'muted' aka disabled this frame + continue + old_trackers.append(track) + + # Filter overlapping trackers + filtered_trackers = [] + time_start = time.time() + diaglen = math.sqrt(clip.size[0]**2 + clip.size[1]**2) + for new_track in new_trackers: + new_marker = new_track.markers.find_frame(current_frame, exact=True) + if new_marker: + for old_track in old_trackers: + old_marker = old_track.markers.find_frame(current_frame, exact=True) + if old_marker: + distance = (new_marker.co - old_marker.co).length * diaglen + if distance < scene.autotrack_detect_distance: + # print("dist %s %s %s %s" % ( + # new_track.name, old_track.name, distance, + # (new_marker.co - old_marker.co).length + # )) + filtered_trackers.append(new_track) + bpy.ops.clip.select_all(action='DESELECT') + for track in filtered_trackers: + track.select = True + bpy.ops.clip.delete_track() + print('Filtered %s overlapping trackers in %.4f sec' % (len(filtered_trackers), time.time() - time_start)) + filtered_trackers.clear() + + # Start tracking + context.area.spaces.active.show_disabled = False # Hide disabled trackers when tracking + bpy.ops.clip.select_all(action='SELECT') + bpy.ops.clip.track_markers('INVOKE_DEFAULT', backwards=False, sequence=True) + print('Tracking %s features' % (len(new_trackers))) + + # wm.progress_update(1.0) + # wm.progress_end() + self._frame_redetect = current_frame + scene.autotrack_rate + return {'FINISHED'} + + def modal(self, context, event): + if event.type in {'ESC'}: + print('Cancelling...') + self.cancel(context) + print('Canceled') + return {'CANCELLED'} + + if event.type == 'TIMER': + if context.scene.frame_current >= context.scene.frame_end: + print('End of clip') + self.cancel(context) + return {'FINISHED'} + if self._frame_changed: + self._frame_changed = False + self.execute(context) + return {'PASS_THROUGH'} + return {'RUNNING_MODAL'} + + def invoke(self, context, event): + wm = context.window_manager + # wm.show_progress_widget(0.0, 1.0) + wm.modal_handler_add(self) + bpy.app.handlers.frame_change_post.append(self._frame_change_event) + self._timer = wm.event_timer_add(time_step=2, window=context.window) + self._frame_changed = False + self.execute(context) + return {'RUNNING_MODAL'} + + def cancel(self, context): + bpy.app.handlers.frame_change_post.remove(self._frame_change_event) + wm = context.window_manager + wm.event_timer_remove(self._timer) + for track in context.area.spaces.active.clip.tracking.tracks: + track.frames_limit = 0 + + +class CLIP_OT_autotrack_filter(Operator): + bl_idname = 'autotrack.filter' + bl_label = 'Filter All Tracks' + bl_description = 'Apply filters to all tracks' + bl_options = {'REGISTER', 'UNDO', 'BLOCKING', 'PRESET'} + + @classmethod + def poll(cls, context): + return (context.area.spaces.active.clip is not None) + + def execute(self, context): + scene = context.scene + clip = context.area.spaces.active.clip + tracks = clip.tracking.tracks + time_start = time.time() + filtered_trackers = [] + + bpy.ops.clip.filter_tracks( + track_threshold=scene.autotrack_filter_threshold, + ) + for track in tracks: + if track.select: + filtered_trackers.append(track) + + print('Filtered %s tracks in %.4f sec' % (len(filtered_trackers), time.time() - time_start)) + return {'FINISHED'} + + def modal(self, context, event): + self.execute(context) + return {'FINISHED'} + + def invoke(self, context, event): + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + +class CLIP_PT_autotrack_main(Panel): + bl_label = 'Auto-track' + bl_space_type = 'CLIP_EDITOR' + bl_region_type = 'TOOLS' + bl_category = 'Auto-track' + + def draw(self, context): + scene = context.scene + layout = self.layout + layout.use_property_split = True # Compact Label + Value + layout.use_property_decorate = False # Keyframe diamond ? + + col = layout.column(align=True) + col.scale_y = 1.5 + col.operator('autotrack.auto_track', text='Autotrack', icon='CON_FOLLOWTRACK') + col = layout.column(align=True) + col.prop(scene, "autotrack_rate") + + +class CLIP_PT_autotrack_tracker_settings(Panel): + bl_label = 'Tracking Settings' + bl_space_type = 'CLIP_EDITOR' + bl_region_type = 'TOOLS' + bl_category = 'Auto-track' + + def draw(self, context): + layout = self.layout + layout.use_property_split = True # Compact Label + Value + layout.use_property_decorate = False # Keyframe diamond ? + sc = context.space_data + clip = sc.clip + settings = clip.tracking.settings + + col = layout.column(align=True) + col.prop(settings, "default_pattern_size") + col.prop(settings, "default_search_size") + col.separator() + col.prop(settings, "default_motion_model") + col.prop(settings, "default_pattern_match", text="Match") + col.prop(settings, "use_default_brute") + col.prop(settings, "use_default_normalization") + col = layout.column(align=True) + col.prop(settings, "default_correlation_min") + col.prop(settings, "default_margin") + + +class CLIP_PT_autotrack_detect_settings(Panel): + bl_label = 'Feature Detection Settings' + bl_space_type = 'CLIP_EDITOR' + bl_region_type = 'TOOLS' + bl_category = 'Auto-track' + + def draw(self, context): + scene = context.scene + layout = self.layout + layout.use_property_split = True # Compact Label + Value + layout.use_property_decorate = False # Keyframe diamond ? + + col = layout.column(align=True) + col.prop(scene, 'autotrack_detect_margin') + col.prop(scene, 'autotrack_detect_threshold') + col.prop(scene, 'autotrack_detect_distance') + col.prop(scene, 'autotrack_detect_placement') + + +class CLIP_PT_autotrack_filter_settings(Panel): + bl_label = 'Filter Settings' + bl_space_type = 'CLIP_EDITOR' + bl_region_type = 'TOOLS' + bl_category = 'Auto-track' + + def draw(self, context): + scene = context.scene + layout = self.layout + layout.use_property_split = True # Compact Label + Value + layout.use_property_decorate = False # Keyframe diamond ? + col = layout.column(align=True) + col.scale_y = 1.5 + col.operator('autotrack.filter', text='Filter All Tracks', icon='FILTER') + + col = layout.column(align=True) + col.prop(scene, "autotrack_filter_threshold") + col.prop(scene, 'autotrack_filter_mintime') + + +classes = ( + CLIP_OT_autotrack_autotrack, + CLIP_OT_autotrack_filter, + CLIP_PT_autotrack_main, + CLIP_PT_autotrack_tracker_settings, + CLIP_PT_autotrack_detect_settings, + CLIP_PT_autotrack_filter_settings +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + # Autotrack Properties + Scene.autotrack_rate = IntProperty( + name='Rate', + description='Detect new features every X frames', + default=30, + min=1 + ) + + # Feature Detection Properties + Scene.autotrack_detect_margin = IntProperty( + name='Margin', + description='Distance from edge of image detected features must be', + subtype='PIXEL', + default=0, + min=0 + ) + Scene.autotrack_detect_threshold = FloatProperty( + name='Threshold', + description='Minimum threshold value for a feature to be considered', + precision=3, + default=0.1, + min=0.001, + ) + Scene.autotrack_detect_distance = IntProperty( + name='Distance', + description='Minimum distance detected features must be from each other', + subtype='PIXEL', + default=60, + min=5 + ) + Scene.autotrack_detect_placement = EnumProperty( + name='Allowed Placement', + description='Allowed areas to detect new features', + items=( + ("FRAME", "Whole Frame", "The entire frame can be used for feature detection"), + ("INSIDE_GPENCIL", "Inside Grease Pencil", + "Only areas inside the grease mask can be used for feature detection"), + ("OUTSIDE_GPENCIL", "Outside Grease Pencil", + "Only areas outside the grease mask can be used for feature detection") + ), + default='FRAME' + ) + + # Filter Properties + Scene.autotrack_filter_threshold = FloatProperty( + name='Threshold', + description='Threshold for builtin filter on all tracks (Lower value means more strict)', + precision=3, + default=10.0, + min=0.0, + ) + Scene.autotrack_filter_mintime = IntProperty( + name='Minimum Track Time', + description='Minimum amount of frames a tracker should have a valid track to be kept', + default=30, + min=0 + ) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + +if __name__ == '__main__': + register()