Video Tools: Add auto-track addon #104927

Open
mcd1992 wants to merge 1 commits from mcd1992/blender-addons:autotracker into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.

View File

@ -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()