From 88918dcef321754ebb0a1859e88b2d538f7c87d4 Mon Sep 17 00:00:00 2001 From: lijenstina Date: Sun, 23 Jul 2017 15:02:05 +0200 Subject: Autotrack: Cleanup, add some report messages Bumped version to 0.1.1 Pep8 cleanup Imports as tuples, remove unused ones Simplify the UI code Change the Cancel tip to Stop as it has different meaning for Operators (as the results are kept) Use a debug_print instead of regular print enabled / disabled by a DEBUG global as from the code it seems that is for debugging purposes Add some operator report messages to be more clear what happens --- space_clip_editor_autotracker.py | 397 +++++++++++++++++++++------------------ 1 file changed, 213 insertions(+), 184 deletions(-) (limited to 'space_clip_editor_autotracker.py') diff --git a/space_clip_editor_autotracker.py b/space_clip_editor_autotracker.py index 9dea7e54..1cd20886 100644 --- a/space_clip_editor_autotracker.py +++ b/space_clip_editor_autotracker.py @@ -19,7 +19,7 @@ bl_info = { "name": "Autotrack", "author": "Miika Puustinen, Matti Kaihola, Stephen Leger", - "version": (0, 1, 0), + "version": (0, 1, 1), "blender": (2, 78, 0), "location": "Movie clip Editor > Tools Panel > Autotrack", "description": "Motion Tracking with automatic feature detection.", @@ -31,35 +31,58 @@ bl_info = { import bpy import bgl import blf -import math -from mathutils import Vector -from bpy.types import Operator, Panel, PropertyGroup, WindowManager -from bpy.props import BoolProperty, FloatProperty, IntProperty, EnumProperty, PointerProperty +from bpy.types import ( + Operator, + Panel, + PropertyGroup, + WindowManager, + ) +from bpy.props import ( + BoolProperty, + FloatProperty, + IntProperty, + EnumProperty, + PointerProperty, + ) -# for debug purpose +# for debug purposes import time +# set to True to enable debug prints +DEBUG = False + + +# pass variables just like for the regular prints +def debug_print(*args, **kwargs): + global DEBUG + if DEBUG: + print(*args, **kwargs) + + # http://blenderscripting.blogspot.ch/2011/07/bgl-drawing-with-opengl-onto-blender-25.html class GlDrawOnScreen(): black = (0.0, 0.0, 0.0, 0.7) white = (1.0, 1.0, 1.0, 0.5) progress_colour = (0.2, 0.7, 0.2, 0.5) + def String(self, text, x, y, size, colour): ''' my_string : the text we want to print pos_x, pos_y : coordinates in integer values size : font height. colour : used for definining the colour''' - dpi, font_id = 72, 0 # dirty fast assignment + dpi, font_id = 72, 0 # dirty fast assignment bgl.glColor4f(*colour) blf.position(font_id, x, y, 0) blf.size(font_id, size, dpi) blf.draw(font_id, text) + def _end(self): bgl.glEnd() bgl.glPopAttrib() bgl.glLineWidth(1) bgl.glDisable(bgl.GL_BLEND) bgl.glColor4f(0.0, 0.0, 0.0, 1.0) + def _start_line(self, colour, width=2, style=bgl.GL_LINE_STIPPLE): bgl.glPushAttrib(bgl.GL_ENABLE_BIT) bgl.glLineStipple(1, 0x9999) @@ -68,123 +91,127 @@ class GlDrawOnScreen(): bgl.glColor4f(*colour) bgl.glLineWidth(width) bgl.glBegin(bgl.GL_LINE_STRIP) + def Rectangle(self, x0, y0, x1, y1, colour, width=2, style=bgl.GL_LINE): - self._start_line(colour, width, style) + self._start_line(colour, width, style) bgl.glVertex2i(x0, y0) bgl.glVertex2i(x1, y0) bgl.glVertex2i(x1, y1) bgl.glVertex2i(x0, y1) bgl.glVertex2i(x0, y0) self._end() + def Polygon(self, pts, colour): bgl.glPushAttrib(bgl.GL_ENABLE_BIT) bgl.glEnable(bgl.GL_BLEND) - bgl.glColor4f(*colour) + bgl.glColor4f(*colour) bgl.glBegin(bgl.GL_POLYGON) for pt in pts: x, y = pt - bgl.glVertex2f(x, y) + bgl.glVertex2f(x, y) self._end() + def ProgressBar(self, x, y, width, height, start, percent): - x1, y1 = x+width, y+height + x1, y1 = x + width, y + height # progress from current point to either start or end - xs = x+(x1-x) * float(start) + xs = x + (x1 - x) * float(start) if percent > 0: # going forward - xi = xs+(x1-xs) * float(percent) + xi = xs + (x1 - xs) * float(percent) else: # going backward - xi = xs-(x-xs) * float(percent) + xi = xs - (x - xs) * float(percent) self.Polygon([(xs, y), (xs, y1), (xi, y1), (xi, y)], self.progress_colour) self.Rectangle(x, y, x1, y1, self.white, width=1) - + + def draw_callback(self, context): - #print("draw_callback : %s" % (self.progress)) self.gl.ProgressBar(10, 24, 200, 16, self.start, self.progress) - self.gl.String(str(int(100*abs(self.progress)))+"% ESC to Cancel", 14, 28, 10, self.gl.white) - + self.gl.String(str(int(100 * abs(self.progress))) + "% ESC to Stop", 14, 28, 10, self.gl.white) + + class OP_Tracking_auto_tracker(Operator): - """Autotrack. Esc to cancel.""" bl_idname = "tracking.auto_track" bl_label = "AutoTracking" + bl_description = ("Start Autotracking, Press Esc to Stop \n" + "When stopped, the added Track Markers will be kept") _timer = None _draw_handler = None - + gl = GlDrawOnScreen() progress = 0 limits = 0 t = 0 - + def find_track_start(self, track): for m in track.markers: if not m.mute: return m.frame return track.markers[0].frame - + def find_track_end(self, track): for m in reversed(track.markers): if not m.mute: return m.frame - return track.markers[-1].frame-1 - + return track.markers[-1].frame - 1 + def find_track_length(self, track): tstart = self.find_track_start(track) - tend = self.find_track_end(track) - return tend-tstart - + tend = self.find_track_end(track) + return tend - tstart + def show_tracks(self, context): - scene = context.scene - clip = context.area.spaces.active.clip + clip = context.area.spaces.active.clip tracks = clip.tracking.tracks for track in tracks: track.hide = False - - def get_vars_from_context(self, context): + + def get_vars_from_context(self, context): scene = context.scene props = context.window_manager.autotracker_props - clip = context.area.spaces.active.clip + clip = context.area.spaces.active.clip tracks = clip.tracking.tracks current_frame = scene.frame_current - clip_end = clip.frame_start+clip.frame_duration + clip_end = clip.frame_start + clip.frame_duration clip_start = clip.frame_start if props.track_backwards: - last_frame = min(clip_end, current_frame+props.frame_separation) + last_frame = min(clip_end, current_frame + props.frame_separation) else: - last_frame = max(clip_start, current_frame-props.frame_separation) + last_frame = max(clip_start, current_frame - props.frame_separation) return scene, props, clip, tracks, current_frame, last_frame - + def delete_tracks(self, to_delete): bpy.ops.clip.select_all(action='DESELECT') for track in to_delete: track.select = True bpy.ops.clip.delete_track() - + # DETECT FEATURES def auto_features(self, context): """ - Detect features + Detect features """ t = time.time() - + scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context) - + selected = [] old = [] to_delete = [] width = clip.size[0] - delete_threshold = float(props.delete_threshold)/100.0 - + delete_threshold = float(props.delete_threshold) / 100.0 + bpy.ops.clip.select_all(action='DESELECT') - + # Detect Features bpy.ops.clip.detect_features( - threshold=props.df_threshold, - min_distance=props.df_distance/100.0*width, - margin=props.df_margin/100.0*width, - placement=props.placement_list - ) - + threshold=props.df_threshold, + min_distance=props.df_distance / 100.0 * width, + margin=props.df_margin / 100.0 * width, + placement=props.placement_list + ) + # filter new and old tracks for track in tracks: if track.hide or track.lock: @@ -195,41 +222,40 @@ class OP_Tracking_auto_tracker(Operator): old.append(track) if track.select: selected.append(track) - + added_tracks = len(selected) - + # Select overlapping new markers for track_new in selected: marker0 = track_new.markers.find_frame(current_frame) for track_old in old: marker1 = track_old.markers.find_frame(current_frame) - distance = (marker1.co-marker0.co).length + distance = (marker1.co - marker0.co).length if distance < delete_threshold: to_delete.append(track_new) added_tracks -= 1 break - + # Delete Overlapping Markers self.delete_tracks(to_delete) - print("auto_features %.4f seconds add:%s tracks." % (time.time()-t, added_tracks)) - + debug_print("auto_features %.4f seconds, add: %s tracks" % (time.time() - t, added_tracks)) + # AUTOTRACK FRAMES def track_frames_backward(self): # INVOKE_DEFAULT to show progress and take account of frame_limit t = time.time() res = bpy.ops.clip.track_markers('INVOKE_DEFAULT', backwards=True, sequence=True) - print("track_frames_backward %.2f seconds %s" % (time.time()-t, res)) - + debug_print("track_frames_backward %.2f seconds %s" % (time.time() - t, res)) + def track_frames_forward(self): # INVOKE_DEFAULT to show progress and take account of frame_limit t = time.time() res = bpy.ops.clip.track_markers('INVOKE_DEFAULT', backwards=False, sequence=True) - print("track_frames_forward %.2f seconds %s" % (time.time()-t, res)) - + debug_print("track_frames_forward %.2f seconds %s" % (time.time() - t, res)) + def get_active_tracks(self, context): scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context) - # Select active trackers for tracking - #bpy.ops.clip.select_all(action='DESELECT') + active_tracks = [] for track in tracks: if track.hide or track.lock: @@ -239,9 +265,9 @@ class OP_Tracking_auto_tracker(Operator): else: marker = track.markers.find_frame(current_frame) if (marker is not None) and (not marker.mute): - active_tracks.append(track) + active_tracks.append(track) return active_tracks - + def select_active_tracks(self, context): t = time.time() scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context) @@ -250,9 +276,10 @@ class OP_Tracking_auto_tracker(Operator): selected = self.get_active_tracks(context) for track in selected: track.select = True - print("select_active_tracks %.2f seconds selected:%s" % (time.time()-t, len(selected))) + debug_print("select_active_tracks %.2f seconds," + " selected: %s" % (time.time() - t, len(selected))) return selected - + def estimate_motion(self, context, last, frame): """ compute mean pixel motion for current frame @@ -270,7 +297,7 @@ class OP_Tracking_auto_tracker(Operator): marker0 = track.markers.find_frame(frame) marker1 = track.markers.find_frame(last) if marker0 is not None and marker1 is not None: - d = (marker0.co-marker1.co).length + d = (marker0.co - marker1.co).length # skip fixed tracks if d > 0: distance += d @@ -280,9 +307,9 @@ class OP_Tracking_auto_tracker(Operator): else: # arbitrary set to prevent division by 0 error mean = 10 - + return mean - + # REMOVE SMALL TRACKS def remove_small(self, context): t = time.time() @@ -298,8 +325,8 @@ class OP_Tracking_auto_tracker(Operator): to_delete.append(track) deleted_tracks = len(to_delete) self.delete_tracks(to_delete) - print("remove_small %.4f seconds %s tracks deleted." % (time.time()-t, deleted_tracks)) - + debug_print("remove_small %.4f seconds, %s tracks deleted" % (time.time() - t, deleted_tracks)) + def split_track(self, context, track, split_frame, skip=0): scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context) if props.track_backwards: @@ -308,14 +335,14 @@ class OP_Tracking_auto_tracker(Operator): else: end = scene.frame_end step = 1 - new_track = \ - tracks.new(frame=split_frame) + new_track = tracks.new(frame=split_frame) + for frame in range(split_frame, end, step): marker = track.markers.find_frame(frame) if marker is None: return - # add new marker on new track for frame - if abs(frame - split_frame) >= skip: + # add new marker on new track for frame + if abs(frame - split_frame) >= skip: new_marker = new_track.markers.find_frame(frame) if new_marker is None: new_marker = new_track.markers.insert_frame(frame) @@ -326,268 +353,274 @@ class OP_Tracking_auto_tracker(Operator): else: track.markers.delete_frame(frame) marker.mute = True - + # REMOVE JUMPING MARKERS def remove_jumping(self, context): - + t = time.time() - scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context) - + if props.track_backwards: step = -1 else: step = 1 - + to_split = [None for track in tracks] for frame in range(last_frame, current_frame, step): - + last = frame - step - + # mean motion (normalized [0-1]) distance for tracks between last and current frame mean = self.estimate_motion(context, last, frame) - - # how much a track is allowed to move + + # how much a track is allowed to move allowed = mean * props.jump_cut - + for i, track in enumerate(tracks): if track.hide or track.lock: continue marker0 = track.markers.find_frame(frame) marker1 = track.markers.find_frame(last) if marker0 is not None and marker1 is not None: - distance = (marker0.co-marker1.co).length + distance = (marker0.co - marker1.co).length # Jump Cut threshold if distance > allowed: if to_split[i] is None: to_split[i] = [frame, frame] else: - to_split[i][1] = frame - + to_split[i][1] = frame + jumping = 0 for i, split in enumerate(to_split): if split is not None: - self.split_track(context, tracks[i], split[0], abs(split[0]-split[1])) + self.split_track(context, tracks[i], split[0], abs(split[0] - split[1])) jumping += 1 - - print("remove_jumping :%.4f seconds %s tracks cut." % (time.time()-t, jumping)) - + + debug_print("remove_jumping: %.4f seconds, %s tracks cut." % (time.time() - t, jumping)) + def get_frame_range(self, context): """ get tracking frames range - use clip limits when clip shorter than scene + use clip limits when clip shorter than scene else use scene limits """ scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context) frame_start = max(scene.frame_start, clip.frame_start) - frame_end = min(scene.frame_end, clip.frame_start+clip.frame_duration) + frame_end = min(scene.frame_end, clip.frame_start + clip.frame_duration) frame_duration = frame_end - frame_start return frame_start, frame_end, frame_duration - + def modal(self, context, event): - + if event.type in {'ESC'}: - print("Cancelling") + self.report({'INFO'}, + "Stopping, up to now added Markers will be kept. Autotracking Finished") self.cancel(context) return {'FINISHED'} - + scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context) frame_start, frame_end, frame_duration = self.get_frame_range(context) - + if (((not props.track_backwards) and current_frame >= frame_end) or - (props.track_backwards and current_frame <= frame_start)): - print("Reached clip end") + (props.track_backwards and current_frame <= frame_start)): + + self.report({'INFO'}, + "Reached the end of the Clip. Autotracking Finished") self.cancel(context) return {'FINISHED'} - - # dont run this modal while tracking operator runs - # Known issue, youll have to keep ESC pressed + + # do not run this modal while tracking operator runs + # Known issue, you'll have to keep ESC pressed if event.type not in {'TIMER'} or context.scene.frame_current != self.next_frame: return {'PASS_THROUGH'} - + # prevent own TIMER event while running self.stop_timer(context) - + if props.track_backwards: self.next_frame = scene.frame_current - props.frame_separation total = self.start_frame - frame_start else: self.next_frame = scene.frame_current + props.frame_separation total = frame_end - self.start_frame - + if total > 0: - self.progress = (current_frame-self.start_frame)/total + self.progress = (current_frame - self.start_frame) / total else: self.progress = 0 - - print("Tracking frame %s" % (scene.frame_current)) - + + debug_print("Tracking frame %s" % (scene.frame_current)) + # Remove bad tracks before adding new ones self.remove_small(context) self.remove_jumping(context) - + # add new tracks self.auto_features(context) # Select active trackers for tracking active_tracks = self.select_active_tracks(context) - + # finish if there is nothing to track if len(active_tracks) == 0: - print("No new tracks created. Doing nothing.") + self.report({'INFO'}, + "No new tracks created, nothing to track. Autotrack Finished") self.cancel(context) return {'FINISHED'} - + # setup frame_limit on tracks for track in active_tracks: track.frames_limit = 0 active_tracks[0].frames_limit = props.frame_separation - + # Forwards or backwards tracking if props.track_backwards: self.track_frames_backward() else: self.track_frames_forward() - - # setup a timer to broadcast a TIMER event to force modal to re-run as fast as possible (not waiting for any mouse or keyboard event) + + # setup a timer to broadcast a TIMER event to force modal to + # re-run as fast as possible (not waiting for any mouse or keyboard event) self.start_timer(context) - + return {'RUNNING_MODAL'} - + def invoke(self, context, event): scene = context.scene frame_start, frame_end, frame_duration = self.get_frame_range(context) - + if scene.frame_current > frame_end: scene.frame_current = frame_end elif scene.frame_current < frame_start: scene.frame_current = frame_start - + self.start_frame = scene.frame_current - self.start = (scene.frame_current-frame_start) / (frame_duration) + self.start = (scene.frame_current - frame_start) / (frame_duration) self.progress = 0 - + # keep track of frame at witch we should detect new features and filter tracks self.next_frame = scene.frame_current - + # draw progress args = (self, context) - self._draw_handler = bpy.types.SpaceClipEditor.draw_handler_add(draw_callback, args, 'WINDOW', 'POST_PIXEL') - + self._draw_handler = bpy.types.SpaceClipEditor.draw_handler_add( + draw_callback, args, + 'WINDOW', 'POST_PIXEL' + ) self.start_timer(context) context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} - + def __init__(self): self.t = time.time() def __del__(self): - print("AutoTrack %.2f seconds" % (time.time()-self.t)) - + debug_print("AutoTrack %.2f seconds" % (time.time() - self.t)) + def execute(self, context): - print("execute") + debug_print("Autotrack execute called") return {'FINISHED'} - + def stop_timer(self, context): context.window_manager.event_timer_remove(self._timer) - + def start_timer(self, context): self._timer = context.window_manager.event_timer_add(time_step=0.1, window=context.window) - + def cancel(self, context): self.stop_timer(context) - self.show_tracks(context) + self.show_tracks(context) bpy.types.SpaceClipEditor.draw_handler_remove(self._draw_handler, 'WINDOW') - + @classmethod def poll(cls, context): - return (context.area.spaces.active.clip is not None) - + return (context.area.spaces.active.clip is not None) + + class AutotrackerSettings(PropertyGroup): """Create properties""" df_margin = FloatProperty( name="Detect Features Margin", - description="Only features further margin pixels from the image edges are considered.", + description="Only consider features from pixels located outside\n" + "the defined margin from the clip borders", subtype='PERCENTAGE', default=5, min=0, max=100 ) - df_threshold = FloatProperty( name="Detect Features Threshold", - description="Threshold level to concider feature good enough for tracking.", + description="Threshold level to deem a feature being good enough for tracking", default=0.3, min=0.0, max=1.0 ) - # Note: merge this one with delete_threshold + # Note: merge this one with delete_threshold df_distance = FloatProperty( name="Detect Features Distance", - description="Minimal distance accepted between two features.", + description="Minimal acceptable distance between two features", subtype='PERCENTAGE', default=8, min=1, max=100 ) - delete_threshold = FloatProperty( name="New Marker Threshold", - description="Threshold how near new features can appear during autotracking.", + description="Threshold of how close a new features can appear during tracking", subtype='PERCENTAGE', default=8, min=1, max=100 ) - small_tracks = IntProperty( name="Minimum track length", - description="Delete tracks shortest than this number of frames (set to 0 to keep all tracks).", + description="Delete tracks shorter than this number of frames\n" + "Note: set to 0 for keeping all tracks", default=50, min=1, max=1000 ) - frame_separation = IntProperty( name="Frame Separation", - description="How often new features are generated.", + description="How often new features are generated", default=5, min=1, max=100 ) - jump_cut = 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. (factor relative to mean motion)", + description="How much distance a marker can travel before it is considered " + "to be a bad track and cut.\nA new track wil be added " + "(factor relative to mean motion)", default=5.0, min=0.0, max=50.0 ) - track_backwards = BoolProperty( name="AutoTrack Backwards", - description="Autotrack backwards.", + description="Track from the last frame of the selected clip", default=False ) - # Dropdown menu list_items = [ - ("FRAME", "Whole Frame", "", 1), - ("INSIDE_GPENCIL", "Inside Grease Pencil", "", 2), - ("OUTSIDE_GPENCIL", "Outside Grease Pencil", "", 3), - ] - + ("FRAME", "Whole Frame", "", 1), + ("INSIDE_GPENCIL", "Inside Grease Pencil", "", 2), + ("OUTSIDE_GPENCIL", "Outside Grease Pencil", "", 3), + ] placement_list = EnumProperty( name="Placement", description="Feature Placement", items=list_items ) - + + """ NOTE: - All size properties are in percent of clip size, so presets does not depends on clip size + All size properties are in percent of the clip size, + so presets do not depend on the clip size """ + + class AutotrackerPanel(Panel): """Creates a Panel in the Render Layer properties window""" bl_label = "Autotrack" @@ -595,19 +628,18 @@ class AutotrackerPanel(Panel): bl_space_type = 'CLIP_EDITOR' bl_region_type = 'TOOLS' bl_category = "Track" - + @classmethod def poll(cls, context): - return (context.area.spaces.active.clip is not None) - - # Draw UI + return (context.area.spaces.active.clip is not None) + def draw(self, context): layout = self.layout wm = context.window_manager - + row = layout.row() row.scale_y = 1.5 - props = row.operator("tracking.auto_track", text="Autotrack! ", icon='PLAY') + row.operator("tracking.auto_track", text="Autotrack! ", icon='PLAY') row = layout.row() row.prop(wm.autotracker_props, "track_backwards") @@ -615,39 +647,36 @@ class AutotrackerPanel(Panel): row = layout.row() col = layout.column(align=True) col.prop(wm.autotracker_props, "delete_threshold") - sub = col.row(align=True) - sub.prop(wm.autotracker_props, "small_tracks") - sub = col.row(align=True) - sub.prop(wm.autotracker_props, "frame_separation", text="Frame Separation") - sub = col.row(align=True) - sub.prop(wm.autotracker_props, "jump_cut", text="Jump Threshold") + col.prop(wm.autotracker_props, "small_tracks") + col.prop(wm.autotracker_props, "frame_separation", text="Frame Separation") + col.prop(wm.autotracker_props, "jump_cut", text="Jump Threshold") row = layout.row() row.label(text="Detect Features Settings:") col = layout.column(align=True) col.prop(wm.autotracker_props, "df_margin", text="Margin:") - sub = col.row(align=True) - sub.prop(wm.autotracker_props, "df_distance", text="Distance:") - sub = col.row(align=True) - sub.prop(wm.autotracker_props, "df_threshold", text="Threshold:") + col.prop(wm.autotracker_props, "df_distance", text="Distance:") + col.prop(wm.autotracker_props, "df_threshold", text="Threshold:") row = layout.row() row.label(text="Feature Placement:") col = layout.column(align=True) col.prop(wm.autotracker_props, "placement_list", text="") - layout.separator() - + def register(): bpy.utils.register_class(AutotrackerSettings) - WindowManager.autotracker_props = \ - PointerProperty(type=AutotrackerSettings) - bpy.utils.register_module(__name__) - + WindowManager.autotracker_props = PointerProperty( + type=AutotrackerSettings + ) + bpy.utils.register_module(__name__) + + def unregister(): bpy.utils.unregister_class(AutotrackerSettings) - bpy.utils.unregister_module(__name__) + bpy.utils.unregister_module(__name__) del WindowManager.autotracker_props + if __name__ == "__main__": register() -- cgit v1.2.3