tracksperanto 3.3.13 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +4 -0
- data/README.md +2 -0
- data/lib/export/flame_stabilizer_2014.rb +259 -0
- data/lib/export/flame_stabilizer_2014_cornerpin.rb +108 -0
- data/lib/tracksperanto.rb +1 -1
- data/test/export/samples/ref_flame_2014.stabilizer +893 -0
- data/test/export/samples/ref_flame_2014_cornerpin.stabilizer +1422 -0
- data/test/export/test_flame_stabilizer_2014_cornerpin_export.rb +34 -0
- data/test/export/test_flame_stabilizer_2014_export.rb +18 -0
- data/test/import/test_shake_script_import.rb +1 -1
- data/test/test_cli.rb +3 -1
- data/tracksperanto.gemspec +8 -2
- metadata +11 -5
data/History.txt
CHANGED
data/README.md
CHANGED
@@ -119,6 +119,8 @@ If you want your own copy of the web application at your facility we can discuss
|
|
119
119
|
* Autodesk Softimage nulls Python script
|
120
120
|
* Bare Ruby code
|
121
121
|
* Flame/Smoke 2D Stabilizer setup
|
122
|
+
* Flame/Smoke 2D Stabilizer setup (v. 2014 and above)
|
123
|
+
* Flame/Smoke 2D Stabilizer setup (v. 2014 and above) for corner pins
|
122
124
|
* Flame/Smoke 2D Stabilizer setup for bilinear corner pins
|
123
125
|
* MatchMover REALVIZ Ascii Point Tracks .rz2 file
|
124
126
|
* Maya ASCII scene with locators on an image plane
|
@@ -0,0 +1,259 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
class Tracksperanto::Export::FlameStabilizer2014 < Tracksperanto::Export::Base
|
3
|
+
|
4
|
+
COLOR = "50 50 50"
|
5
|
+
DATETIME_FORMAT = '%a %b %d %H:%M:%S %Y'
|
6
|
+
|
7
|
+
def self.desc_and_extension
|
8
|
+
"flamesmoke2014.stabilizer"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.human_name
|
12
|
+
"Flame/Smoke 2D Stabilizer setup (v. 2014 and above)"
|
13
|
+
end
|
14
|
+
|
15
|
+
def start_export( img_width, img_height)
|
16
|
+
@counter = 0
|
17
|
+
@first_ref_frames = []
|
18
|
+
@width, @height = img_width, img_height
|
19
|
+
@temp = Tracksperanto::BufferIO.new
|
20
|
+
@writer = FlameChannelParser::Builder.new(@temp)
|
21
|
+
end
|
22
|
+
|
23
|
+
def end_export
|
24
|
+
# Now make another writer, this time for our main IO
|
25
|
+
@writer = FlameChannelParser::Builder.new(@io)
|
26
|
+
|
27
|
+
# Now we know how many trackers we have so we can write the header
|
28
|
+
# data along with NbTrackers
|
29
|
+
write_header_with_number_of_trackers(@counter)
|
30
|
+
|
31
|
+
# Now write everything that we accumulated earlier into the base IO
|
32
|
+
@temp.rewind
|
33
|
+
@io.write(@temp.read) until @temp.eof?
|
34
|
+
@temp.close!
|
35
|
+
|
36
|
+
# Send the ChannelEnd command and list the trackers
|
37
|
+
@writer.channel_end
|
38
|
+
@first_ref_frames.each_with_index do | ref_frame, i|
|
39
|
+
@writer.write_unterminated_block!("tracker", i) do |t|
|
40
|
+
t.active true
|
41
|
+
t.color_hash!("colour", 0, 100, 0)
|
42
|
+
t.fixed_ref true
|
43
|
+
t.fixed_x false
|
44
|
+
t.fixed_y false
|
45
|
+
t.tolerance 100
|
46
|
+
t.offsets_x 0
|
47
|
+
t.offsets_y 0
|
48
|
+
t.first_ref_frame ref_frame
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Write the finalizing "End"
|
53
|
+
@writer.write_loose!("end")
|
54
|
+
end
|
55
|
+
|
56
|
+
def start_tracker_segment(tracker_name)
|
57
|
+
@counter += 1
|
58
|
+
@write_first_frame = true
|
59
|
+
end
|
60
|
+
|
61
|
+
def export_point(frame, abs_float_x, abs_float_y, float_residual)
|
62
|
+
flame_frame = frame + 1
|
63
|
+
if @write_first_frame
|
64
|
+
export_first_point(flame_frame, abs_float_x, abs_float_y)
|
65
|
+
# Record the first ref frame for this tracker
|
66
|
+
@first_ref_frames << flame_frame
|
67
|
+
@write_first_frame = false
|
68
|
+
else
|
69
|
+
export_remaining_point(flame_frame, abs_float_x, abs_float_y)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def end_tracker_segment
|
74
|
+
# We write these at tracker end since we need to know in advance
|
75
|
+
# how many keyframes they should contain
|
76
|
+
write_shift_channel("shift/x", @x_shift_values)
|
77
|
+
write_shift_channel("shift/y", @y_shift_values)
|
78
|
+
|
79
|
+
# And finish with the offset channels. The order of channels is important!
|
80
|
+
# (otherwise the last tracker's shift animation is not imported by Flame)
|
81
|
+
# https://github.com/guerilla-di/tracksperanto/issues/1
|
82
|
+
write_offset_channels
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def export_remaining_point(flame_frame, abs_float_x, abs_float_y)
|
88
|
+
# Just continue buffering the upcoming shift keyframes and flush them in the end
|
89
|
+
shift_x, shift_y = @base_x - abs_float_x, @base_y - abs_float_y
|
90
|
+
@x_shift_values.push([flame_frame, shift_x])
|
91
|
+
@y_shift_values.push([flame_frame, shift_y])
|
92
|
+
end
|
93
|
+
|
94
|
+
def export_first_point(flame_frame, abs_float_x, abs_float_y)
|
95
|
+
@base_x, @base_y = abs_float_x, abs_float_y
|
96
|
+
write_first_frame(abs_float_x, abs_float_y)
|
97
|
+
# For Flame to recognize the reference frame of the Shift channel
|
98
|
+
# we need it to contain zero as an int, not as a float. The shift proceeds
|
99
|
+
# from there.
|
100
|
+
@x_shift_values = [[flame_frame, 0]]
|
101
|
+
@y_shift_values = [[flame_frame, 0]]
|
102
|
+
end
|
103
|
+
|
104
|
+
# The shift channel is what determines how the tracking point moves.
|
105
|
+
def write_shift_channel(name_without_prefix, values)
|
106
|
+
@writer.channel(prefix(name_without_prefix)) do | c |
|
107
|
+
c.extrapolation :constant
|
108
|
+
c.value values[0][1]
|
109
|
+
c.key_version 1
|
110
|
+
c.size values.length
|
111
|
+
values.each_with_index do | (f, v), i |
|
112
|
+
c.key(i) do | k |
|
113
|
+
k.frame f
|
114
|
+
k.value v
|
115
|
+
# Omit:
|
116
|
+
# RHandle_dX 0.333333343
|
117
|
+
# RHandle_dY -5.7284646
|
118
|
+
# LHandle_dX -0.333333343
|
119
|
+
# LHandle_dY 5.7284646
|
120
|
+
k.curve_mode :hermite
|
121
|
+
k.curve_order :linear
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def prefix(tracker_channel)
|
128
|
+
"tracker%d/%s" % [@counter, tracker_channel]
|
129
|
+
end
|
130
|
+
|
131
|
+
def write_header_with_number_of_trackers(number_of_trackers)
|
132
|
+
@writer.stabilizer_file_version "6.0"
|
133
|
+
@writer.creation_date(Time.now.strftime(DATETIME_FORMAT))
|
134
|
+
@writer.linebreak!(2)
|
135
|
+
|
136
|
+
@writer.nb_trackers number_of_trackers
|
137
|
+
@writer.selected 0
|
138
|
+
@writer.frame_width @width
|
139
|
+
@writer.frame_height @height
|
140
|
+
@writer.auto_key true
|
141
|
+
@writer.motion_path true
|
142
|
+
@writer.icons true
|
143
|
+
@writer.auto_pan false # hate it!
|
144
|
+
@writer.edit_mode 0
|
145
|
+
@writer.format 0
|
146
|
+
@writer.color_hash!("padding", 0, 100, 0)
|
147
|
+
@writer.oversampling false
|
148
|
+
@writer.opacity 50
|
149
|
+
@writer.zoom 3
|
150
|
+
@writer.field false
|
151
|
+
@writer.backward false
|
152
|
+
@writer.anim
|
153
|
+
end
|
154
|
+
|
155
|
+
def write_first_frame(x, y)
|
156
|
+
write_track_channels
|
157
|
+
write_track_width_and_height
|
158
|
+
write_ref_width_and_height
|
159
|
+
write_ref_channels(x, y)
|
160
|
+
write_deltax_and_deltay_channels
|
161
|
+
end
|
162
|
+
|
163
|
+
def write_track_channels
|
164
|
+
ctr_x, ctr_y = @width / 2, @height / 2
|
165
|
+
|
166
|
+
# track determines where the tracking box is, and should be in the center
|
167
|
+
# of the image for Flame to compute all other shifts properly
|
168
|
+
%w( track/x track/y).map(&method(:prefix)).zip([ctr_x, ctr_y]).each do | cname, default |
|
169
|
+
@writer.channel(cname) do | c |
|
170
|
+
c.extrapolation("constant")
|
171
|
+
c.value(default.to_i)
|
172
|
+
c.colour(COLOR)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# The size of the tracking area
|
178
|
+
def write_track_width_and_height
|
179
|
+
%w( track/width track/height ).map(&method(:prefix)).each do | channel_name |
|
180
|
+
@writer.channel(channel_name) do | c |
|
181
|
+
c.extrapolation :linear
|
182
|
+
c.value 15
|
183
|
+
c.colour COLOR
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# The size of the reference area
|
189
|
+
def write_ref_width_and_height
|
190
|
+
%w( ref/width ref/height).map(&method(:prefix)).each do | channel_name |
|
191
|
+
@writer.channel(channel_name) do | c |
|
192
|
+
c.extrapolation :linear
|
193
|
+
c.value 10
|
194
|
+
c.colour COLOR
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# The Ref channel contains the reference for the shift channel in absolute
|
200
|
+
# coordinates, and is set as float. Since we do not "snap" the tracker in
|
201
|
+
# the process it's enough for us to make one keyframe in the ref channels
|
202
|
+
# at the same frame as the first shift keyframe
|
203
|
+
def write_ref_channels(ref_x, ref_y)
|
204
|
+
%w( ref/x ref/y).map(&method(:prefix)).zip([ref_x, ref_y]).each do | cname, default |
|
205
|
+
@writer.channel(cname) do | c |
|
206
|
+
c.extrapolation("constant")
|
207
|
+
c.value(default)
|
208
|
+
c.colour(COLOR)
|
209
|
+
c.key_version 1
|
210
|
+
c.size 1
|
211
|
+
c.key(0) do | k |
|
212
|
+
k.frame 1
|
213
|
+
k.value default
|
214
|
+
k.interpolation :constant
|
215
|
+
k.left_slope 2.4
|
216
|
+
k.right_slope 2.4
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def write_deltax_and_deltay_channels
|
223
|
+
# This is used for deltax and deltay (offset tracking).
|
224
|
+
# We set it to zero and lock
|
225
|
+
%w( ref/dx ref/dy).map(&method(:prefix)).each do | chan, v |
|
226
|
+
@writer.channel(chan) do | c |
|
227
|
+
c.extrapolation("constant")
|
228
|
+
c.value 0
|
229
|
+
c.colour(COLOR)
|
230
|
+
c.size 2
|
231
|
+
c.key_version 1
|
232
|
+
c.key(0) do | k |
|
233
|
+
k.frame 0
|
234
|
+
k.value 0
|
235
|
+
k.value_lock true
|
236
|
+
k.delete_lock true
|
237
|
+
k.interpolation :constant
|
238
|
+
end
|
239
|
+
c.key(1) do | k |
|
240
|
+
k.frame 1
|
241
|
+
k.value 0
|
242
|
+
k.value_lock true
|
243
|
+
k.delete_lock true
|
244
|
+
k.interpolation :constant
|
245
|
+
end
|
246
|
+
end # Chan block
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def write_offset_channels
|
251
|
+
%w(offset/x offset/y).map(&method(:prefix)).each do | c |
|
252
|
+
@writer.channel(c) do | chan |
|
253
|
+
chan.extrapolation :constant
|
254
|
+
chan.value 0
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# Exports setups with tracker naming that works with the Action bilinears
|
3
|
+
class Tracksperanto::Export::FlameStabilizer2014Cornerpin < Tracksperanto::Export::FlameStabilizer2014
|
4
|
+
|
5
|
+
def self.desc_and_extension
|
6
|
+
"flamesmoke_2014_cornerpin.stabilizer"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.human_name
|
10
|
+
"Flame/Smoke 2D Stabilizer setup (v. 2014 and above) for corner pins"
|
11
|
+
end
|
12
|
+
|
13
|
+
# The trackers for cornerpins should go in Z order, but now
|
14
|
+
# in N order, unline it did previously. Like so:
|
15
|
+
#
|
16
|
+
# TL(1) TR(3)
|
17
|
+
# ˆ \ ˆ
|
18
|
+
# | \ |
|
19
|
+
# | \ |
|
20
|
+
# | \ |
|
21
|
+
# BL(0) BR(2)
|
22
|
+
#
|
23
|
+
# This "kinda tool" ensures that this is indeed taking place
|
24
|
+
class Sorter
|
25
|
+
include Tracksperanto::SimpleExport # so that it calls OUR methods
|
26
|
+
|
27
|
+
def initialize(exporter)
|
28
|
+
@exp = exporter
|
29
|
+
end
|
30
|
+
|
31
|
+
def start_export(w,h)
|
32
|
+
@width, @height = w, h
|
33
|
+
@corners, @four_done = [], false
|
34
|
+
end
|
35
|
+
|
36
|
+
def start_tracker_segment(name)
|
37
|
+
@four_done = (@corners.length == 4)
|
38
|
+
return if @four_done
|
39
|
+
@corners.push(Tracksperanto::Tracker.new(:name => name))
|
40
|
+
end
|
41
|
+
|
42
|
+
def export_point(f, x, y, r)
|
43
|
+
return if @four_done
|
44
|
+
@corners[-1].keyframe! :frame => f, :abs_x => x, :abs_y => y, :residual => r
|
45
|
+
end
|
46
|
+
|
47
|
+
def end_tracker_segment
|
48
|
+
# Just leave that
|
49
|
+
end
|
50
|
+
|
51
|
+
def end_export
|
52
|
+
# We will have problems sorting if we have too few trackers
|
53
|
+
return @exp.just_export(@corners, @width, @height) unless @corners.length == 4
|
54
|
+
|
55
|
+
# Sort the trackers, first in Y of the first keyframe
|
56
|
+
in_y = sort_on_first_keyframe(@corners, :abs_y)
|
57
|
+
|
58
|
+
# then on the X for the two separate blocks for top and bottom
|
59
|
+
tl, tr = sort_on_first_keyframe(in_y[2..3], :abs_x)
|
60
|
+
bl, br = sort_on_first_keyframe(in_y[0..1], :abs_x)
|
61
|
+
|
62
|
+
bulk = [bl, tl, br, tr] # New Flame 2014 order
|
63
|
+
|
64
|
+
@exp.just_export(bulk, @width, @height)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def sort_on_first_keyframe(enum, property)
|
70
|
+
enum.sort{|a, b| a[0].send(property) <=> b[0].send(property) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Initialize the exporter with a preconfigured sorter around it.
|
75
|
+
# When this object receives the commands they will come from the Sorter instead,
|
76
|
+
# and the trackers will already be in their Z-order
|
77
|
+
def self.new(*arguments)
|
78
|
+
object = super
|
79
|
+
Sorter.new(object)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Now instead of names we got vague indices. YAY for Rue Duc!
|
83
|
+
CORNERPIN_NAMING = %w( none tracker0_0 tracker0_1 tracker1_0 tracker1_1 )
|
84
|
+
|
85
|
+
# Overridden to give the right names to trackers
|
86
|
+
def prefix(tracker_channel)
|
87
|
+
tracker_name = CORNERPIN_NAMING[@counter]
|
88
|
+
[tracker_name, tracker_channel].join("/")
|
89
|
+
end
|
90
|
+
|
91
|
+
def start_tracker_segment(tracker_name)
|
92
|
+
if (@counter == 4)
|
93
|
+
@skip = true
|
94
|
+
else
|
95
|
+
super
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def export_point(frame, abs_float_x, abs_float_y, float_residual)
|
100
|
+
return if @skip
|
101
|
+
super
|
102
|
+
end
|
103
|
+
|
104
|
+
def end_tracker_segment
|
105
|
+
return if @skip
|
106
|
+
super
|
107
|
+
end
|
108
|
+
end
|