guerilla-di/tracksperanto

View on GitHub
lib/export/flame_stabilizer.rb

Summary

Maintainability
B
6 hrs
Test Coverage
# 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