lib/pipeline/base.rb
require 'progressive_io'
require 'obuf'
module Tracksperanto::Pipeline
class EmptySourceFileError < RuntimeError
def message; "This is an empty source file"; end
end
class UnknownFormatError < RuntimeError
def message; "Unknown input format"; end
end
class DimensionsRequiredError < RuntimeError
def message; "Width and height must be provided for this importer"; end
end
class NoTrackersRecoveredError < RuntimeError
def initialize(importer)
if importer.class.known_snags
@snags = "Also note that this particular format (%s) has the following snags: %s" % [importer.const_name, importer.class.known_snags]
end
end
def message;
[
"Could not recover any non-empty trackers from this file.",
"Wrong import format maybe?",
@snags
].join("\n")
end
end
# The base pipeline is the whole process of track conversion from start to finish.
# The pipeline object organizes the import formats, scans them,
# applies the tools. Here's how a calling sequence for a pipeline looks like:
#
# pipe = Tracksperanto::Pipeline::Base.new
# pipe.tool_tuples = ["Shift", {:x => 10}]
# pipe.progress_block = lambda{|percent, msg| puts("#{msg}..#{percent.to_i}%") }
# pipe.run("/tmp/shakescript.shk", :width => 720, :height => 576)
#
# The pipeline will also automatically allocate output files with the right extensions
# at the same place where the original file resides,
# and setup outputs for all supported export formats.
class Base
EXTENSION = /\.([^\.]+)$/ #:nodoc:
PERMITTED_OPTIONS = [:importer, :width, :height]
include Tracksperanto::BlockInit
# How many points have been converted
attr_reader :converted_points
# How many keyframes have been converted
attr_reader :converted_keyframes
# A block acepting percent and message vars can be assigned here.
# When it's assigned, the pipeline will pass the status reports
# of all the importers and exporters to the block, together with
# percent complete
attr_accessor :progress_block
# Assign an array of exporter classes to use them instead of the default "All"
attr_accessor :exporters
# Contains arrays of the form ["MiddewareName", {:param => value}]
attr_accessor :tool_tuples
def initialize(*any)
super
@ios = []
end
# Will scan the tool_tuples attribute and create a processing chain.
# Tools will be instantiated and wrap each other, starting with the first one
def wrap_output_with_tools(output)
return output unless (tool_tuples && tool_tuples.any?)
tool_tuples.reverse.inject(output) do | wrapped, (tool_name, options) |
Tracksperanto.get_tool(tool_name).new(wrapped, options || {})
end
end
# Runs the whole pipeline. Accepts the following options
# * width - The comp width, for the case that the format does not support auto size
# * height - The comp height, for the case that the format does not support auto size
# * parser - The parser class, for the case that it can't be autodetected from the file name
# Returns the number of trackers and the number of keyframes processed during the run
def run(from_input_file_path, passed_options = {})
# Prevent formats that we do not support
Tracksperanto::Blacklist.raise_if_format_unsupported(from_input_file_path)
# Check for empty files
raise EmptySourceFileError if File.stat(from_input_file_path).size.zero?
# Reset stats
@converted_keyframes, @converted_points = 0, 0
# Assign the parser
importer = initialize_importer_with_path_and_options(from_input_file_path, passed_options)
# Open the file
read_data = File.open(from_input_file_path, "rb")
# Setup a multiplexer
mux = setup_outputs_for(from_input_file_path)
# Wrap it into a module that will prevent us from exporting invalid trackers
lint = Tracksperanto::Tool::Lint.new(mux)
# Setup tools
endpoint = wrap_output_with_tools(lint)
@converted_points, @converted_keyframes = run_export(read_data, importer, endpoint)
end
def report_progress(percent_complete, message)
int_pct = percent_complete.to_f.floor # Prevent float overflow above 100 percent
@progress_block.call(int_pct, message) if @progress_block
end
def initialize_importer_with_path_and_options(from_input_file_path, options)
d = Tracksperanto::FormatDetector.new(from_input_file_path)
if options[:importer]
imp = Tracksperanto.get_importer(options[:importer])
require_dimensions_in!(options) unless imp.autodetects_size?
imp.new(:width => options[:width], :height => options[:height])
elsif d.match? && d.auto_size?
d.importer_klass.new
elsif d.match?
require_dimensions_in!(options)
d.importer_klass.new(:width => options[:width], :height => options[:height])
else
raise UnknownFormatError
end
end
def require_dimensions_in!(opts)
raise DimensionsRequiredError unless (opts[:width] && opts[:height])
end
# Runs the export and returns the number of points and keyframes processed.
# If a block is passed, the block will receive the percent complete and the last
# status message that you can pass back to the UI
def run_export(tracker_data_io, importer, exporter)
points, keyframes, percent_complete = 0, 0, 0.0
last_reported_percentage = 0.0
report_progress(percent_complete, "Starting the parser")
progress_lambda = lambda do | m |
last_reported_percentage = percent_complete
report_progress(percent_complete, m)
end
# Report progress from the parser
importer.progress_block = progress_lambda
# Wrap the input in a progressive IO, setup a lambda that will spy on the reader and
# update the percentage. We will only broadcast messages that come from the parser
# though (complementing it with a percentage)
io_with_progress = ProgressiveIO.new(tracker_data_io) do | offset, of_total |
percent_complete = (50.0 / of_total) * offset
# Some importers do not signal where they are and do not send nice reports. The way we can help that in the interim
# would be just to indicate where we are in the input, but outside of the exporter. We do not want to flood
# the logs though so what we WILL do instead is report some progress going on every 2-3 percent
progress_lambda.call("Parsing the file") if (percent_complete - last_reported_percentage) > 3
end
@ios.push(io_with_progress)
importer.io = io_with_progress
obuf = Obuf.new(Tracksperanto::YieldNonEmpty.new(importer))
report_progress(percent_complete = 50.0, "Validating #{obuf.size} imported trackers")
raise NoTrackersRecoveredError.new(importer) if obuf.size.zero?
report_progress(percent_complete, "Starting export")
percent_per_tracker = (100.0 - percent_complete) / obuf.size
# Use the width and height provided by the parser itself
exporter.start_export(importer.width, importer.height)
# Now send each tracker through the tool chain
obuf.each_with_index do | t, tracker_idx |
kf_weight = percent_per_tracker / t.keyframes.length
points += 1
exporter.start_tracker_segment(t.name)
t.each_with_index do | kf, idx |
keyframes += 1
exporter.export_point(kf.frame, kf.abs_x, kf.abs_y, kf.residual)
report_progress(
percent_complete += kf_weight,
"Writing keyframe #{idx+1} of #{t.name.inspect}, #{obuf.size - tracker_idx} trackers to go"
)
end
exporter.end_tracker_segment
end
exporter.end_export
report_progress(100.0, "Wrote #{points} points and #{keyframes} keyframes")
obuf.clear
@ios.map!{|e| e.close! rescue e.close }
@ios.clear
return [points, keyframes]
end
# Setup output files and return a single output
# that replays to all of them
def setup_outputs_for(input_file_path)
file_name_without_extension = File.basename(input_file_path, '.*')
outputs = (exporters || Tracksperanto.exporters).map do | exporter_class |
export_name = [file_name_without_extension, exporter_class.desc_and_extension].join("_")
export_path = File.join(File.dirname(input_file_path), export_name)
exporter_class.new(open_owned_export_file(export_path))
end
Tracksperanto::Export::Mux.new(outputs)
end
# Open the file for writing and register it to be closed automatically
def open_owned_export_file(path_to_file)
@ios.push(File.open(path_to_file, "wb"))[-1]
end
end
end