From e924519f44a16cabf6c17a0e7852ed07c3907175 Mon Sep 17 00:00:00 2001 From: Stephen Leger Date: Sat, 15 Apr 2017 18:16:41 +0200 Subject: Initial release --- space_clip_editor_autotracker.py | 348 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 space_clip_editor_autotracker.py diff --git a/space_clip_editor_autotracker.py b/space_clip_editor_autotracker.py new file mode 100644 index 00000000..56c2bcae --- /dev/null +++ b/space_clip_editor_autotracker.py @@ -0,0 +1,348 @@ +bl_info = { + "name": "Autotrack", + "author": "Miika Puustinen, Matti Kaihola", + "version": (0, 0, 95), + "blender": (2, 78, 0), + "location": "Movie clip Editor > Tools Panel > Autotrack", + "description": "Motion Tracking with automatic feature detection.", + "warning": "", + "wiki_url": "https://github.com/miikapuustinen/blender_autotracker", + "category": "Motion Tracking", + } + + +import bpy +import math + + +class AutotrackerOperator(bpy.types.Operator): + """Autotrack. Esc to cancel.""" + bl_idname = "wm.modal_timer_operator" + bl_label = "Modal Timer Operator" + + limits = bpy.props.IntProperty(default=0) + _timer = None + + def active_clip(self): + area = [i for i in bpy.context.screen.areas if i.type == 'CLIP_EDITOR'][0] + clip = area.spaces.active.clip.name + return clip + + # DETECT FEATURES + def auto_features(self, delete_threshold, limits): + tracks = [] + selected = [] + old = [] + to_delete = [] + + bpy.ops.clip.select_all(action='DESELECT') + + # Detect Features + bpy.ops.clip.detect_features( + threshold=bpy.context.scene.autotracker_props.df_threshold, + min_distance=bpy.context.scene.autotracker_props.df_distance, + margin=bpy.context.scene.autotracker_props.df_margin, + placement=bpy.context.scene.autotracker_props.placement_list + ) + + current_frame = bpy.context.scene.frame_current + + tracks = bpy.data.movieclips[self.active_clip()].tracking.tracks + for track in tracks: + if track.markers.find_frame(current_frame) is not None: + if track.select is not True and track.hide is False and track.markers.find_frame(current_frame).mute is False: + old.append(track) + if track.select is True: + selected.append(track) + + # Select overlapping new markers + for i in selected: + for j in old: + i_marker = i.markers.find_frame(current_frame) + j_marker = j.markers.find_frame(current_frame) + distance = math.sqrt(((i_marker.co[0] - j_marker.co[0])**2) + ((i_marker.co[1] - j_marker.co[1])**2)) + + if distance < delete_threshold: + to_delete.append(i) + break + + # delete short tracks + for track in tracks: + muted = [] + active = [] + # print(track) + for marker in track.markers: + if marker.mute is True: + muted.append(marker) + else: + active.append(marker) + if len(muted) > 3 and len(active) < 1: + to_delete.append(track) + + if len(track.markers) > 1 and len(active) == 0: + to_delete.append(track) + + # Delete Overlapping Markers + bpy.ops.clip.select_all(action='DESELECT') + for track in tracks: + if track in to_delete: + track.select = True + + bpy.ops.clip.delete_track() + + print(str(len(selected)) + "/" + str(len(tracks)) + " tracks tracking.") + + # AUTOTRACK FRAMES + def track_frames_backward(self): + bpy.ops.clip.track_markers(backwards=True, sequence=False) + + def track_frames_forward(self): + bpy.ops.clip.track_markers(backwards=False, sequence=False) + + # REMOVE BAD MARKERS + def remove_extra(self, jump_cut, track_backwards): + trackers = [] + + if track_backwards is True: + one_frame = -1 + two_frames = -2 + else: + one_frame = 1 + two_frames = 2 + + if self.limits >= 3: + trackers = bpy.data.movieclips[self.active_clip()].tracking.tracks + + for i in trackers: + if len(i.markers) > 5: + current_frame = bpy.context.scene.frame_current + + if (i.markers.find_frame(current_frame) is not None and + i.markers.find_frame(current_frame - one_frame) is not None and + i.markers.find_frame(current_frame - two_frames) is not None): + + key_frame = i.markers.find_frame(current_frame).co + prev_frame = i.markers.find_frame(current_frame - one_frame).co + distance = math.sqrt(((key_frame[0] - prev_frame[0])**2) + ((key_frame[1] - prev_frame[1])**2)) + # Jump Cut threshold + if distance > jump_cut: + if (i.markers.find_frame(current_frame) is not None and + i.markers.find_frame(current_frame - one_frame) is not None): + + # create new track to new pos + new_track = \ + bpy.data.movieclips[self.active_clip()].tracking.tracks.new(frame=current_frame) + new_track.markers[0].co = i.markers.find_frame(current_frame).co + i.markers.find_frame(current_frame).mute = True + i.markers.find_frame(current_frame - one_frame).mute = True + + def modal(self, context, event): + scene = bpy.context.scene + if (event.type in {'ESC'} or scene.frame_current == scene.frame_end + 1 or + scene.frame_current == scene.frame_start - 1): + self.limits = 0 + self.cancel(context) + return {'FINISHED'} + + if event.type == 'TIMER': + + # PROP VARIABLES + delete_threshold = bpy.context.scene.autotracker_props.delete_threshold + endframe = bpy.context.scene.frame_end + start_frame = bpy.context.scene.frame_start + frame_separate = bpy.context.scene.autotracker_props.frame_separation + margin = bpy.context.scene.autotracker_props.df_margin + distance = bpy.context.scene.autotracker_props.df_distance + threshold = bpy.context.scene.autotracker_props.df_threshold + jump_cut = bpy.context.scene.autotracker_props.jump_cut + track_backwards = bpy.context.scene.autotracker_props.track_backwards + + # Auto features every frame separate step + if bpy.context.scene.frame_current % frame_separate == 0 or self.limits == 0: + limits = self.limits + self.auto_features(delete_threshold, limits) + + # Select all trackers for tracking + select_all = bpy.ops.clip.select_all(action='SELECT') + tracks = bpy.data.movieclips[self.active_clip()].tracking.tracks + active_tracks = [] + for track in tracks: + if track.lock is True: + track.select = False + else: + active_tracks.append(track) + + # Forwards or backwards tracking + if track_backwards is True: + if len(active_tracks) == 0: + print("No new tracks created. Doing nothing.") + self.limits = 0 + self.cancel(context) + return {'FINISHED'} + else: + self.track_frames_backward() + else: + if len(active_tracks) == 0: + print("No new tracks created. Doing nothing.") + self.limits = 0 + self.cancel(context) + return {'FINISHED'} + else: + self.track_frames_forward() + + # Remove bad tracks + self.remove_extra(jump_cut, track_backwards) + + self.limits += 1 + + return {'PASS_THROUGH'} + + def execute(self, context): + wm = context.window_manager + self._timer = wm.event_timer_add(time_step=0.5, window=context.window) + wm.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def cancel(self, context): + wm = context.window_manager + wm.event_timer_remove(self._timer) + + +# UI CREATION # + +class Autotracker_UI(bpy.types.Panel): + """Creates a Panel in the Render Layer properties window""" + bl_label = "Autotrack" + bl_idname = "autotrack" + bl_space_type = 'CLIP_EDITOR' + bl_region_type = 'TOOLS' + bl_category = "Track" + + # Draw UI + def draw(self, context): + layout = self.layout + + row = layout.row(align=True) + row.scale_y = 1.5 + + props = row.operator("wm.modal_timer_operator", text="Autotrack! ", icon='PLAY') + + row = layout.row(align=True) + row.prop(context.scene.autotracker_props, "track_backwards") + + row = layout.row(align=True) # make next row + row.prop(context.scene.autotracker_props, "delete_threshold") + + row = layout.row(align=True) + row.prop(context.scene.autotracker_props, "frame_separation", text="Frame Separation") + + row = layout.row(align=True) + row.prop(context.scene.autotracker_props, "jump_cut", text="Jump Threshold") + + row = layout.row(align=True) + row.label(text="Detect Features Settings:") + + row = layout.row(align=True) + row.prop(context.scene.autotracker_props, "df_margin", text="Margin:") + + row = layout.row(align=True) + row.prop(context.scene.autotracker_props, "df_threshold", text="Threshold:") + + row = layout.row(align=True) + row.prop(context.scene.autotracker_props, "df_distance", text="Distance:") + + row = layout.row(align=True) + row.label(text="Feature Placement:") + + row = layout.row(align=True) + row.prop(context.scene.autotracker_props, "placement_list") + + +class AutotrackerSettings(bpy.types.PropertyGroup): + """Create properties""" + df_margin = bpy.props.IntProperty( + name="Detect Features Margin", + description="Only features further margin pixels from the image edges are considered.", + default=16, + min=0, + max=2000 + ) + df_threshold = bpy.props.FloatProperty( + name="Detect Features Threshold", + description="Threshold level to concider feature good enough for tracking.", + default=0.01, + min=0.0, + max=1.0 + ) + + df_distance = bpy.props.IntProperty( + name="Detect Features Distance", + description="Minimal distance accepted between two features.", + default=64, + min=1, + max=300 + ) + + delete_threshold = bpy.props.FloatProperty( + name="New Marker Threshold", + description="Threshold how near new features can appear during autotracking.", + default=0.1, + min=0.0, + max=1.0 + ) + + frame_separation = bpy.props.IntProperty( + name="Frame Separation", + description="How often new features are generated.", + default=5, + min=1, + max=100 + ) + + jump_cut = bpy.props.FloatProperty( + name="Jump Cut", + description="Distance how much a marker can travel before it is considered " + "to be a bad track and cut. A new track is added.", + default=0.1, + min=0.0, + max=1.0 + ) + + track_backwards = bpy.props.BoolProperty( + name="AutoTrack Backwards", + description="Autotrack backwards.", + default=False + ) + + # Dropdown menu + list_items = [ + ("FRAME", "Whole Frame", "", 1), + ("INSIDE_GPENCIL", "Inside Grease Pencil", "", 2), + ("OUTSIDE_GPENCIL", "Outside Grease Pencil", "", 3), + ] + + placement_list = bpy.props.EnumProperty( + name="", + description="Feaure Placement", + items=list_items + ) + + +# REGISTER BLOCK # +def register(): + bpy.utils.register_class(AutotrackerOperator) + bpy.utils.register_class(Autotracker_UI) + bpy.utils.register_class(AutotrackerSettings) + + bpy.types.Scene.autotracker_props = \ + bpy.props.PointerProperty(type=AutotrackerSettings) + + +def unregister(): + bpy.utils.unregister_class(AutotrackerOperator) + bpy.utils.unregister_class(Autotracker_UI) + bpy.utils.unregister_class(AutotrackerSettings) + + +if __name__ == "__main__": + register() -- cgit v1.2.3