UI: Move Extensions repositories popover to header
[blender-addons-contrib.git] / space_clip_editor_autotracker.py
blobdd2967326749965afece8ee3a861a9c5d5c411cd
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 "doc_url": "https://github.com/miikapuustinen/blender_autotracker",
28 "category": "Video Tools",
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 defining 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=None):
87 if style is None:
88 # NOTE: this is no longer supported in Blender3.0.
89 # style = bgl.GL_LINE_STIPPLE
90 style = bgl.GL_LINE
91 bgl.glPushAttrib(bgl.GL_ENABLE_BIT)
92 bgl.glLineStipple(1, 0x9999)
93 bgl.glEnable(style)
94 bgl.glEnable(bgl.GL_BLEND)
95 bgl.glColor4f(*colour)
96 bgl.glLineWidth(width)
97 bgl.glBegin(bgl.GL_LINE_STRIP)
99 def Rectangle(self, x0, y0, x1, y1, colour, width=2, style=bgl.GL_LINE):
100 self._start_line(colour, width, style)
101 bgl.glVertex2i(x0, y0)
102 bgl.glVertex2i(x1, y0)
103 bgl.glVertex2i(x1, y1)
104 bgl.glVertex2i(x0, y1)
105 bgl.glVertex2i(x0, y0)
106 self._end()
108 def Polygon(self, pts, colour):
109 bgl.glPushAttrib(bgl.GL_ENABLE_BIT)
110 bgl.glEnable(bgl.GL_BLEND)
111 bgl.glColor4f(*colour)
112 bgl.glBegin(bgl.GL_POLYGON)
113 for pt in pts:
114 x, y = pt
115 bgl.glVertex2f(x, y)
116 self._end()
118 def ProgressBar(self, x, y, width, height, start, percent):
119 x1, y1 = x + width, y + height
120 # progress from current point to either start or end
121 xs = x + (x1 - x) * float(start)
122 if percent > 0:
123 # going forward
124 xi = xs + (x1 - xs) * float(percent)
125 else:
126 # going backward
127 xi = xs - (x - xs) * float(percent)
128 self.Polygon([(xs, y), (xs, y1), (xi, y1), (xi, y)], self.progress_colour)
129 self.Rectangle(x, y, x1, y1, self.white, width=1)
132 def draw_callback(self, context):
133 self.gl.ProgressBar(10, 24, 200, 16, self.start, self.progress)
134 self.gl.String(str(int(100 * abs(self.progress))) + "% ESC to Stop", 14, 28, 10, self.gl.white)
137 class OP_Tracking_auto_tracker(Operator):
138 bl_idname = "tracking.auto_track"
139 bl_label = "AutoTracking"
140 bl_description = ("Start Autotracking, Press Esc to Stop \n"
141 "When stopped, the added Track Markers will be kept")
143 _timer = None
144 _draw_handler = None
146 gl = GlDrawOnScreen()
147 progress = 0
148 limits = 0
149 t = 0
151 def find_track_start(self, track):
152 for m in track.markers:
153 if not m.mute:
154 return m.frame
155 return track.markers[0].frame
157 def find_track_end(self, track):
158 for m in reversed(track.markers):
159 if not m.mute:
160 return m.frame
161 return track.markers[-1].frame - 1
163 def find_track_length(self, track):
164 tstart = self.find_track_start(track)
165 tend = self.find_track_end(track)
166 return tend - tstart
168 def show_tracks(self, context):
169 clip = context.area.spaces.active.clip
170 tracks = clip.tracking.tracks
171 for track in tracks:
172 track.hide = False
174 def get_vars_from_context(self, context):
175 scene = context.scene
176 props = context.window_manager.autotracker_props
177 clip = context.area.spaces.active.clip
178 tracks = clip.tracking.tracks
179 current_frame = scene.frame_current
180 clip_end = clip.frame_start + clip.frame_duration
181 clip_start = clip.frame_start
182 if props.track_backwards:
183 last_frame = min(clip_end, current_frame + props.frame_separation)
184 else:
185 last_frame = max(clip_start, current_frame - props.frame_separation)
186 return scene, props, clip, tracks, current_frame, last_frame
188 def delete_tracks(self, to_delete):
189 bpy.ops.clip.select_all(action='DESELECT')
190 for track in to_delete:
191 track.select = True
192 bpy.ops.clip.delete_track()
194 # DETECT FEATURES
195 def auto_features(self, context):
197 Detect features
199 t = time.time()
201 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
203 selected = []
204 old = []
205 to_delete = []
206 width = clip.size[0]
207 delete_threshold = float(props.delete_threshold) / 100.0
209 bpy.ops.clip.select_all(action='DESELECT')
211 # Detect Features
212 bpy.ops.clip.detect_features(
213 threshold=props.df_threshold,
214 min_distance=props.df_distance / 100.0 * width,
215 margin=props.df_margin / 100.0 * width,
216 placement=props.placement_list
219 # filter new and old tracks
220 for track in tracks:
221 if track.hide or track.lock:
222 continue
223 marker = track.markers.find_frame(current_frame)
224 if marker is not None:
225 if (not track.select) and (not marker.mute):
226 old.append(track)
227 if track.select:
228 selected.append(track)
230 added_tracks = len(selected)
232 # Select overlapping new markers
233 for track_new in selected:
234 marker0 = track_new.markers.find_frame(current_frame)
235 for track_old in old:
236 marker1 = track_old.markers.find_frame(current_frame)
237 distance = (marker1.co - marker0.co).length
238 if distance < delete_threshold:
239 to_delete.append(track_new)
240 added_tracks -= 1
241 break
243 # Delete Overlapping Markers
244 self.delete_tracks(to_delete)
245 debug_print("auto_features %.4f seconds, add: %s tracks" % (time.time() - t, added_tracks))
247 # AUTOTRACK FRAMES
248 def track_frames_backward(self):
249 # INVOKE_DEFAULT to show progress and take account of frame_limit
250 t = time.time()
251 res = bpy.ops.clip.track_markers('INVOKE_DEFAULT', backwards=True, sequence=True)
252 debug_print("track_frames_backward %.2f seconds %s" % (time.time() - t, res))
254 def track_frames_forward(self):
255 # INVOKE_DEFAULT to show progress and take account of frame_limit
256 t = time.time()
257 res = bpy.ops.clip.track_markers('INVOKE_DEFAULT', backwards=False, sequence=True)
258 debug_print("track_frames_forward %.2f seconds %s" % (time.time() - t, res))
260 def get_active_tracks(self, context):
261 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
263 active_tracks = []
264 for track in tracks:
265 if track.hide or track.lock:
266 continue
267 if len(track.markers) < 2:
268 active_tracks.append(track)
269 else:
270 marker = track.markers.find_frame(current_frame)
271 if (marker is not None) and (not marker.mute):
272 active_tracks.append(track)
273 return active_tracks
275 def select_active_tracks(self, context):
276 t = time.time()
277 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
278 # Select active trackers for tracking
279 bpy.ops.clip.select_all(action='DESELECT')
280 selected = self.get_active_tracks(context)
281 for track in selected:
282 track.select = True
283 debug_print("select_active_tracks %.2f seconds,"
284 " selected: %s" % (time.time() - t, len(selected)))
285 return selected
287 def estimate_motion(self, context, last, frame):
289 compute mean pixel motion for current frame
290 TODO: use statistic here to make filtering more efficient
291 last : last frame number
292 frame: current frame number
293 return mean pixel distance error
295 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
296 nbtracks = 0
297 distance = 0.0
298 for track in tracks:
299 if track.hide or track.lock:
300 continue
301 marker0 = track.markers.find_frame(frame)
302 marker1 = track.markers.find_frame(last)
303 if marker0 is not None and marker1 is not None:
304 d = (marker0.co - marker1.co).length
305 # skip fixed tracks
306 if d > 0:
307 distance += d
308 nbtracks += 1
309 if nbtracks > 0:
310 mean = distance / nbtracks
311 else:
312 # arbitrary set to prevent division by 0 error
313 mean = 10
315 return mean
317 # REMOVE SMALL TRACKS
318 def remove_small(self, context):
319 t = time.time()
320 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
321 to_delete = []
322 bpy.ops.clip.select_all(action='DESELECT')
323 for track in tracks:
324 if track.hide or track.lock:
325 continue
326 if len(track.markers) > 1:
327 marker = track.markers.find_frame(current_frame)
328 if marker is None and self.find_track_length(track) < props.small_tracks:
329 to_delete.append(track)
330 deleted_tracks = len(to_delete)
331 self.delete_tracks(to_delete)
332 debug_print("remove_small %.4f seconds, %s tracks deleted" % (time.time() - t, deleted_tracks))
334 def split_track(self, context, track, split_frame, skip=0):
335 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
336 if props.track_backwards:
337 end = scene.frame_start
338 step = -1
339 else:
340 end = scene.frame_end
341 step = 1
342 new_track = tracks.new(frame=split_frame)
344 for frame in range(split_frame, end, step):
345 marker = track.markers.find_frame(frame)
346 if marker is None:
347 return
348 # add new marker on new track for frame
349 if abs(frame - split_frame) >= skip:
350 new_marker = new_track.markers.find_frame(frame)
351 if new_marker is None:
352 new_marker = new_track.markers.insert_frame(frame)
353 new_marker.co = marker.co
354 # remove marker on track for frame
355 if frame == split_frame:
356 track.hide = True
357 else:
358 track.markers.delete_frame(frame)
359 marker.mute = True
361 # REMOVE JUMPING MARKERS
362 def remove_jumping(self, context):
364 t = time.time()
365 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
367 if props.track_backwards:
368 step = -1
369 else:
370 step = 1
372 to_split = [None for track in tracks]
373 for frame in range(last_frame, current_frame, step):
375 last = frame - step
377 # mean motion (normalized [0-1]) distance for tracks between last and current frame
378 mean = self.estimate_motion(context, last, frame)
380 # how much a track is allowed to move
381 allowed = mean * props.jump_cut
383 for i, track in enumerate(tracks):
384 if track.hide or track.lock:
385 continue
386 marker0 = track.markers.find_frame(frame)
387 marker1 = track.markers.find_frame(last)
388 if marker0 is not None and marker1 is not None:
389 distance = (marker0.co - marker1.co).length
390 # Jump Cut threshold
391 if distance > allowed:
392 if to_split[i] is None:
393 to_split[i] = [frame, frame]
394 else:
395 to_split[i][1] = frame
397 jumping = 0
398 for i, split in enumerate(to_split):
399 if split is not None:
400 self.split_track(context, tracks[i], split[0], abs(split[0] - split[1]))
401 jumping += 1
403 debug_print("remove_jumping: %.4f seconds, %s tracks cut." % (time.time() - t, jumping))
405 def get_frame_range(self, context):
407 get tracking frames range
408 use clip limits when clip shorter than scene
409 else use scene limits
411 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
412 frame_start = max(scene.frame_start, clip.frame_start)
413 frame_end = min(scene.frame_end, clip.frame_start + clip.frame_duration)
414 frame_duration = frame_end - frame_start
415 return frame_start, frame_end, frame_duration
417 def modal(self, context, event):
419 if event.type in {'ESC'}:
420 self.report({'INFO'},
421 "Stopping, up to now added Markers will be kept. Autotracking Finished")
422 self.cancel(context)
423 return {'FINISHED'}
425 scene, props, clip, tracks, current_frame, last_frame = self.get_vars_from_context(context)
426 frame_start, frame_end, frame_duration = self.get_frame_range(context)
428 if (((not props.track_backwards) and current_frame >= frame_end) or
429 (props.track_backwards and current_frame <= frame_start)):
431 self.report({'INFO'},
432 "Reached the end of the Clip. Autotracking Finished")
433 self.cancel(context)
434 return {'FINISHED'}
436 # do not run this modal while tracking operator runs
437 # Known issue, you'll have to keep ESC pressed
438 if event.type not in {'TIMER'} or context.scene.frame_current != self.next_frame:
439 return {'PASS_THROUGH'}
441 # prevent own TIMER event while running
442 self.stop_timer(context)
444 if props.track_backwards:
445 self.next_frame = scene.frame_current - props.frame_separation
446 total = self.start_frame - frame_start
447 else:
448 self.next_frame = scene.frame_current + props.frame_separation
449 total = frame_end - self.start_frame
451 if total > 0:
452 self.progress = (current_frame - self.start_frame) / total
453 else:
454 self.progress = 0
456 debug_print("Tracking frame %s" % (scene.frame_current))
458 # Remove bad tracks before adding new ones
459 self.remove_small(context)
460 self.remove_jumping(context)
462 # add new tracks
463 self.auto_features(context)
465 # Select active trackers for tracking
466 active_tracks = self.select_active_tracks(context)
468 # finish if there is nothing to track
469 if len(active_tracks) == 0:
470 self.report({'INFO'},
471 "No new tracks created, nothing to track. Autotrack Finished")
472 self.cancel(context)
473 return {'FINISHED'}
475 # setup frame_limit on tracks
476 for track in active_tracks:
477 track.frames_limit = 0
478 active_tracks[0].frames_limit = props.frame_separation
480 # Forwards or backwards tracking
481 if props.track_backwards:
482 self.track_frames_backward()
483 else:
484 self.track_frames_forward()
486 # setup a timer to broadcast a TIMER event to force modal to
487 # re-run as fast as possible (not waiting for any mouse or keyboard event)
488 self.start_timer(context)
490 return {'RUNNING_MODAL'}
492 def invoke(self, context, event):
493 scene = context.scene
494 frame_start, frame_end, frame_duration = self.get_frame_range(context)
496 if scene.frame_current > frame_end:
497 scene.frame_current = frame_end
498 elif scene.frame_current < frame_start:
499 scene.frame_current = frame_start
501 self.start_frame = scene.frame_current
502 self.start = (scene.frame_current - frame_start) / (frame_duration)
503 self.progress = 0
505 # keep track of frame at witch we should detect new features and filter tracks
506 self.next_frame = scene.frame_current
508 # draw progress
509 args = (self, context)
510 self._draw_handler = bpy.types.SpaceClipEditor.draw_handler_add(
511 draw_callback, args,
512 'WINDOW', 'POST_PIXEL'
514 self.start_timer(context)
515 context.window_manager.modal_handler_add(self)
516 return {'RUNNING_MODAL'}
518 def __init__(self):
519 self.t = time.time()
521 def __del__(self):
522 debug_print("AutoTrack %.2f seconds" % (time.time() - self.t))
524 def execute(self, context):
525 debug_print("Autotrack execute called")
526 return {'FINISHED'}
528 def stop_timer(self, context):
529 context.window_manager.event_timer_remove(self._timer)
531 def start_timer(self, context):
532 self._timer = context.window_manager.event_timer_add(time_step=0.1, window=context.window)
534 def cancel(self, context):
535 self.stop_timer(context)
536 self.show_tracks(context)
537 bpy.types.SpaceClipEditor.draw_handler_remove(self._draw_handler, 'WINDOW')
539 @classmethod
540 def poll(cls, context):
541 return (context.area.spaces.active.clip is not None)
544 class AutotrackerSettings(PropertyGroup):
545 """Create properties"""
546 df_margin: FloatProperty(
547 name="Detect Features Margin",
548 description="Only consider features from pixels located outside\n"
549 "the defined margin from the clip borders",
550 subtype='PERCENTAGE',
551 default=5,
552 min=0,
553 max=100
555 df_threshold: FloatProperty(
556 name="Detect Features Threshold",
557 description="Threshold level to deem a feature being good enough for tracking",
558 default=0.3,
559 min=0.0,
560 max=1.0
562 # Note: merge this one with delete_threshold
563 df_distance: FloatProperty(
564 name="Detect Features Distance",
565 description="Minimal acceptable distance between two features",
566 subtype='PERCENTAGE',
567 default=8,
568 min=1,
569 max=100
571 delete_threshold: FloatProperty(
572 name="New Marker Threshold",
573 description="Threshold of how close a new features can appear during tracking",
574 subtype='PERCENTAGE',
575 default=8,
576 min=1,
577 max=100
579 small_tracks: IntProperty(
580 name="Minimum track length",
581 description="Delete tracks shorter than this number of frames\n"
582 "Note: set to 0 for keeping all tracks",
583 default=50,
584 min=1,
585 max=1000
587 frame_separation: IntProperty(
588 name="Frame Separation",
589 description="How often new features are generated",
590 default=5,
591 min=1,
592 max=100
594 jump_cut: FloatProperty(
595 name="Jump Cut",
596 description="How much distance a marker can travel before it is considered "
597 "to be a bad track and cut.\nA new track will be added "
598 "(factor relative to mean motion)",
599 default=5.0,
600 min=0.0,
601 max=50.0
603 track_backwards: BoolProperty(
604 name="AutoTrack Backwards",
605 description="Track from the last frame of the selected clip",
606 default=False
608 # Dropdown menu
609 list_items = [
610 ("FRAME", "Whole Frame", "", 1),
611 ("INSIDE_GPENCIL", "Inside Grease Pencil", "", 2),
612 ("OUTSIDE_GPENCIL", "Outside Grease Pencil", "", 3),
614 placement_list: EnumProperty(
615 name="Placement",
616 description="Feature Placement",
617 items=list_items
622 NOTE:
623 All size properties are in percent of the clip size,
624 so presets do not depend on the clip size
628 class AutotrackerPanel(Panel):
629 """Creates a Panel in the Render Layer properties window"""
630 bl_label = "Autotrack"
631 bl_idname = "autotrack"
632 bl_space_type = 'CLIP_EDITOR'
633 bl_region_type = 'TOOLS'
634 bl_category = "Track"
636 @classmethod
637 def poll(cls, context):
638 return (context.area.spaces.active.clip is not None)
640 def draw(self, context):
641 layout = self.layout
642 wm = context.window_manager
644 row = layout.row()
645 row.scale_y = 1.5
646 row.operator("tracking.auto_track", text="Autotrack! ", icon='PLAY')
648 row = layout.row()
649 row.prop(wm.autotracker_props, "track_backwards")
651 row = layout.row()
652 col = layout.column(align=True)
653 col.prop(wm.autotracker_props, "delete_threshold")
654 col.prop(wm.autotracker_props, "small_tracks")
655 col.prop(wm.autotracker_props, "frame_separation", text="Frame Separation")
656 col.prop(wm.autotracker_props, "jump_cut", text="Jump Threshold")
658 row = layout.row()
659 row.label(text="Detect Features Settings:")
660 col = layout.column(align=True)
661 col.prop(wm.autotracker_props, "df_margin", text="Margin:")
662 col.prop(wm.autotracker_props, "df_distance", text="Distance:")
663 col.prop(wm.autotracker_props, "df_threshold", text="Threshold:")
665 row = layout.row()
666 row.label(text="Feature Placement:")
667 col = layout.column(align=True)
668 col.prop(wm.autotracker_props, "placement_list", text="")
671 def register():
672 bpy.utils.register_class(AutotrackerSettings)
673 WindowManager.autotracker_props = PointerProperty(
674 type=AutotrackerSettings
676 bpy.utils.register_module(__name__)
679 def unregister():
680 bpy.utils.unregister_class(AutotrackerSettings)
681 bpy.utils.unregister_module(__name__)
682 del WindowManager.autotracker_props
685 if __name__ == "__main__":
686 register()