Video Tools: Add auto-track addon #104927
377
space_clip_editor_auto_track.py
Normal file
377
space_clip_editor_auto_track.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user