tracksperanto 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/.DS_Store +0 -0
  2. data/History.txt +11 -0
  3. data/Manifest.txt +64 -20
  4. data/README.txt +82 -30
  5. data/Rakefile +1 -0
  6. data/bin/tracksperanto +21 -14
  7. data/lib/export/base.rb +16 -6
  8. data/lib/export/match_mover.rb +40 -0
  9. data/lib/export/nuke_script.rb +78 -0
  10. data/lib/export/pftrack.rb +14 -14
  11. data/lib/export/shake_text.rb +7 -8
  12. data/lib/export/syntheyes.rb +9 -5
  13. data/lib/import/base.rb +45 -12
  14. data/lib/import/flame_stabilizer.rb +17 -32
  15. data/lib/import/match_mover.rb +65 -0
  16. data/lib/import/nuke_script.rb +96 -0
  17. data/lib/import/pftrack.rb +9 -2
  18. data/lib/import/shake_grammar/catcher.rb +56 -0
  19. data/lib/import/shake_grammar/lexer.rb +95 -0
  20. data/lib/import/shake_script.rb +189 -103
  21. data/lib/import/shake_text.rb +5 -3
  22. data/lib/import/syntheyes.rb +9 -2
  23. data/lib/middleware/base.rb +6 -1
  24. data/lib/middleware/golden.rb +7 -0
  25. data/lib/middleware/reformat.rb +10 -3
  26. data/lib/middleware/scaler.rb +14 -4
  27. data/lib/middleware/shift.rb +10 -0
  28. data/lib/middleware/slipper.rb +3 -0
  29. data/lib/pipeline/base.rb +111 -37
  30. data/lib/tracksperanto/block_init.rb +7 -0
  31. data/lib/tracksperanto/casts.rb +31 -0
  32. data/lib/tracksperanto/format_detector.rb +35 -0
  33. data/lib/tracksperanto/keyframe.rb +31 -0
  34. data/lib/tracksperanto/safety.rb +20 -0
  35. data/lib/tracksperanto/tracker.rb +38 -0
  36. data/lib/tracksperanto/zip_tuples.rb +20 -0
  37. data/lib/tracksperanto.rb +13 -100
  38. data/test/.DS_Store +0 -0
  39. data/test/export/.DS_Store +0 -0
  40. data/test/export/README_EXPORT_TESTS.txt +15 -0
  41. data/test/export/samples/ref_NukeScript.nk +25 -0
  42. data/test/export/samples/ref_NukeScript.nk.autosave +77 -0
  43. data/test/export/samples/ref_PFTrack.2dt +50 -0
  44. data/test/export/samples/ref_ShakeText.txt +48 -0
  45. data/test/export/samples/ref_Syntheyes.txt +42 -0
  46. data/test/export/samples/ref_matchmover.rz2 +52 -0
  47. data/test/export/test_match_mover_export.rb +16 -0
  48. data/test/export/test_mux.rb +23 -0
  49. data/test/export/test_nuke_export.rb +22 -0
  50. data/test/export/test_pftrack_export.rb +19 -0
  51. data/test/export/test_shake_export.rb +15 -0
  52. data/test/export/test_syntheyes_export.rb +15 -0
  53. data/test/helper.rb +85 -2
  54. data/test/import/.DS_Store +0 -0
  55. data/test/{samples → import/samples}/.DS_Store +0 -0
  56. data/test/{samples → import/samples}/flyover2DP_syntheyes.txt +0 -0
  57. data/test/import/samples/four_tracks_in_one_matchmove.shk +323 -0
  58. data/test/import/samples/four_tracks_in_one_stabilizer.shk +321 -0
  59. data/test/{samples → import/samples}/fromCombustion_fromMidClip_wSnap.stabilizer +0 -0
  60. data/test/{samples → import/samples}/hugeFlameSetup.stabilizer +0 -0
  61. data/test/import/samples/kipPointsMatchmover.rz2 +523 -0
  62. data/test/{samples → import/samples}/megaTrack.action.3dtrack.stabilizer +0 -0
  63. data/test/{samples → import/samples}/one_shake_tracker.txt +0 -0
  64. data/test/{samples → import/samples}/one_shake_tracker_from_first.txt +0 -0
  65. data/test/import/samples/one_tracker_with_break.nk +71 -0
  66. data/test/import/samples/one_tracker_with_break_in_grp.nk +91 -0
  67. data/test/{samples → import/samples}/shake_tracker_nodes.shk +0 -0
  68. data/test/{samples → import/samples}/shake_tracker_nodes_to_syntheyes.txt +0 -0
  69. data/test/{samples → import/samples}/sourcefile_pftrack.2dt +0 -0
  70. data/test/{samples → import/samples}/three_tracks_in_one_stabilizer.shk +0 -0
  71. data/test/{samples → import/samples}/two_shake_trackers.txt +0 -0
  72. data/test/{samples → import/samples}/two_tracks_in_one_tracker.shk +0 -0
  73. data/test/{test_flame_import.rb → import/test_flame_import.rb} +15 -9
  74. data/test/import/test_match_mover_import.rb +44 -0
  75. data/test/import/test_nuke_import.rb +63 -0
  76. data/test/{test_pftrack_import.rb → import/test_pftrack_import.rb} +10 -4
  77. data/test/import/test_shake_catcher.rb +72 -0
  78. data/test/import/test_shake_lexer.rb +95 -0
  79. data/test/import/test_shake_script_import.rb +75 -0
  80. data/test/{test_shake_text_import.rb → import/test_shake_text_import.rb} +10 -3
  81. data/test/{test_syntheyes_import.rb → import/test_syntheyes_import.rb} +8 -3
  82. data/test/middleware/test_golden_middleware.rb +32 -0
  83. data/test/middleware/test_reformat_middleware.rb +35 -0
  84. data/test/middleware/test_scaler_middleware.rb +51 -0
  85. data/test/middleware/test_shift_middleware.rb +26 -0
  86. data/test/middleware/test_slip_middleware.rb +31 -0
  87. data/test/pipeline/test_pipeline_base.rb +14 -0
  88. data/test/test_format_detector.rb +39 -0
  89. data/test/test_tracker.rb +48 -0
  90. data/tracksperanto.gemspec +9 -7
  91. metadata +101 -30
  92. data/lib/middleware/close.rb +0 -6
  93. data/test/test_shake_export.rb +0 -58
  94. data/test/test_shake_script_import.rb +0 -50
@@ -1,128 +1,214 @@
1
- require 'strscan'
1
+ require File.dirname(__FILE__) + "/shake_grammar/lexer"
2
+ require File.dirname(__FILE__) + "/shake_grammar/catcher"
2
3
 
3
4
  class Tracksperanto::Import::ShakeScript < Tracksperanto::Import::Base
4
5
 
5
- # Allow literal strings instead of Regexps
6
- class SloppyParser < StringScanner
7
- def skip_until(pattern)
8
- pattern.is_a?(String) ? super(/#{Regexp.escape(pattern)}/m) : super(pattern)
9
- end
10
-
11
- def scan_until(pattern)
12
- pattern.is_a?(String) ? super(/#{Regexp.escape(pattern)}/m) : super(pattern)
13
- end
6
+ def self.human_name
7
+ "Shake .shk script file"
14
8
  end
15
-
16
- class ValueAt
17
- attr_accessor :value, :frame
18
- def self.from_string(str)
19
- v = new
20
- val_s, frame_s = str.scan(/(.+)@(\d+)/).to_a.flatten
21
- v.value, v.frame = val_s.to_f, frame_s.to_i
22
- v
23
- end
9
+
10
+ def self.distinct_file_ext
11
+ ".shk"
24
12
  end
25
-
26
- class CurveParser < SloppyParser
27
- BLOCK_ENDS = /\d,|\)/
13
+
14
+ class Traxtractor < Tracksperanto::ShakeGrammar::Catcher
15
+ include Tracksperanto::ZipTuples
28
16
 
29
- attr_reader :values
30
- def initialize(with_curve_arg)
31
- @values = []
32
-
33
- super(with_curve_arg.to_s)
34
-
35
- # Skip the interpolation name, we assume it to be linear anyway
36
- skip_until '('
37
-
38
- # Skip the first defining parameter whatever that might be
39
- skip_until ','
40
-
41
- loop do
42
- break unless (value_at = scan_until(BLOCK_ENDS))
43
- # Grab the value
44
- val = ValueAt.from_string(value_at)
45
-
46
- @values << val
17
+ class << self
18
+ attr_accessor :accumulator
19
+ attr_accessor :progress_block
20
+ end
21
+
22
+ self.accumulator = []
23
+
24
+ # For Linear() curve calls. If someone selected JSpline or Hermite it's his problem.
25
+ # We put the frame number at the beginning since it works witih oru tuple zipper
26
+ def linear(first_arg, *keyframes)
27
+ report_progress("Translating Linear animation")
28
+ keyframes.map do | kf |
29
+ [kf[1][1], kf[0][1]]
47
30
  end
48
31
  end
49
-
50
- end
51
-
52
- class TrackerParser < SloppyParser
53
- attr_reader :x_curve, :y_curve, :c_curve, :name
54
32
 
55
- def initialize(with_tracker_args)
56
- super(with_tracker_args)
57
-
58
- # Name me!
59
- @name = scan_until(/(\w+) /).strip
60
-
61
- # All the tracker arguments
62
- 17.times { skip_until ',' } # input data
33
+ # image Tracker(
34
+ # image In,
35
+ # const char * trackRange,
36
+ # const char * subPixelRes,
37
+ # const char * matchSpace,
38
+ # float referenceTolerance,
39
+ # const char * referenceBehavior,
40
+ # float failureTolerance,
41
+ # const char * failureBehavior,
42
+ # int limitProcessing,
43
+ # float referencFrame
44
+ # ...
45
+ # );
46
+ def tracker(input, trackRange, subPixelRes, matchSpace,
47
+ referenceTolerance, referenceBehavior, failureTolerance, failureBehavior, limitProcessing, referencFrame, s1, s2,
48
+ s3, s4, s5, s6, *trackers)
63
49
 
64
- # Grab the curves
65
- @x_curve, @y_curve, @c_curve = (0..2).map{ CurveParser.new(scan_until('),')).values }
50
+ report_progress("Parsing Tracker node")
51
+ collect_trackers_from(get_variable_name, trackers)
52
+ true
53
+ end
54
+
55
+ # stabilize {
56
+ # image In,
57
+ # int applyTransform,
58
+ # int inverseTransform
59
+ # const char * trackType,
60
+ # float track1X,
61
+ # float track1Y,
62
+ # int stabilizeX,
63
+ # int stabilizeY,
64
+ # float track2X,
65
+ # float track2Y,
66
+ # int matchScale,
67
+ # int matchRotation,
68
+ # float track3X,
69
+ # float track3Y,
70
+ # float track4X,
71
+ # float track4Y,
72
+ # const char * xFilter,
73
+ # const char * yFilter,
74
+ # const char * transformationOrder,
75
+ # float motionBlur,
76
+ # float shutterTiming,
77
+ # float shutterOffset,
78
+ # float referenceFrame,
79
+ # float aspectRatio,
80
+ # ...
81
+ # };
82
+ def stabilize(imageIn, applyTransform, inverseTransform, trackType,
83
+ track1X, track1Y,
84
+ stabilizeX, stabilizeY,
85
+ track2X, track2Y,
86
+ matchScale,
87
+ matchRotation,
88
+ track3X, track3Y,
89
+ track4X, track4Y,
90
+ *useless_args)
66
91
 
67
- # if the next argument is an integer, we reached the end of the tracks. If not - make a nested one.
92
+ report_progress("Parsing Stabilize node")
93
+ node_name = get_variable_name
94
+ collect_stabilizer_tracker("#{node_name}_track1", track1X, track1Y)
95
+ collect_stabilizer_tracker("#{node_name}_track2", track2X, track2Y)
96
+ collect_stabilizer_tracker("#{node_name}_track3", track3X, track3Y)
97
+ collect_stabilizer_tracker("#{node_name}_track4", track4X, track4Y)
68
98
  end
69
99
 
70
- def curves
71
- [@x_curve, @y_curve, @c_curve]
100
+ # image = MatchMove(
101
+ # Background,
102
+ # Foreground,
103
+ # applyTransform,
104
+ # "trackType",
105
+ # track1X,
106
+ # track1Y,
107
+ # matchX,
108
+ # matchY,
109
+ # track2X,
110
+ # track2Y,
111
+ # scale,
112
+ # rotation,
113
+ # track3X,
114
+ # track3Y,
115
+ # track4X,
116
+ # track4Y,
117
+ # x1,
118
+ # y1,
119
+ # x2,
120
+ # y2,
121
+ # x3,
122
+ # y3,
123
+ # x4,
124
+ # y4,
125
+ # "xFilter",
126
+ # "yFilter",
127
+ # motionBlur,
128
+ # shutterTiming,
129
+ # shutterOffset,
130
+ # referenceFrame,
131
+ # "compositeType",
132
+ # clipMode,
133
+ # "trackRange",
134
+ # "subPixelRes",
135
+ # "matchSpace",
136
+ # float referenceTolerance,
137
+ # "referenceBehavior",
138
+ # float failureTolerance,
139
+ # "failureBehavior",
140
+ # int limitProcessing,
141
+ # ...
142
+ # );
143
+ def matchmove(bgImage, fgImage, applyTransform,
144
+ trackType,
145
+ track1X,
146
+ track1Y,
147
+ matchX,
148
+ matchY,
149
+ track2X,
150
+ track2Y,
151
+ scale,
152
+ rotation,
153
+ track3X,
154
+ track3Y,
155
+ track4X,
156
+ track4Y, *others)
157
+
158
+ report_progress("Parsing MatchMove node")
159
+ node_name = get_variable_name
160
+ collect_stabilizer_tracker("#{node_name}_track1", track1X, track1Y)
161
+ collect_stabilizer_tracker("#{node_name}_track2", track2X, track2Y)
162
+ collect_stabilizer_tracker("#{node_name}_track3", track3X, track3Y)
163
+ collect_stabilizer_tracker("#{node_name}_track4", track4X, track4Y)
164
+
72
165
  end
73
- end
74
-
75
- class TrackParser < SloppyParser
76
- def initialize(with_tracker_block)
77
- # scan until the first " - name of the track
78
- skip_until ','
79
- # scan values
80
- # discard the 8 box determinators
81
- 7.times { skip_until ',' }
82
- # discard the box animation
83
- 2.times { skip_until ',' }
166
+
167
+ private
168
+
169
+ def report_progress(with_message)
170
+ self.class.progress_block.call(with_message) if self.class.progress_block
84
171
  end
85
- end
86
-
87
- TRACKER_PATTERN = /((\w+) = Tracker\(([^;]+))/m
88
-
89
- def parse(sript_file_content)
90
172
 
91
- trackers = []
173
+ def collect_trackers_from(name, array)
174
+ parameters_per_node = 16
175
+ nb_trackers = array.length / parameters_per_node
176
+ nb_trackers.times do | idx |
177
+ from_index, to_index = (idx * parameters_per_node), (idx+1) * parameters_per_node
178
+ tracker_args = array[from_index...to_index]
179
+ tracker_args[0] = "#{name}_#{tracker_args[0]}"
180
+ collect_tracker(*tracker_args)
181
+ end
182
+ end
92
183
 
93
- sript_file_content.scan(TRACKER_PATTERN).each_with_index do | tracker_text_block, idx |
94
-
95
- parser = TrackerParser.new(tracker_text_block.to_s)
96
-
97
- tracker = Tracksperanto::Tracker.new{|t| t.name = parser.name }
184
+ def collect_stabilizer_tracker(name, x_curve, y_curve)
185
+ return if (x_curve == :unknown || y_curve == :unknown)
98
186
 
99
- report_progress("Reading tracker #{tracker.name}")
187
+ keyframes = zip_curve_tuples(x_curve, y_curve).map do | (frame, x, y) |
188
+ Tracksperanto::Keyframe.new(:frame => frame - 1, :abs_x => x, :abs_y => y)
189
+ end
100
190
 
101
- x_keyframes, y_keyframes, residual_keyframes = TrackerParser.new(tracker_text_block.to_s).curves
102
- x_keyframes.each_with_index do | value_at, kf_index |
103
-
104
- # Find the Y keyframe with the same frame
105
- matching_y = y_keyframes.find{|f| f.frame == value_at.frame }
106
-
107
- # Find the correlation keyframe with the same frame
108
- matching_residual = residual_keyframes.find{|f| f.frame == value_at.frame }
109
-
110
- # Skip frame if only one keyframe is present
111
- if !matching_y
112
- STDERR.puts "Cannot find matching Y for frame #{value_at.frame} in tracker #{parser.name}, skipping keyframe"
113
- next
114
- end
115
-
116
- tracker.keyframes << Tracksperanto::Keyframe.new do |k|
117
- k.frame = (value_at.frame - 1)
118
- k.abs_x = value_at.value
119
- k.abs_y = matching_y.value
120
- k.residual = 1 - (matching_residual.value rescue 1.0)
121
- end
191
+ t = Tracksperanto::Tracker.new(:name => name, :keyframes => keyframes )
192
+ self.class.accumulator.push(t)
193
+ end
194
+
195
+ def collect_tracker(name, x_curve, y_curve, corr_curve, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12)
196
+ report_progress("Scavenging tracker #{name}")
197
+ keyframes = zip_curve_tuples(x_curve, y_curve, corr_curve).map do | (frame, x, y, corr) |
198
+ Tracksperanto::Keyframe.new(:frame => frame - 1, :abs_x => x, :abs_y => y, :residual => (1 - corr))
122
199
  end
123
200
 
124
- trackers << tracker
201
+ t = Tracksperanto::Tracker.new(:name => name, :keyframes => keyframes )
202
+ self.class.accumulator.push(t)
125
203
  end
204
+ end
205
+
206
+ def parse(script_io)
207
+ trackers = []
208
+
209
+ Traxtractor.accumulator = trackers
210
+ Traxtractor.progress_block = lambda{|msg| report_progress(msg) }
211
+ Traxtractor.new(script_io)
126
212
 
127
213
  trackers
128
214
  end
@@ -1,10 +1,12 @@
1
- require 'stringio'
2
1
 
3
2
  class Tracksperanto::Import::ShakeText < Tracksperanto::Import::Base
4
3
 
5
- def parse(track_file_content)
4
+ def self.human_name
5
+ "Shake .txt tracker file"
6
+ end
7
+
8
+ def parse(io)
6
9
  trackers = []
7
- io = StringIO.new(track_file_content)
8
10
  until io.eof?
9
11
  line = io.gets
10
12
  if line =~ /TrackName (.+)/
@@ -1,12 +1,18 @@
1
1
  class Tracksperanto::Import::Syntheyes < Tracksperanto::Import::Base
2
- def parse(file_content)
2
+
3
+ def self.human_name
4
+ "Syntheyes tracker export (UV) file"
5
+ end
6
+
7
+ def parse(io)
3
8
  trackers = []
4
- file_content.split("\n").each do | line |
9
+ io.each_line do | line |
5
10
  name, frame, x, y, corr = line.split
6
11
 
7
12
  # Do we already have this tracker?
8
13
  t = trackers.find {|e| e.name == name}
9
14
  if !t
15
+ report_progress("Allocating tracker #{name}")
10
16
  t = Tracksperanto::Tracker.new{|t| t.name = name }
11
17
  trackers << t
12
18
  end
@@ -18,6 +24,7 @@ class Tracksperanto::Import::Syntheyes < Tracksperanto::Import::Base
18
24
  e.abs_y = height - convert_from_uv(height, y) # Convert TL to BL
19
25
  e.residual = corr
20
26
  end
27
+ report_progress("Adding keyframe #{frame} to #{name}")
21
28
  end
22
29
 
23
30
  trackers
@@ -1,8 +1,13 @@
1
+ # The base middleware class works just like a Tracksperanto::Export::Base, but it only wraps another exporting object and does not get registered on it's own
2
+ # as an export format. Middleware can be used to massage the tracks being exported in various interesting ways - like moving the coordinates, clipping the keyframes,
3
+ # scaling the whole export or even reversing the trackers to go backwards
1
4
  class Tracksperanto::Middleware::Base
2
5
  include Tracksperanto::Casts
6
+ include Tracksperanto::BlockInit
3
7
 
4
- def initialize(exporter)
8
+ def initialize(exporter, *args_for_block_init)
5
9
  @exporter = exporter
10
+ super(*args_for_block_init)
6
11
  end
7
12
 
8
13
  # Called on export start
@@ -1,6 +1,13 @@
1
+ # This middleware marks all trackers as being 100% accurate
1
2
  class Tracksperanto::Middleware::Golden < Tracksperanto::Middleware::Base
2
3
  attr_accessor :enabled
4
+
5
+ def enabled
6
+ @enabled || false
7
+ end
8
+
3
9
  def export_point(frame, float_x, float_y, float_residual)
4
10
  super(frame, float_x, float_y, (enabled ? 0.0 : float_residual))
5
11
  end
12
+
6
13
  end
@@ -1,8 +1,14 @@
1
1
  require File.dirname(__FILE__) + '/scaler'
2
2
 
3
- # Reformats (scales) the track setup to a specific pixel resolution
3
+ # This middleware reformats (scales) the track setup to a specific pixel resolution. Very useful for
4
+ # applying proxy tracks to full-res images
4
5
  class Tracksperanto::Middleware::Reformat < Tracksperanto::Middleware::Scaler
6
+
7
+ # To which format we have to scale
5
8
  attr_accessor :width, :height
9
+ cast_to_int :width, :height
10
+
11
+ private :x_factor=, :y_factor=
6
12
 
7
13
  # Called on export start
8
14
  def start_export( img_width, img_height)
@@ -10,7 +16,8 @@ class Tracksperanto::Middleware::Reformat < Tracksperanto::Middleware::Scaler
10
16
  @height ||= img_height
11
17
 
12
18
  self.x_factor, self.y_factor = (@width / img_width.to_f), (@height / img_height.to_f)
13
-
14
- super(@width, @height)
19
+ set_residual_factor
20
+ # Do not call super since it scales by itself :-)
21
+ @exporter.start_export(@width, @height)
15
22
  end
16
23
  end
@@ -1,13 +1,14 @@
1
+ # Scales the comp being exported by a specific factor, together with the tracker keyframes
1
2
  class Tracksperanto::Middleware::Scaler < Tracksperanto::Middleware::Base
2
3
  DEFAULT_FACTOR = 1
3
4
 
4
5
  attr_accessor :x_factor, :y_factor
6
+ cast_to_float :x_factor, :y_factor
5
7
 
6
8
  # Called on export start
7
9
  def start_export( img_width, img_height)
8
- # Compute the average factor
9
- @residual_factor = (x_factor + y_factor) / 2
10
- super( (img_width * x_factor), (img_height * y_factor))
10
+ set_residual_factor
11
+ super( (img_width * x_factor).to_i, (img_height * y_factor).to_i)
11
12
  end
12
13
 
13
14
  def y_factor
@@ -19,6 +20,15 @@ class Tracksperanto::Middleware::Scaler < Tracksperanto::Middleware::Base
19
20
  end
20
21
 
21
22
  def export_point(frame, float_x, float_y, float_residual)
22
- super(frame, (float_x * x_factor), (float_y * y_factor), (float_residual * @residual_factor))
23
+ super(frame,
24
+ (float_x * x_factor),
25
+ (float_y * y_factor),
26
+ (float_residual * @residual_factor)
27
+ )
23
28
  end
29
+
30
+ private
31
+ def set_residual_factor
32
+ @residual_factor = Math.sqrt((x_factor ** 2) + (y_factor ** 2))
33
+ end
24
34
  end
@@ -0,0 +1,10 @@
1
+ # This middleware moves the keyframs by a preset number of pixels
2
+ class Tracksperanto::Middleware::Shift < Tracksperanto::Middleware::Base
3
+ attr_accessor :x_shift, :y_shift
4
+ cast_to_float :x_shift, :y_shift
5
+
6
+ def export_point(frame, float_x, float_y, float_residual)
7
+ super(frame, float_x + (@x_shift || 0), float_y + (@y_shift || 0), float_residual)
8
+ end
9
+
10
+ end
@@ -1,6 +1,9 @@
1
+ # Slips the keyframe positions by a specific integer amount of frames, positive values slip forward (later in time). Useful if you just edited some stuff onto
2
+ # the beginning if your sequence and need to extend your tracks.
1
3
  class Tracksperanto::Middleware::Slipper < Tracksperanto::Middleware::Base
2
4
  DEFAULT_SLIP = 0
3
5
  attr_accessor :slip
6
+ cast_to_int :slip
4
7
 
5
8
  def slip
6
9
  @slip.to_i || DEFAULT_SLIP
data/lib/pipeline/base.rb CHANGED
@@ -1,62 +1,113 @@
1
- # This is currently a bit of a mess.
2
- module Tracksperanto::Pipeline
3
- class Base
4
- attr_accessor :converted_points
5
- attr_accessor :converted_keyframes
1
+ # The base pipeline is the whole process of track conversion from start to finish. The pipeline object organizes the import formats, scans them,
2
+ # applies the default middlewares and yields them for processing. Here's how a calling sequence for a pipeline looks like:
3
+ #
4
+ # pipe = Tracksperanto::Pipeline::Base.new
5
+ # pipe.progress_block = lambda{|percent, msg| puts("#{msg}..#{percent.to_i}%") }
6
+ #
7
+ # pipe.run(input_file, width, height, reader_klass) do | scaler, slipper, golden, reformat |
8
+ # golden.enabled = false
9
+ # reformat.width = 1024
10
+ # reformat.width = 576
11
+ # end
12
+ #
13
+ # The pipeline will also automatically allocate output files with the right extensions at the same place where the original file resides,
14
+ # and setup outputs for all supported export formats. The pipeline will also report progress (with percent) using the passed progress block
15
+ class Tracksperanto::Pipeline::Base
16
+
17
+ # How many points have been converted. In general, the pipeline does not preserve the parsed tracker objects
18
+ # after they have been exported
19
+ attr_reader :converted_points
20
+
21
+ # How many keyframes have been converted
22
+ attr_reader :converted_keyframes
23
+
24
+ # A block acepting percent and message vars can be assigned here.
25
+ # When it's assigned, the pipeline will pass the status reports
26
+ # of all the importers and exporters to the block, together with
27
+ # percent complete
6
28
  attr_accessor :progress_block
7
29
 
8
- def run(from_input_file_path, pix_w, pix_h, parser_class)
9
- # Read the input file
10
- read_data = File.read(from_input_file_path)
30
+ # Assign an array of exporters to use them instead of the standard ones
31
+ attr_accessor :exporters
32
+
33
+ DEFAULT_OPTIONS = {:pix_w => 720, :pix_h => 576, :parser => Tracksperanto::Import::ShakeScript }
34
+
35
+ # Runs the whole pipeline. Accepts the following options
36
+ # * pix_w - The comp width, for the case that the format does not support auto size
37
+ # * pix_h - The comp height, for the case that the format does not support auto size
38
+ # * parser - The parser class, for the case that the format does not support auto size
39
+
40
+ def run(from_input_file_path, passed_options = {})
41
+ pix_w, pix_h, parser_class = detect_importer_or_use_options(from_input_file_path, DEFAULT_OPTIONS.merge(passed_options))
42
+
43
+ # Reset stats
44
+ @converted_keyframes, @converted_points = 0, 0
45
+
46
+ # Grab the input
47
+ read_data = File.open(from_input_file_path)
11
48
 
12
49
  # Assign the parser
13
- parser = create_parser(parser_class, pix_w, pix_h)
50
+ importer = parser_class.new(:width => pix_w, :height => pix_h)
14
51
 
15
52
  # Setup a multiplexer
16
53
  mux = setup_outputs_for(from_input_file_path)
17
54
 
18
- # Setup middlewares - skip for now
19
- scaler = Tracksperanto::Middleware::Scaler.new(mux)
20
- slipper = Tracksperanto::Middleware::Slipper.new(scaler)
21
- golden = Tracksperanto::Middleware::Golden.new(slipper)
22
- reformat = Tracksperanto::Middleware::Reformat.new(golden)
55
+ # Setup middlewares
56
+ middlewares = setup_middleware_chain_with(mux)
23
57
 
24
58
  # Yield middlewares to the block
25
- yield(scaler, slipper, golden, reformat) if block_given?
59
+ yield(*middlewares) if block_given?
26
60
 
27
- @converted_points, @converted_keyframes = run_export(read_data, parser, reformat) do | p, m |
61
+ @converted_points, @converted_keyframes = run_export(read_data, importer, middlewares[-1]) do | p, m |
28
62
  @progress_block.call(p, m) if @progress_block
29
63
  end
30
64
  end
31
65
 
66
+ def detect_importer_or_use_options(path, opts)
67
+ d = Tracksperanto::FormatDetector.new(path)
68
+ if d.match? && d.auto_size?
69
+ return [1, 1, d.importer_klass]
70
+ elsif d.match?
71
+ raise "Width and height must be provided for a #{d.importer_klass}" unless (opts[:pix_w] && opts[:pix_h])
72
+ opts[:parser] = d.importer_klass
73
+ else
74
+ raise "Cannot autodetect the file format - please specify the importer explicitly" unless opts[:parser]
75
+ raise "Width and height must be provided for this importer" unless (opts[:pix_w] && opts[:pix_h])
76
+ end
77
+
78
+ [opts[:pix_w], opts[:pix_h], opts[:parser]]
79
+ end
80
+
32
81
  # Runs the export and returns the number of points and keyframes processed.
33
82
  # If a block is passed, the block will receive the percent complete and the last
34
83
  # status message that you can pass back to the UI
35
- # # :yields: percent_complete, status_message
36
- def run_export(tracker_data_blob, parser, processor)
84
+ def run_export(tracker_data_io, parser, processor)
85
+ @ios << tracker_data_io
86
+
37
87
  points, keyframes, percent_complete = 0, 0, 0.0
38
88
 
39
- yield(percent_complete, "Starting the parse routine") if block_given?
89
+ yield(percent_complete, "Starting the parser") if block_given?
40
90
  parser.progress_block = lambda do | message |
41
91
  yield(percent_complete, message) if block_given?
42
92
  end
43
93
 
44
- trackers = parser.parse(tracker_data_blob)
94
+ trackers = parser.parse(tracker_data_io)
45
95
 
46
- yield(percent_complete = 20.0, "Starting export for #{trackers.length} trackers") if block_given?
96
+ validate_trackers!(trackers)
47
97
 
48
- percent_per_tracker = 80.0 / trackers.length
98
+ yield(percent_complete = 50.0, "Parsing complete, starting export for #{trackers.length} trackers") if block_given?
49
99
 
50
- # Use the width and height provided by the parser itself
51
- processor.start_export(parser.width, parser.height)
100
+ percent_per_tracker = (100.0 - percent_complete) / trackers.length
52
101
 
53
102
  yield(percent_complete, "Starting export") if block_given?
54
103
 
104
+ # Use the width and height provided by the parser itself
105
+ processor.start_export(parser.width, parser.height)
55
106
  trackers.each do | t |
56
107
  kf_weight = percent_per_tracker / t.keyframes.length
57
108
  points += 1
58
109
  processor.start_tracker_segment(t.name)
59
- t.keyframes.each do | kf |
110
+ t.each do | kf |
60
111
  keyframes += 1
61
112
  processor.export_point(kf.frame, kf.abs_x, kf.abs_y, kf.residual)
62
113
  yield(percent_complete += kf_weight, "Writing keyframe") if block_given?
@@ -68,24 +119,47 @@ class Base
68
119
  yield(100.0, "Wrote #{points} points and #{keyframes} keyframes") if block_given?
69
120
 
70
121
  [points, keyframes]
122
+ ensure
123
+ @ios.reject!{|e| e.close unless (!e.respond_to?(:closed?) || e.closed?) } if @ios
71
124
  end
72
125
 
73
- def create_parser(parser_class, w, h)
74
- p = parser_class.new
75
- p.width, p.height = w, h
76
- p
77
- end
78
-
126
+ # Setup output files and return a single output
127
+ # that replays to all of them
79
128
  def setup_outputs_for(input_file_path)
80
129
  file_name = File.basename(input_file_path).gsub(/\.([^\.]+)$/, '')
130
+ export_klasses = exporters || Tracksperanto.exporters
81
131
  Tracksperanto::Export::Mux.new(
82
- Tracksperanto.exporters.map do | exporter_class |
83
- export_name = "%s_%s" % [file_name, exporter_class.desc_and_extension]
84
- export_path = File.dirname(input_file_path) + '/' + export_name
85
- exporter = exporter_class.new(File.open(export_path, 'w'))
86
- Tracksperanto::Middleware::Close.new(exporter)
132
+ export_klasses.map do | exporter_class |
133
+ export_name = [file_name, exporter_class.desc_and_extension].join("_")
134
+ export_path = File.join(File.dirname(input_file_path), export_name)
135
+ exporter = exporter_class.new(open_owned_export_file(export_path))
87
136
  end
88
137
  )
89
138
  end
90
- end
139
+
140
+ # Setup and return the output multiplexor wrapped in all necessary middlewares.
141
+ # Middlewares must be returned in an array to be passed to the configuration block
142
+ # afterwards
143
+ def setup_middleware_chain_with(output)
144
+ scaler = Tracksperanto::Middleware::Scaler.new(output)
145
+ slipper = Tracksperanto::Middleware::Slipper.new(scaler)
146
+ golden = Tracksperanto::Middleware::Golden.new(slipper)
147
+ reformat = Tracksperanto::Middleware::Reformat.new(golden)
148
+ shift = Tracksperanto::Middleware::Shift.new(reformat)
149
+ [scaler, slipper, golden, reformat, shift]
150
+ end
151
+
152
+ # Open the file for writing and register it to be closed automatically
153
+ def open_owned_export_file(path_to_file)
154
+ @ios ||= []
155
+ handle = File.open(path_to_file, "w")
156
+ @ios << handle
157
+ handle
158
+ end
159
+
160
+ # Check that the trackers made by the parser are A-OK
161
+ def validate_trackers!(trackers)
162
+ raise "Could not recover any trackers from this file. Wrong import format maybe?" if trackers.empty?
163
+ trackers.reject!{|t| t.empty? }
164
+ end
91
165
  end