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