# TODO: this exporter is MAJORLY slow now class Tracksperanto::Export::FlameStabilizer < Tracksperanto::Export::Base COLOR = "50 50 50" DATETIME_FORMAT = '%a %b %d %H:%M:%S %Y' def self.desc_and_extension "flame.stabilizer" end def self.human_name "Flame/Smoke 2D Stabilizer setup" end def start_export( img_width, img_height) @counter = 0 @width, @height = img_width, img_height @temp = Tracksperanto::BufferIO.new @writer = FlameChannelParser::Builder.new(@temp) end def end_export # Now make another writer, this time for our main IO @writer = FlameChannelParser::Builder.new(@io) # Now we know how many trackers we have so we can write the header # data along with NbTrackers write_header_with_number_of_trackers(@counter) # Now write everything that we accumulated earlier into the base IO @temp.rewind @io.write(@temp.read) until @temp.eof? @temp.close! # Send the ChannelEnd command and list the trackers @writer.channel_end @counter.times do |i| @writer.write_unterminated_block!("tracker", i) do |t| t.active true t.color_hash!("colour", 0, 100, 0) t.fixed_ref true t.fixed_x false t.fixed_y false t.tolerance 100 end end # Write the finalizing "End" @writer.write_loose!("end") end def start_tracker_segment(tracker_name) @counter += 1 @write_first_frame = true end def export_point(frame, abs_float_x, abs_float_y, float_residual) flame_frame = frame + 1 if @write_first_frame export_first_point(flame_frame, abs_float_x, abs_float_y) @write_first_frame = false else export_remaining_point(flame_frame, abs_float_x, abs_float_y) end end def end_tracker_segment # We write these at tracker end since we need to know in advance # how many keyframes they should contain write_shift_channel("shift/x", @x_shift_values) write_shift_channel("shift/y", @y_shift_values) # And finish with the offset channels. The order of channels is important! # (otherwise the last tracker's shift animation is not imported by Flame) # https://github.com/guerilla-di/tracksperanto/issues/1 write_offset_channels end private def export_remaining_point(flame_frame, abs_float_x, abs_float_y) # Just continue buffering the upcoming shift keyframes and flush them in the end shift_x, shift_y = @base_x - abs_float_x, @base_y - abs_float_y @x_shift_values.push([flame_frame, shift_x]) @y_shift_values.push([flame_frame, shift_y]) end def export_first_point(flame_frame, abs_float_x, abs_float_y) @base_x, @base_y = abs_float_x, abs_float_y write_first_frame(abs_float_x, abs_float_y) # For Flame to recognize the reference frame of the Shift channel # we need it to contain zero as an int, not as a float. The shift proceeds # from there. @x_shift_values = [[flame_frame, 0]] @y_shift_values = [[flame_frame, 0]] end # The shift channel is what determines how the tracking point moves. def write_shift_channel(name_without_prefix, values) @writer.channel(prefix(name_without_prefix)) do | c | c.extrapolation :constant c.value values[0][1] c.key_version 1 c.size values.length values.each_with_index do | (f, v), i | c.key(i) do | k | k.frame f k.value v k.interpolation :linear k.left_slope 2.4 k.right_slope 2.4 end end end end def prefix(tracker_channel) "tracker%d/%s" % [@counter, tracker_channel] end def write_header_with_number_of_trackers(number_of_trackers) @writer.stabilizer_file_version "5.0" @writer.creation_date(Time.now.strftime(DATETIME_FORMAT)) @writer.linebreak!(2) @writer.nb_trackers number_of_trackers @writer.selected 0 @writer.frame_width @width @writer.frame_height @height @writer.auto_key true @writer.motion_path true @writer.icons true @writer.auto_pan false # hate it! @writer.edit_mode 0 @writer.format 0 @writer.color_hash!("padding", 0, 100, 0) @writer.oversampling false @writer.opacity 50 @writer.zoom 3 @writer.field false @writer.backward false @writer.anim end def write_first_frame(x, y) write_track_channels write_track_width_and_height write_ref_width_and_height write_ref_channels(x, y) write_deltax_and_deltay_channels end def write_track_channels ctr_x, ctr_y = @width / 2, @height / 2 # track determines where the tracking box is, and should be in the center # of the image for Flame to compute all other shifts properly %w( track/x track/y).map(&method(:prefix)).zip([ctr_x, ctr_y]).each do | cname, default | @writer.channel(cname) do | c | c.extrapolation("constant") c.value(default.to_i) c.colour(COLOR) end end end # The size of the tracking area def write_track_width_and_height %w( track/width track/height ).map(&method(:prefix)).each do | channel_name | @writer.channel(channel_name) do | c | c.extrapolation :linear c.value 15 c.colour COLOR end end end # The size of the reference area def write_ref_width_and_height %w( ref/width ref/height).map(&method(:prefix)).each do | channel_name | @writer.channel(channel_name) do | c | c.extrapolation :linear c.value 10 c.colour COLOR end end end # The Ref channel contains the reference for the shift channel in absolute # coordinates, and is set as float. Since we do not "snap" the tracker in # the process it's enough for us to make one keyframe in the ref channels # at the same frame as the first shift keyframe def write_ref_channels(ref_x, ref_y) %w( ref/x ref/y).map(&method(:prefix)).zip([ref_x, ref_y]).each do | cname, default | @writer.channel(cname) do | c | c.extrapolation("constant") c.value(default) c.colour(COLOR) c.key_version 1 c.size 1 c.key(0) do | k | k.frame 1 k.value default k.interpolation :constant k.left_slope 2.4 k.right_slope 2.4 end end end end def write_deltax_and_deltay_channels # This is used for deltax and deltay (offset tracking). # We set it to zero and lock %w( ref/dx ref/dy).map(&method(:prefix)).each do | chan, v | @writer.channel(chan) do | c | c.extrapolation("constant") c.value 0 c.colour(COLOR) c.size 2 c.key_version 1 c.key(0) do | k | k.frame 0 k.value 0 k.value_lock true k.delete_lock true k.interpolation :constant end c.key(1) do | k | k.frame 1 k.value 0 k.value_lock true k.delete_lock true k.interpolation :constant end end # Chan block end end def write_offset_channels %w(offset/x offset/y).map(&method(:prefix)).each do | c | @writer.channel(c) do | chan | chan.extrapolation :constant chan.value 0 end end end end