tracksperanto 3.3.13 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|