math_vis: remove use of register_module
[blender-addons.git] / space_clip_editor_autotracker.py
blob1cd20886a041ea98107f6366c52dd57a0dc7558f
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Autotrack",
21 "author": "Miika Puustinen, Matti Kaihola, Stephen Leger",
22 "version": (0, 1, 1),
23 "blender": (2, 78, 0),
24 "location": "Movie clip Editor > Tools Panel > Autotrack",
25 "description": "Motion Tracking with automatic feature detection.",
26 "warning": "",
27 "wiki_url": "https://github.com/miikapuustinen/blender_autotracker",
28 "category": "Motion Tracking",
31 import bpy
32 import bgl
33 import blf
34 from bpy.types import (
35 Operator,
36 Panel,
37 PropertyGroup,
38 WindowManager,
40 from bpy.props import (
41 BoolProperty,
42 FloatProperty,
43 IntProperty,
44 EnumProperty,
45 PointerProperty,
48 # for debug purposes
49 import time
51 # set to True to enable debug prints
52 DEBUG = False
55 # pass variables just like for the regular prints
56 def debug_print(*args, **kwargs):
57 global DEBUG
58 if DEBUG:
59 print(*args, **kwargs)
62 # http://blenderscripting.blogspot.ch/2011/07/bgl-drawing-with-opengl-onto-blender-25.html
63 class GlDrawOnScreen():
64 black = (0.0, 0.0, 0.0, 0.7)
65 white = (1.0, 1.0, 1.0, 0.5)
66 progress_colour = (0.2, 0.7, 0.2, 0.5)
68 def String(self, text, x, y, size, colour):
69 ''' my_string : the text we want to print
70 pos_x, pos_y : coordinates in integer values
71 size : font height.
72 colour : used for definining the colour'''
73 dpi, font_id = 72, 0 # dirty fast assignment
74 bgl.glColor4f(*colour)
75 blf.position(font_id, x, y, 0)
76 blf.size(font_id, size, dpi)
77 blf.draw(font_id, text)
79 def _end(self):
80 bgl.glEnd()
81 bgl.glPopAttrib()
82 bgl.glLineWidth(1)
83 bgl.glDisable(bgl.GL_BLEND)
84 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
86 def _start_line(self, colour, width=2, style=bgl.GL_LINE_STIPPLE):
87 bgl.glPushAttrib(bgl.GL_ENABLE_BIT)
88 bgl.glLineStipple(1, 0x9999)
89 bgl.glEnable(style)
90 bgl.glEnable(bgl.GL_BLEND)
91 bgl.glColor4f(*colour)
92 bgl.glLineWidth(width)
93 bgl.glBegin(bgl.GL_LINE_STRIP)
95 def Rectangle(self, x0, y0, x1, y1, colour, width=2, style=bgl.GL_LINE):
96 self._start_line(colour, width, style)
97 bgl.glVertex2i(x0, y0)
98 bgl.glVertex2i(x1, y0)
99 bgl.glVertex2i(x1, y1)
100 bgl.glVertex2i(x0, y1)
101 bgl.glVertex2i(x0, y0)
102 self._end()
104 def Polygon(self, pts, colour):
105 bgl.glPushAttrib(bgl.GL_ENABLE_BIT)
106 bgl.glEnable(bgl.GL_BLEND)
107 bgl.glColor4f(*colour)
108 bgl.glBegin(bgl.GL_POLYGON)
109 for pt in pts:
110 x, y = pt
111 bgl.glVertex2f(x, y)
112 self._end()
114 def ProgressBar(self, x, y, width, height, start, percent):
115 x1, y1 = x + width, y + height
116 # progress from current point to either start or end
117 xs = x + (x1 - x) * float(start)
118 if percent > 0:
119 # going forward
120 xi = xs + (x1 - xs) * float(percent)
121 else:
122 # going backward
123 xi = xs - (x - xs) * float(percent)
124 self.Polygon([(xs, y), (xs, y1), (xi, y1), (xi, y)], self.progress_colour)
125 self.Rectangle(x, y, x1, y1, self.white, width=1)
128 def draw_callback(self, context):
129 self.gl.ProgressBar(10, 24, 200, 16, self.start, self.progress)
130 self.gl.String(str(int(100 * abs(self.progress))) + "% ESC to Stop", 14, 28, 10, self.gl.white)
133 class OP_Tracking_auto_tracker(Operator):
134 bl_idname = "tracking.auto_track"
135 bl_label = "AutoTracking"
136 bl_description = ("Start Autotracking, Press Esc to Stop \n"
137 "When stopped, the added Track Markers will be kept")
139 _timer = None
140 _draw_handler = None
142 gl = GlDrawOnScreen()
143 progress = 0
144 limits = 0
145 t = 0
147 def find_track_start(self, track):
148 for m in track.markers:
149 if not m.mute:
150 return m.frame
151 return track.markers[0].frame
153 def find_track_end(self, track):
154 for m in reversed(track.markers):
155 if not m.mute:
156 return m.frame
157 return track.markers[-1].frame - 1
159 def find_track_length(self, track):
160 tstart = self.find_track_start(track)
161 tend = self.find_track_end(track)
162 return tend - tstart
164 def show_tracks(self, context):
165 clip = context.area.spaces.active.clip
166 tracks = clip.tracking.tracks
167 for track in tracks:
168 track.hide = False
170 def get_vars_from_context(self, context):
171 scene = context.scene
172 props = context.window_manager.autotracker_props
173 clip = context.area.spaces.active.clip
174 tracks = clip.tracking.tracks
175 current_frame = scene.frame_current
176 clip_end = clip.frame_start + clip.frame_duration
177 clip_start = clip.frame_start
178 if props.track_backwards:
179 last_frame = min(clip_end, current_frame + props.frame_separation)
180 else:
181 last_frame = max(clip_start, current_frame - props.frame_separation)
182 return scene, props, clip, tracks, current_frame, last_frame
184 def delete_tracks(self, to_delete):
185 bpy.ops.clip.select_all(action='DESELECT')
186 for track in to_delete:
187 track.select = True
188 bpy.ops.clip.delete_track()
190 # DETECT FEATURES
191 def auto_features(self, context):
193 Detect features
195 t = time.time()
197 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
199 selected = []
200 old = []
201 to_delete = []
202 width = clip.size[0]
203 delete_threshold = float(props.delete_threshold) / 100.0
205 bpy.ops.clip.select_all(action='DESELECT')
207 # Detect Features
208 bpy.ops.clip.detect_features(
209 threshold=props.df_threshold,
210 min_distance=props.df_distance / 100.0 * width,
211 margin=props.df_margin / 100.0 * width,
212 placement=props.placement_list
215 # filter new and old tracks
216 for track in tracks:
217 if track.hide or track.lock:
218 continue
219 marker = track.markers.find_frame(current_frame)
220 if marker is not None:
221 if (not track.select) and (not marker.mute):
222 old.append(track)
223 if track.select:
224 selected.append(track)
226 added_tracks = len(selected)
228 # Select overlapping new markers
229 for track_new in selected:
230 marker0 = track_new.markers.find_frame(current_frame)
231 for track_old in old:
232 marker1 = track_old.markers.find_frame(current_frame)
233 distance = (marker1.co - marker0.co).length
234 if distance < delete_threshold:
235 to_delete.append(track_new)
236 added_tracks -= 1
237 break
239 # Delete Overlapping Markers
240 self.delete_tracks(to_delete)
241 debug_print("auto_features %.4f seconds, add: %s tracks" % (time.time() - t, added_tracks))
243 # AUTOTRACK FRAMES
244 def track_frames_backward(self):
245 # INVOKE_DEFAULT to show progress and take account of frame_limit
246 t = time.time()
247 res = bpy.ops.clip.track_markers('INVOKE_DEFAULT', backwards=True, sequence=True)
248 debug_print("track_frames_backward %.2f seconds %s" % (time.time() - t, res))
250 def track_frames_forward(self):
251 # INVOKE_DEFAULT to show progress and take account of frame_limit
252 t = time.time()
253 res = bpy.ops.clip.track_markers('INVOKE_DEFAULT', backwards=False, sequence=True)
254 debug_print("track_frames_forward %.2f seconds %s" % (time.time() - t, res))
256 def get_active_tracks(self, context):
257 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
259 active_tracks = []
260 for track in tracks:
261 if track.hide or track.lock:
262 continue
263 if len(track.markers) < 2:
264 active_tracks.append(track)
265 else:
266 marker = track.markers.find_frame(current_frame)
267 if (marker is not None) and (not marker.mute):
268 active_tracks.append(track)
269 return active_tracks
271 def select_active_tracks(self, context):
272 t = time.time()
273 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
274 # Select active trackers for tracking
275 bpy.ops.clip.select_all(action='DESELECT')
276 selected = self.get_active_tracks(context)
277 for track in selected:
278 track.select = True
279 debug_print("select_active_tracks %.2f seconds,"
280 " selected: %s" % (time.time() - t, len(selected)))
281 return selected
283 def estimate_motion(self, context, last, frame):
285 compute mean pixel motion for current frame
286 TODO: use statistic here to make filtering more efficient
287 last : last frame number
288 frame: current frame number
289 return mean pixel distance error
291 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
292 nbtracks = 0
293 distance = 0.0
294 for track in tracks:
295 if track.hide or track.lock:
296 continue
297 marker0 = track.markers.find_frame(frame)
298 marker1 = track.markers.find_frame(last)
299 if marker0 is not None and marker1 is not None:
300 d = (marker0.co - marker1.co).length
301 # skip fixed tracks
302 if d > 0:
303 distance += d
304 nbtracks += 1
305 if nbtracks > 0:
306 mean = distance / nbtracks
307 else:
308 # arbitrary set to prevent division by 0 error
309 mean = 10
311 return mean
313 # REMOVE SMALL TRACKS
314 def remove_small(self, context):
315 t = time.time()
316 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
317 to_delete = []
318 bpy.ops.clip.select_all(action='DESELECT')
319 for track in tracks:
320 if track.hide or track.lock:
321 continue
322 if len(track.markers) > 1:
323 marker = track.markers.find_frame(current_frame)
324 if marker is None and self.find_track_length(track) < props.small_tracks:
325 to_delete.append(track)
326 deleted_tracks = len(to_delete)
327 self.delete_tracks(to_delete)
328 debug_print("remove_small %.4f seconds, %s tracks deleted" % (time.time() - t, deleted_tracks))
330 def split_track(self, context, track, split_frame, skip=0):
331 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
332 if props.track_backwards:
333 end = scene.frame_start
334 step = -1
335 else:
336 end = scene.frame_end
337 step = 1
338 new_track = tracks.new(frame=split_frame)
340 for frame in range(split_frame, end, step):
341 marker = track.markers.find_frame(frame)
342 if marker is None:
343 return
344 # add new marker on new track for frame
345 if abs(frame - split_frame) >= skip:
346 new_marker = new_track.markers.find_frame(frame)
347 if new_marker is None:
348 new_marker = new_track.markers.insert_frame(frame)
349 new_marker.co = marker.co
350 # remove marker on track for frame
351 if frame == split_frame:
352 track.hide = True
353 else:
354 track.markers.delete_frame(frame)
355 marker.mute = True
357 # REMOVE JUMPING MARKERS
358 def remove_jumping(self, context):
360 t = time.time()
361 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
363 if props.track_backwards:
364 step = -1
365 else:
366 step = 1
368 to_split = [None for track in tracks]
369 for frame in range(last_frame, current_frame, step):
371 last = frame - step
373 # mean motion (normalized [0-1]) distance for tracks between last and current frame
374 mean = self.estimate_motion(context, last, frame)
376 # how much a track is allowed to move
377 allowed = mean * props.jump_cut
379 for i, track in enumerate(tracks):
380 if track.hide or track.lock:
381 continue
382 marker0 = track.markers.find_frame(frame)
383 marker1 = track.markers.find_frame(last)
384 if marker0 is not None and marker1 is not None:
385 distance = (marker0.co - marker1.co).length
386 # Jump Cut threshold
387 if distance > allowed:
388 if to_split[i] is None:
389 to_split[i] = [frame, frame]
390 else:
391 to_split[i][1] = frame
393 jumping = 0
394 for i, split in enumerate(to_split):
395 if split is not None:
396 self.split_track(context, tracks[i], split[0], abs(split[0] - split[1]))
397 jumping += 1
399 debug_print("remove_jumping: %.4f seconds, %s tracks cut." % (time.time() - t, jumping))
401 def get_frame_range(self, context):
403 get tracking frames range
404 use clip limits when clip shorter than scene
405 else use scene limits
407 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
408 frame_start = max(scene.frame_start, clip.frame_start)
409 frame_end = min(scene.frame_end, clip.frame_start + clip.frame_duration)
410 frame_duration = frame_end - frame_start
411 return frame_start, frame_end, frame_duration
413 def modal(self, context, event):
415 if event.type in {'ESC'}:
416 self.report({'INFO'},
417 "Stopping, up to now added Markers will be kept. Autotracking Finished")
418 self.cancel(context)
419 return {'FINISHED'}
421 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
422 frame_start, frame_end, frame_duration = self.get_frame_range(context)
424 if (((not props.track_backwards) and current_frame >= frame_end) or
425 (props.track_backwards and current_frame <= frame_start)):
427 self.report({'INFO'},
428 "Reached the end of the Clip. Autotracking Finished")
429 self.cancel(context)
430 return {'FINISHED'}
432 # do not run this modal while tracking operator runs
433 # Known issue, you'll have to keep ESC pressed
434 if event.type not in {'TIMER'} or context.scene.frame_current != self.next_frame:
435 return {'PASS_THROUGH'}
437 # prevent own TIMER event while running
438 self.stop_timer(context)
440 if props.track_backwards:
441 self.next_frame = scene.frame_current - props.frame_separation
442 total = self.start_frame - frame_start
443 else:
444 self.next_frame = scene.frame_current + props.frame_separation
445 total = frame_end - self.start_frame
447 if total > 0:
448 self.progress = (current_frame - self.start_frame) / total
449 else:
450 self.progress = 0
452 debug_print("Tracking frame %s" % (scene.frame_current))
454 # Remove bad tracks before adding new ones
455 self.remove_small(context)
456 self.remove_jumping(context)
458 # add new tracks
459 self.auto_features(context)
461 # Select active trackers for tracking
462 active_tracks = self.select_active_tracks(context)
464 # finish if there is nothing to track
465 if len(active_tracks) == 0:
466 self.report({'INFO'},
467 "No new tracks created, nothing to track. Autotrack Finished")
468 self.cancel(context)
469 return {'FINISHED'}
471 # setup frame_limit on tracks
472 for track in active_tracks:
473 track.frames_limit = 0
474 active_tracks[0].frames_limit = props.frame_separation
476 # Forwards or backwards tracking
477 if props.track_backwards:
478 self.track_frames_backward()
479 else:
480 self.track_frames_forward()
482 # setup a timer to broadcast a TIMER event to force modal to
483 # re-run as fast as possible (not waiting for any mouse or keyboard event)
484 self.start_timer(context)
486 return {'RUNNING_MODAL'}
488 def invoke(self, context, event):
489 scene = context.scene
490 frame_start, frame_end, frame_duration = self.get_frame_range(context)
492 if scene.frame_current > frame_end:
493 scene.frame_current = frame_end
494 elif scene.frame_current < frame_start:
495 scene.frame_current = frame_start
497 self.start_frame = scene.frame_current
498 self.start = (scene.frame_current - frame_start) / (frame_duration)
499 self.progress = 0
501 # keep track of frame at witch we should detect new features and filter tracks
502 self.next_frame = scene.frame_current
504 # draw progress
505 args = (self, context)
506 self._draw_handler = bpy.types.SpaceClipEditor.draw_handler_add(
507 draw_callback, args,
508 'WINDOW', 'POST_PIXEL'
510 self.start_timer(context)
511 context.window_manager.modal_handler_add(self)
512 return {'RUNNING_MODAL'}
514 def __init__(self):
515 self.t = time.time()
517 def __del__(self):
518 debug_print("AutoTrack %.2f seconds" % (time.time() - self.t))
520 def execute(self, context):
521 debug_print("Autotrack execute called")
522 return {'FINISHED'}
524 def stop_timer(self, context):
525 context.window_manager.event_timer_remove(self._timer)
527 def start_timer(self, context):
528 self._timer = context.window_manager.event_timer_add(time_step=0.1, window=context.window)
530 def cancel(self, context):
531 self.stop_timer(context)
532 self.show_tracks(context)
533 bpy.types.SpaceClipEditor.draw_handler_remove(self._draw_handler, 'WINDOW')
535 @classmethod
536 def poll(cls, context):
537 return (context.area.spaces.active.clip is not None)
540 class AutotrackerSettings(PropertyGroup):
541 """Create properties"""
542 df_margin = FloatProperty(
543 name="Detect Features Margin",
544 description="Only consider features from pixels located outside\n"
545 "the defined margin from the clip borders",
546 subtype='PERCENTAGE',
547 default=5,
548 min=0,
549 max=100
551 df_threshold = FloatProperty(
552 name="Detect Features Threshold",
553 description="Threshold level to deem a feature being good enough for tracking",
554 default=0.3,
555 min=0.0,
556 max=1.0
558 # Note: merge this one with delete_threshold
559 df_distance = FloatProperty(
560 name="Detect Features Distance",
561 description="Minimal acceptable distance between two features",
562 subtype='PERCENTAGE',
563 default=8,
564 min=1,
565 max=100
567 delete_threshold = FloatProperty(
568 name="New Marker Threshold",
569 description="Threshold of how close a new features can appear during tracking",
570 subtype='PERCENTAGE',
571 default=8,
572 min=1,
573 max=100
575 small_tracks = IntProperty(
576 name="Minimum track length",
577 description="Delete tracks shorter than this number of frames\n"
578 "Note: set to 0 for keeping all tracks",
579 default=50,
580 min=1,
581 max=1000
583 frame_separation = IntProperty(
584 name="Frame Separation",
585 description="How often new features are generated",
586 default=5,
587 min=1,
588 max=100
590 jump_cut = FloatProperty(
591 name="Jump Cut",
592 description="How much distance a marker can travel before it is considered "
593 "to be a bad track and cut.\nA new track wil be added "
594 "(factor relative to mean motion)",
595 default=5.0,
596 min=0.0,
597 max=50.0
599 track_backwards = BoolProperty(
600 name="AutoTrack Backwards",
601 description="Track from the last frame of the selected clip",
602 default=False
604 # Dropdown menu
605 list_items = [
606 ("FRAME", "Whole Frame", "", 1),
607 ("INSIDE_GPENCIL", "Inside Grease Pencil", "", 2),
608 ("OUTSIDE_GPENCIL", "Outside Grease Pencil", "", 3),
610 placement_list = EnumProperty(
611 name="Placement",
612 description="Feature Placement",
613 items=list_items
618 NOTE:
619 All size properties are in percent of the clip size,
620 so presets do not depend on the clip size
624 class AutotrackerPanel(Panel):
625 """Creates a Panel in the Render Layer properties window"""
626 bl_label = "Autotrack"
627 bl_idname = "autotrack"
628 bl_space_type = 'CLIP_EDITOR'
629 bl_region_type = 'TOOLS'
630 bl_category = "Track"
632 @classmethod
633 def poll(cls, context):
634 return (context.area.spaces.active.clip is not None)
636 def draw(self, context):
637 layout = self.layout
638 wm = context.window_manager
640 row = layout.row()
641 row.scale_y = 1.5
642 row.operator("tracking.auto_track", text="Autotrack! ", icon='PLAY')
644 row = layout.row()
645 row.prop(wm.autotracker_props, "track_backwards")
647 row = layout.row()
648 col = layout.column(align=True)
649 col.prop(wm.autotracker_props, "delete_threshold")
650 col.prop(wm.autotracker_props, "small_tracks")
651 col.prop(wm.autotracker_props, "frame_separation", text="Frame Separation")
652 col.prop(wm.autotracker_props, "jump_cut", text="Jump Threshold")
654 row = layout.row()
655 row.label(text="Detect Features Settings:")
656 col = layout.column(align=True)
657 col.prop(wm.autotracker_props, "df_margin", text="Margin:")
658 col.prop(wm.autotracker_props, "df_distance", text="Distance:")
659 col.prop(wm.autotracker_props, "df_threshold", text="Threshold:")
661 row = layout.row()
662 row.label(text="Feature Placement:")
663 col = layout.column(align=True)
664 col.prop(wm.autotracker_props, "placement_list", text="")
667 def register():
668 bpy.utils.register_class(AutotrackerSettings)
669 WindowManager.autotracker_props = PointerProperty(
670 type=AutotrackerSettings
672 bpy.utils.register_module(__name__)
675 def unregister():
676 bpy.utils.unregister_class(AutotrackerSettings)
677 bpy.utils.unregister_module(__name__)
678 del WindowManager.autotracker_props
681 if __name__ == "__main__":
682 register()