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 "doc_url": "https://github.com/miikapuustinen/blender_autotracker",
28 "category": "Video Tools",
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 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
)
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):
88 # NOTE: this is no longer supported in Blender3.0.
89 # style = bgl.GL_LINE_STIPPLE
91 bgl
.glPushAttrib(bgl
.GL_ENABLE_BIT
)
92 bgl
.glLineStipple(1, 0x9999)
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
)
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
)
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
)
124 xi
= xs
+ (x1
- xs
) * float(percent
)
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")
146 gl
= GlDrawOnScreen()
151 def find_track_start(self
, track
):
152 for m
in track
.markers
:
155 return track
.markers
[0].frame
157 def find_track_end(self
, track
):
158 for m
in reversed(track
.markers
):
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
)
168 def show_tracks(self
, context
):
169 clip
= context
.area
.spaces
.active
.clip
170 tracks
= clip
.tracking
.tracks
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
)
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
:
192 bpy
.ops
.clip
.delete_track()
195 def auto_features(self
, context
):
201 scene
, props
, clip
, tracks
, current_frame
, last_frame
= self
.get_vars_from_context(context
)
207 delete_threshold
= float(props
.delete_threshold
) / 100.0
209 bpy
.ops
.clip
.select_all(action
='DESELECT')
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
221 if track
.hide
or track
.lock
:
223 marker
= track
.markers
.find_frame(current_frame
)
224 if marker
is not None:
225 if (not track
.select
) and (not marker
.mute
):
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
)
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
))
248 def track_frames_backward(self
):
249 # INVOKE_DEFAULT to show progress and take account of frame_limit
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
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
)
265 if track
.hide
or track
.lock
:
267 if len(track
.markers
) < 2:
268 active_tracks
.append(track
)
270 marker
= track
.markers
.find_frame(current_frame
)
271 if (marker
is not None) and (not marker
.mute
):
272 active_tracks
.append(track
)
275 def select_active_tracks(self
, context
):
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
:
283 debug_print("select_active_tracks %.2f seconds,"
284 " selected: %s" % (time
.time() - t
, len(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
)
299 if track
.hide
or track
.lock
:
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
310 mean
= distance
/ nbtracks
312 # arbitrary set to prevent division by 0 error
317 # REMOVE SMALL TRACKS
318 def remove_small(self
, context
):
320 scene
, props
, clip
, tracks
, current_frame
, last_frame
= self
.get_vars_from_context(context
)
322 bpy
.ops
.clip
.select_all(action
='DESELECT')
324 if track
.hide
or track
.lock
:
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
340 end
= scene
.frame_end
342 new_track
= tracks
.new(frame
=split_frame
)
344 for frame
in range(split_frame
, end
, step
):
345 marker
= track
.markers
.find_frame(frame
)
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
:
358 track
.markers
.delete_frame(frame
)
361 # REMOVE JUMPING MARKERS
362 def remove_jumping(self
, context
):
365 scene
, props
, clip
, tracks
, current_frame
, last_frame
= self
.get_vars_from_context(context
)
367 if props
.track_backwards
:
372 to_split
= [None for track
in tracks
]
373 for frame
in range(last_frame
, current_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
:
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
391 if distance
> allowed
:
392 if to_split
[i
] is None:
393 to_split
[i
] = [frame
, frame
]
395 to_split
[i
][1] = frame
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]))
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")
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")
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
448 self
.next_frame
= scene
.frame_current
+ props
.frame_separation
449 total
= frame_end
- self
.start_frame
452 self
.progress
= (current_frame
- self
.start_frame
) / total
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
)
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")
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()
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
)
505 # keep track of frame at witch we should detect new features and filter tracks
506 self
.next_frame
= scene
.frame_current
509 args
= (self
, context
)
510 self
._draw
_handler
= bpy
.types
.SpaceClipEditor
.draw_handler_add(
512 'WINDOW', 'POST_PIXEL'
514 self
.start_timer(context
)
515 context
.window_manager
.modal_handler_add(self
)
516 return {'RUNNING_MODAL'}
522 debug_print("AutoTrack %.2f seconds" % (time
.time() - self
.t
))
524 def execute(self
, context
):
525 debug_print("Autotrack execute called")
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')
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',
555 df_threshold
: FloatProperty(
556 name
="Detect Features Threshold",
557 description
="Threshold level to deem a feature being good enough for tracking",
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',
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',
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",
587 frame_separation
: IntProperty(
588 name
="Frame Separation",
589 description
="How often new features are generated",
594 jump_cut
: FloatProperty(
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)",
603 track_backwards
: BoolProperty(
604 name
="AutoTrack Backwards",
605 description
="Track from the last frame of the selected clip",
610 ("FRAME", "Whole Frame", "", 1),
611 ("INSIDE_GPENCIL", "Inside Grease Pencil", "", 2),
612 ("OUTSIDE_GPENCIL", "Outside Grease Pencil", "", 3),
614 placement_list
: EnumProperty(
616 description
="Feature Placement",
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"
637 def poll(cls
, context
):
638 return (context
.area
.spaces
.active
.clip
is not None)
640 def draw(self
, context
):
642 wm
= context
.window_manager
646 row
.operator("tracking.auto_track", text
="Autotrack! ", icon
='PLAY')
649 row
.prop(wm
.autotracker_props
, "track_backwards")
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")
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:")
666 row
.label(text
="Feature Placement:")
667 col
= layout
.column(align
=True)
668 col
.prop(wm
.autotracker_props
, "placement_list", text
="")
672 bpy
.utils
.register_class(AutotrackerSettings
)
673 WindowManager
.autotracker_props
= PointerProperty(
674 type=AutotrackerSettings
676 bpy
.utils
.register_module(__name__
)
680 bpy
.utils
.unregister_class(AutotrackerSettings
)
681 bpy
.utils
.unregister_module(__name__
)
682 del WindowManager
.autotracker_props
685 if __name__
== "__main__":