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 #####
21 "author": "Miika Puustinen, Matti Kaihola, Stephen Leger",
23 "blender": (2, 78, 0),
24 "location": "Movie clip Editor > Tools Panel > Autotrack",
25 "description": "Motion Tracking with automatic feature detection.",
27 "wiki_url": "https://github.com/miikapuustinen/blender_autotracker",
28 "category": "Motion Tracking",
34 from bpy
.types
import (
40 from bpy
.props
import (
51 # set to True to enable debug prints
55 # pass variables just like for the regular prints
56 def debug_print(*args
, **kwargs
):
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
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
)
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)
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
)
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
)
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
)
120 xi
= xs
+ (x1
- xs
) * float(percent
)
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")
142 gl
= GlDrawOnScreen()
147 def find_track_start(self
, track
):
148 for m
in track
.markers
:
151 return track
.markers
[0].frame
153 def find_track_end(self
, track
):
154 for m
in reversed(track
.markers
):
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
)
164 def show_tracks(self
, context
):
165 clip
= context
.area
.spaces
.active
.clip
166 tracks
= clip
.tracking
.tracks
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
)
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
:
188 bpy
.ops
.clip
.delete_track()
191 def auto_features(self
, context
):
197 scene
, props
, clip
, tracks
, current_frame
, last_frame
= self
.get_vars_from_context(context
)
203 delete_threshold
= float(props
.delete_threshold
) / 100.0
205 bpy
.ops
.clip
.select_all(action
='DESELECT')
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
217 if track
.hide
or track
.lock
:
219 marker
= track
.markers
.find_frame(current_frame
)
220 if marker
is not None:
221 if (not track
.select
) and (not marker
.mute
):
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
)
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
))
244 def track_frames_backward(self
):
245 # INVOKE_DEFAULT to show progress and take account of frame_limit
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
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
)
261 if track
.hide
or track
.lock
:
263 if len(track
.markers
) < 2:
264 active_tracks
.append(track
)
266 marker
= track
.markers
.find_frame(current_frame
)
267 if (marker
is not None) and (not marker
.mute
):
268 active_tracks
.append(track
)
271 def select_active_tracks(self
, context
):
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
:
279 debug_print("select_active_tracks %.2f seconds,"
280 " selected: %s" % (time
.time() - t
, len(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
)
295 if track
.hide
or track
.lock
:
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
306 mean
= distance
/ nbtracks
308 # arbitrary set to prevent division by 0 error
313 # REMOVE SMALL TRACKS
314 def remove_small(self
, context
):
316 scene
, props
, clip
, tracks
, current_frame
, last_frame
= self
.get_vars_from_context(context
)
318 bpy
.ops
.clip
.select_all(action
='DESELECT')
320 if track
.hide
or track
.lock
:
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
336 end
= scene
.frame_end
338 new_track
= tracks
.new(frame
=split_frame
)
340 for frame
in range(split_frame
, end
, step
):
341 marker
= track
.markers
.find_frame(frame
)
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
:
354 track
.markers
.delete_frame(frame
)
357 # REMOVE JUMPING MARKERS
358 def remove_jumping(self
, context
):
361 scene
, props
, clip
, tracks
, current_frame
, last_frame
= self
.get_vars_from_context(context
)
363 if props
.track_backwards
:
368 to_split
= [None for track
in tracks
]
369 for frame
in range(last_frame
, current_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
:
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
387 if distance
> allowed
:
388 if to_split
[i
] is None:
389 to_split
[i
] = [frame
, frame
]
391 to_split
[i
][1] = frame
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]))
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")
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")
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
444 self
.next_frame
= scene
.frame_current
+ props
.frame_separation
445 total
= frame_end
- self
.start_frame
448 self
.progress
= (current_frame
- self
.start_frame
) / total
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
)
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")
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()
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
)
501 # keep track of frame at witch we should detect new features and filter tracks
502 self
.next_frame
= scene
.frame_current
505 args
= (self
, context
)
506 self
._draw
_handler
= bpy
.types
.SpaceClipEditor
.draw_handler_add(
508 'WINDOW', 'POST_PIXEL'
510 self
.start_timer(context
)
511 context
.window_manager
.modal_handler_add(self
)
512 return {'RUNNING_MODAL'}
518 debug_print("AutoTrack %.2f seconds" % (time
.time() - self
.t
))
520 def execute(self
, context
):
521 debug_print("Autotrack execute called")
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')
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',
551 df_threshold
= FloatProperty(
552 name
="Detect Features Threshold",
553 description
="Threshold level to deem a feature being good enough for tracking",
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',
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',
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",
583 frame_separation
= IntProperty(
584 name
="Frame Separation",
585 description
="How often new features are generated",
590 jump_cut
= FloatProperty(
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)",
599 track_backwards
= BoolProperty(
600 name
="AutoTrack Backwards",
601 description
="Track from the last frame of the selected clip",
606 ("FRAME", "Whole Frame", "", 1),
607 ("INSIDE_GPENCIL", "Inside Grease Pencil", "", 2),
608 ("OUTSIDE_GPENCIL", "Outside Grease Pencil", "", 3),
610 placement_list
= EnumProperty(
612 description
="Feature Placement",
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"
633 def poll(cls
, context
):
634 return (context
.area
.spaces
.active
.clip
is not None)
636 def draw(self
, context
):
638 wm
= context
.window_manager
642 row
.operator("tracking.auto_track", text
="Autotrack! ", icon
='PLAY')
645 row
.prop(wm
.autotracker_props
, "track_backwards")
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")
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:")
662 row
.label(text
="Feature Placement:")
663 col
= layout
.column(align
=True)
664 col
.prop(wm
.autotracker_props
, "placement_list", text
="")
668 bpy
.utils
.register_class(AutotrackerSettings
)
669 WindowManager
.autotracker_props
= PointerProperty(
670 type=AutotrackerSettings
672 bpy
.utils
.register_module(__name__
)
676 bpy
.utils
.unregister_class(AutotrackerSettings
)
677 bpy
.utils
.unregister_module(__name__
)
678 del WindowManager
.autotracker_props
681 if __name__
== "__main__":