panomosity 0.1.32 → 0.1.37

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50e125a826a4accf978aa182be59f36de59c7697254b3eabd27b6fb831fb319d
4
- data.tar.gz: 4b53136c72ddba3fd1515cd5b9aea76fa747abfff344ad9e485ad2026e407209
3
+ metadata.gz: 55fb5d3e10b18279691fb06a4cc567ca0a1bc01098d23d1247a9469e75ffc5e4
4
+ data.tar.gz: f5088d9459ecbeaa7f892d14f9a9deb12c33fd5017dd11625555e29d9fd95817
5
5
  SHA512:
6
- metadata.gz: 21a995367c4d62e67ccbdda9a18587cd7ddefc3399e585307bb8b47c7390276074889f8cccbc267c062eed3a46052dc55796fe0280ad9e27635642a758f0de73
7
- data.tar.gz: da2f34870a506e6b32d4a2be111b614fe2056b049df905d9c5738efa13ffb64c0f45fe02ac4b86f042ee8c692b7610e032ef8f0e0e94ddc835bc925226e79728
6
+ metadata.gz: fe93c2b8c9acfe777523d9533ca0391a7e63964a4a7facb153e7f7f8fb34ccf8616af7b998965c724f4779bc02d94e73356ac8f3f0208b604bbac1e1e0920c8e
7
+ data.tar.gz: 1e69b3b9c79df369d31330188ef7ab7c3d10e6aa9d2e625cbce1fe4e3d327b27ea1d9c5df7df03d27b2cbdc4fd17a2b84ce4832efd7ef04f61ac4fbaa38b6d73
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- panomosity (0.1.32)
4
+ panomosity (0.1.37)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -38,4 +38,4 @@ DEPENDENCIES
38
38
  rspec (~> 3.0)
39
39
 
40
40
  BUNDLED WITH
41
- 1.16.1
41
+ 1.17.3
@@ -1,5 +1,7 @@
1
1
  require 'panomosity/control_point'
2
+ require 'panomosity/generalized_neighborhood'
2
3
  require 'panomosity/image'
4
+ require 'panomosity/measure'
3
5
  require 'panomosity/neighborhood'
4
6
  require 'panomosity/neighborhood_group'
5
7
  require 'panomosity/optimisation_variable'
@@ -13,6 +15,7 @@ require 'panomosity/version'
13
15
  require 'pathname'
14
16
  require 'fileutils'
15
17
  require 'optparse'
18
+ require 'json'
16
19
 
17
20
  module Panomosity
18
21
  def self.parse(arguments)
@@ -70,6 +73,42 @@ module Panomosity
70
73
  options[:report_type] = type
71
74
  end
72
75
 
76
+ parser.on('--darwin', 'Sets a flag to indicate the operating system') do |type|
77
+ options[:darwin] = type
78
+ end
79
+
80
+ parser.on('--version', 'Show the installed version') do
81
+ puts VERSION
82
+ exit
83
+ end
84
+
85
+ parser.on('--regional-distance-similarities-count COUNT', Integer, 'Set the minimum amount of regional control point counts for determining similar neighborhoods (default: 3)') do |count|
86
+ options[:regional_distance_similarities_count] = count
87
+ end
88
+
89
+ parser.on('--max-reduction-attempts COUNT', Integer, 'Set the max reduction attempts when removing neighborhood outliers (default: 2)') do |count|
90
+ options[:max_reduction_attempts] = count
91
+ end
92
+
93
+ desc = <<~DESC
94
+ Set distances to use when determining neighborhood region size in pairs
95
+ Use JSON e.g. '{"x1": 150, "x2": 30}'
96
+ Defaults:
97
+ Vertical pair is x is 10% of image width and y is 100px
98
+ Horizontal pair is x is 100px and y is 10% of image height
99
+ DESC
100
+ parser.on('--distances [DISTANCE_JSON]', desc) do |distances|
101
+ options[:distances] = JSON.parse(distances) rescue nil
102
+ end
103
+
104
+ parser.on('--distances-horizontal [DISTANCE_JSON]', 'Same as above but only affects horizontal image pairs') do |distances|
105
+ options[:regional_distance_similarities_count] = JSON.parse(distances) rescue nil
106
+ end
107
+
108
+ parser.on('--distances-vertical [DISTANCE_JSON]', 'Same as above but only affects vertical image pairs') do |distances|
109
+ options[:regional_distance_similarities_count] = JSON.parse(distances) rescue nil
110
+ end
111
+
73
112
  parser.on('-h', '--help', 'Display this screen') do
74
113
  puts parser
75
114
  exit
@@ -196,8 +196,8 @@ module Panomosity
196
196
  "#{to_s.sub(/\n/, '')} dist #{dist.round(4)} pixel_dist #{px.round(4)},#{py.round(4)},#{pdist.round(4)} pixel_r_dist #{prx.round(4)},#{pry.round(4)},#{prdist.round(4)} conn_type #{conn_type}"
197
197
  end
198
198
 
199
- def attributes
200
- @attributes
199
+ def attributes(raw: false)
200
+ @attributes.keep_if { |k, _| raw || !%i(raw i1 i2).include?(k) }
201
201
  end
202
202
  end
203
203
  end
@@ -0,0 +1,262 @@
1
+ require 'panomosity/utils'
2
+
3
+ module Panomosity
4
+ class GeneralizedNeighborhood
5
+ include Panomosity::Utils
6
+
7
+ CONTROL_POINT_ATTRIBUTES = [:dist_avg, :dist_std, :x_avg, :x_std, :y_avg, :y_std]
8
+ ATTRIBUTES = [:center, :scope, :control_points, :count] + CONTROL_POINT_ATTRIBUTES
9
+ DEFAULT_DISTANCE = 100
10
+
11
+ attr_reader :measure, :options
12
+ attr_accessor *ATTRIBUTES
13
+
14
+ class << self
15
+ include Panomosity::Utils
16
+
17
+ attr_accessor :options, :neighborhoods, :horizontal_similar_neighborhoods, :vertical_similar_neighborhoods, :horizontal_neighborhoods_by_similar_neighborhood, :vertical_neighborhoods_by_similar_neighborhood
18
+
19
+ def logger
20
+ @logger ||= Panomosity.logger
21
+ end
22
+
23
+ def attributes
24
+ { name: name }
25
+ end
26
+
27
+ def horizontal
28
+ horizontal_neighborhoods_by_similar_neighborhood
29
+ end
30
+
31
+ def vertical
32
+ vertical_neighborhoods_by_similar_neighborhood
33
+ end
34
+
35
+ def calculate_all(panorama:, options: {})
36
+ @neighborhoods = []
37
+ @options = options
38
+
39
+ Pair.create_pairs_from_panorama(panorama)
40
+ calculate_from_pairs
41
+ calculate_from_neighborhoods(type: :horizontal)
42
+ calculate_from_neighborhoods(type: :vertical)
43
+
44
+ @neighborhoods
45
+ end
46
+
47
+ def calculate_from_pairs
48
+ logger.debug 'calculating neighborhoods from pairs'
49
+ Pair.all.each do |pair|
50
+ control_points = pair.control_points.select(&:not_generated?)
51
+ control_points.each do |control_point|
52
+ base_params = { center: control_point, scope: pair, options: options }
53
+ position_params = { measure: { type: :position } }
54
+ neighborhood = calculate_neighborhood(**base_params.merge(position_params))
55
+ pair.generalized_neighborhoods << neighborhood
56
+ distance_params = { measure: { type: :distance, distances: { x1: neighborhood.dist_std } } }
57
+ distance_neighborhood = calculate_neighborhood(**base_params.merge(distance_params))
58
+ neighborhood.reference = distance_neighborhood
59
+ pair.generalized_neighborhoods << distance_neighborhood
60
+ end
61
+ end
62
+ end
63
+
64
+ def calculate_from_neighborhoods(type:)
65
+ count = options[:regional_distance_similarities_count] || 3
66
+ attempts = options[:max_reduction_attempts] || 2
67
+
68
+ # calculates similar neighborhoods based on the regional control point distances by pair
69
+ calculate_similar_neighborhoods(type: type, count: count)
70
+ if neighborhoods.empty?
71
+ logger.warn 'total neighborhoods came up empty, neighborhood default count to 2'
72
+ calculate_similar_neighborhoods(type: type, count: 2)
73
+ raise 'still could not find neighborhoods' if neighborhoods.empty?
74
+ end
75
+
76
+ std_outlier_reduction(type: type, max_reduction_attempts: attempts)
77
+ calculate_neighborhoods_by_similar_neighborhood(type: type)
78
+ end
79
+
80
+ def calculate_neighborhood(center:, scope:, options:, measure: {})
81
+ neighborhood = new(center: center, scope: scope, options: options)
82
+ neighborhood.update_measure(measure)
83
+ @neighborhoods << neighborhood.calculate
84
+ neighborhood
85
+ end
86
+
87
+ def similar_neighborhoods(type: :horizontal)
88
+ type == :horizontal ? @horizontal_similar_neighborhoods : @vertical_similar_neighborhoods
89
+ end
90
+
91
+ def neighborhoods_by_similar_neighborhood(type: :horizontal)
92
+ type == :horizontal ? @horizontal_neighborhoods_by_similar_neighborhood : @vertical_neighborhoods_by_similar_neighborhood
93
+ end
94
+
95
+ def calculate_similar_neighborhoods(type: :horizontal, count: 3)
96
+ similar_neighborhoods = neighborhoods.select(&:measure_position?).select(&:"#{type}?").select do |neighborhood|
97
+ neighborhood.scope.similar_neighborhoods << neighborhood if neighborhood.reference.count >= count
98
+ end
99
+ self.send(:"#{type}_similar_neighborhoods=", similar_neighborhoods)
100
+ end
101
+
102
+ def std_outlier_reduction(type: :horizontal, max_reduction_attempts: 2, reduction_attempts: 0)
103
+ return if reduction_attempts >= max_reduction_attempts
104
+ logger.debug "twice reducing #{type} neighborhood std outliers"
105
+ avg, std = *calculate_average_and_std(values: similar_neighborhoods(type: type).map(&:dist_std))
106
+ similar_neighborhoods(type: type).select! { |n| (avg - n.dist_std).abs <= std }
107
+ std_outlier_reduction(type: type, max_reduction_attempts: max_reduction_attempts, reduction_attempts: reduction_attempts + 1)
108
+ end
109
+
110
+ def calculate_neighborhoods_by_similar_neighborhood(type: :horizontal)
111
+ instance_variable_set("@#{type}_neighborhoods_by_similar_neighborhood", [])
112
+ similar_neighborhoods(type: type).each do |neighborhood|
113
+ base_params = { center: neighborhood, scope: self, options: options }
114
+ distance_params = { measure: { type: :distance, distances: { x1: neighborhood.dist_std } } }
115
+ neighborhoods_by_similar_neighborhood(type: type) << calculate_neighborhood(**base_params.merge(distance_params))
116
+ end
117
+
118
+ neighborhoods_by_similar_neighborhood(type: type).sort_by! { |n| -n.count }
119
+ neighborhoods_by_similar_neighborhood(type: type).max_by(5) { |n| n.count }.each do |n|
120
+ logger.debug "#{n.dist_avg} #{n.dist_std} #{n.count} x#{n.x_avg} y#{n.y_avg}"
121
+ end
122
+ end
123
+ end
124
+
125
+ def initialize(center:, scope:, options: {})
126
+ @center = center
127
+ @scope = scope
128
+ @options = options
129
+ @measure = Measure.new
130
+ end
131
+
132
+ def id
133
+ @id
134
+ end
135
+
136
+ def id=(id)
137
+ @id = id
138
+ end
139
+
140
+ def reference
141
+ @reference
142
+ end
143
+
144
+ def reference=(reference)
145
+ @reference = reference
146
+ end
147
+
148
+ def pair_scope?
149
+ scope.class.name == 'Panomosity::Pair'
150
+ end
151
+
152
+ def neighborhood_scope?
153
+ scope.class.name == self.class.name
154
+ end
155
+
156
+ def measure_position?
157
+ measure.position?
158
+ end
159
+
160
+ def measure_distance?
161
+ measure.distance?
162
+ end
163
+
164
+ def horizontal?
165
+ pair_scope? ? scope.horizontal? : center.horizontal?
166
+ end
167
+
168
+ def vertical?
169
+ pair_scope? ? scope.vertical? : center.vertical?
170
+ end
171
+
172
+ def type
173
+ horizontal? ? :horizontal : :vertical
174
+ end
175
+
176
+ def update_measure(params)
177
+ measure.update(params)
178
+ set_distance_defaults
179
+ set_measure_defaults
180
+ end
181
+
182
+ def distances_from_options(type: :horizontal)
183
+ if type == :both
184
+ options.fetch(:distances, {})
185
+ else
186
+ options[:distances]&.fetch(type, {}) || {}
187
+ end
188
+ end
189
+
190
+ def set_distance_defaults
191
+ return unless measure_position?
192
+
193
+ measure.update_distances(distances_from_options(type: :both))
194
+
195
+ if scope.horizontal?
196
+ measure.update_distances(x2: (scope.first_image.h * 0.1).round)
197
+ measure.update_distances(distances_from_options(type: :horizontal))
198
+ else
199
+ measure.update_distances(x1: (scope.first_image.w * 0.1).round)
200
+ measure.update_distances(distances_from_options(type: :vertical))
201
+ end
202
+
203
+ measure.distances[:x1] ||= DEFAULT_DISTANCE
204
+ measure.distances[:x2] ||= DEFAULT_DISTANCE
205
+ end
206
+
207
+ def set_measure_defaults
208
+ center_values = if measure_position?
209
+ { x1: center.x1, x2: center.y1 }
210
+ elsif pair_scope?
211
+ { x1: center.pdist }
212
+ else
213
+ { x1: center.dist_avg }
214
+ end
215
+
216
+ measure.update(center: center_values)
217
+ end
218
+
219
+ def calculate
220
+ if pair_scope?
221
+ elements = scope.control_points.select(&:not_generated?)
222
+
223
+ @control_points = if measure_position?
224
+ elements.select { |cp| measure.includes?(cp.x1, cp.y1) }
225
+ else
226
+ elements.select { |cp| measure.includes?(cp.pdist) }
227
+ end
228
+ else
229
+ @neighborhoods = scope.similar_neighborhoods(type: center.type).select { |n| measure.includes?(n.dist_avg) }
230
+ distance_neighborhoods = @neighborhoods.map(&:reference)
231
+ @control_points = distance_neighborhoods.map(&:control_points).flatten.uniq(&:raw)
232
+ end
233
+
234
+ @dist_avg, @dist_std = *calculate_average_and_std(values: control_points.map(&:pdist), ignore_empty: true)
235
+ @x_avg, @x_std = *calculate_average_and_std(values: control_points.map(&:px), ignore_empty: true)
236
+ @y_avg, @y_std = *calculate_average_and_std(values: control_points.map(&:py), ignore_empty: true)
237
+ @count = control_points.count
238
+
239
+ self
240
+ end
241
+
242
+ def attributes
243
+ attributes = CONTROL_POINT_ATTRIBUTES.reduce({}) { |h, k| h.merge!({ k => self.send(k) }) }
244
+ measure_attributes = measure.attributes.reduce({}) do |hash, (key, value)|
245
+ hash["measure_#{key}"] = value
246
+ hash
247
+ end
248
+ if pair_scope?
249
+ center_id = center[:id]
250
+ scope_id = [scope.control_points.first.n1, scope.control_points.first.n2]
251
+ scope_name = 'pair'
252
+ else
253
+ center_id = center.id
254
+ scope_id = nil
255
+ scope_name = 'neighborhood'
256
+ end
257
+ attributes.merge!(measure_attributes)
258
+ attributes.merge!(id: id, center: center_id, scope_id: scope_id, scope_name: scope_name, type: type, control_points: control_points.map{|c| c[:id]})
259
+ attributes
260
+ end
261
+ end
262
+ end
@@ -1,4 +1,4 @@
1
- # Exmaple of an image line
1
+ # Example of an image line
2
2
  # i w2448 h3264 f0 v=0 Ra=0 Rb=0 Rc=0 Rd=0 Re=0 Eev6.433 Er1 Eb1 r0 p0 y0 TrX0.88957152 TrY0.79560269 TrZ1 Tpy0 Tpp0 j0 a=0 b=0 c=0 d=0 e=0 g0 t0 Va=0 Vb=0 Vc=0 Vd=0 Vx=0 Vy=0 Vm5 n"WZ8ppTx9PtcxASB3hbeeuS6Z"\n
3
3
 
4
4
  module Panomosity
@@ -167,5 +167,11 @@ module Panomosity
167
167
  return unless name =~ /c(\d+)_r(\d+)\.jpg/
168
168
  name.scan(/c(\d+)_r(\d+)\.jpg/).flatten.map(&:to_i).last
169
169
  end
170
+
171
+ def attributes
172
+ attributes = @attributes.keep_if { |k, _| !%i(raw).include?(k) }
173
+ attributes.merge!(column: column || 0, row: row || 0)
174
+ attributes
175
+ end
170
176
  end
171
177
  end
@@ -0,0 +1,56 @@
1
+ require 'panomosity/utils'
2
+
3
+ module Panomosity
4
+ class Measure
5
+ include Panomosity::Utils
6
+
7
+ ATTRIBUTES = [:type, :center, :distances]
8
+ DEFAULT_DISTANCE = 100
9
+
10
+ attr_reader :options
11
+ attr_accessor *ATTRIBUTES
12
+
13
+ def initialize(attributes = {})
14
+ @attributes = attributes
15
+ @attributes[:center] ||= {}
16
+ @attributes[:distances] ||= {}
17
+ end
18
+
19
+ def position?
20
+ type == :position
21
+ end
22
+
23
+ def distance?
24
+ type == :distance
25
+ end
26
+
27
+ def type
28
+ @attributes[:type]
29
+ end
30
+
31
+ def center
32
+ @attributes[:center]
33
+ end
34
+
35
+ def distances
36
+ @attributes[:distances]
37
+ end
38
+
39
+ def update(attributes = {})
40
+
41
+ @attributes.merge!(attributes)
42
+ end
43
+
44
+ def update_distances(values = {})
45
+ @attributes[:distances].merge!(values)
46
+ end
47
+
48
+ def includes?(*values)
49
+ center.values.zip(values, distances.values).all? { |center, value, distance| (center - value).abs <= distance }
50
+ end
51
+
52
+ def attributes
53
+ { type: type, center: center.values, distances: distances.values }
54
+ end
55
+ end
56
+ end
@@ -59,12 +59,12 @@ module Panomosity
59
59
  end
60
60
 
61
61
  def to_s
62
- line_values = @@attributes.map { |attribute| "#{attribute}#{self.send(attribute)}" }
63
- "v #{line_values.join(' ')}\n"
62
+ line_values = @@attributes.map { |attribute| "#{attribute}#{self.send(attribute)}" if self.send(attribute) }
63
+ "v #{line_values.compact.join(' ')}\n"
64
64
  end
65
65
 
66
66
  def attributes
67
- @attributes.keep_if { |k,_| !%i(raw).include?(k) }
67
+ @attributes.keep_if { |k, _| !%i(raw).include?(k) }
68
68
  end
69
69
  end
70
70
  end
@@ -30,17 +30,14 @@ module Panomosity
30
30
  logger.info "applying custom values of xh_avg: #{xh_avg}, yh_avg: #{yh_avg}, xv_avg: #{xv_avg}, yv_avg: #{yv_avg}"
31
31
  end
32
32
 
33
- unless xh_avg && yh_avg && xv_avg && yv_avg
34
- Pair.calculate_neighborhoods(panorama)
35
- Pair.calculate_neighborhood_groups
36
- end
33
+ panorama.calculate_neighborhoods unless xh_avg && yh_avg && xv_avg && yv_avg
37
34
 
38
35
  ds = images.map(&:d).uniq.sort
39
36
  es = images.map(&:e).uniq.sort
40
37
 
41
38
  # get the average error for the best neighborhood group
42
- x_avg = xh_avg || NeighborhoodGroup.horizontal.first.rx_avg
43
- y_avg = yv_avg || NeighborhoodGroup.vertical.first.ry_avg
39
+ x_avg = xh_avg || GeneralizedNeighborhood.horizontal.first.x_avg
40
+ y_avg = yv_avg || GeneralizedNeighborhood.vertical.first.y_avg
44
41
 
45
42
  # start horizontally
46
43
  d_map = {}
@@ -57,8 +54,8 @@ module Panomosity
57
54
  logger.debug "created e_map #{e_map}"
58
55
 
59
56
  # add in the other offset
60
- x_avg = xv_avg || NeighborhoodGroup.vertical.first.rx_avg
61
- y_avg = yh_avg || NeighborhoodGroup.horizontal.first.ry_avg
57
+ x_avg = xv_avg || GeneralizedNeighborhood.vertical.first.x_avg
58
+ y_avg = yh_avg || GeneralizedNeighborhood.horizontal.first.y_avg
62
59
 
63
60
  de_map = {}
64
61
  d_map.each_with_index do |(dk,dv),di|
@@ -142,10 +139,9 @@ module Panomosity
142
139
  end
143
140
 
144
141
  def calculate_average_distance
145
- Pair.calculate_neighborhoods(panorama)
146
- Pair.calculate_neighborhood_groups
147
- horizontal_distances = NeighborhoodGroup.horizontal[0..4].map(&:prdist_avg)
148
- vertical_distances = NeighborhoodGroup.vertical[0..4].map(&:prdist_avg)
142
+ panorama.calculate_neighborhoods
143
+ horizontal_distances = GeneralizedNeighborhood.horizontal[0..4].map(&:dist_avg)
144
+ vertical_distances = GeneralizedNeighborhood.vertical[0..4].map(&:dist_avg)
149
145
  calculate_average(values: horizontal_distances + vertical_distances)
150
146
  end
151
147
 
@@ -161,9 +157,8 @@ module Panomosity
161
157
  images.each do |image|
162
158
  image.r = 0.0
163
159
  end
164
- Pair.calculate_neighborhoods(panorama)
165
- Pair.calculate_neighborhood_groups
166
- cps = NeighborhoodGroup.horizontal.first.control_points
160
+ panorama.calculate_neighborhoods
161
+ cps = GeneralizedNeighborhood.horizontal.first.control_points
167
162
  avg, std = *calculate_average_and_std(values: cps.map(&:roll))
168
163
  # 0.1 degrees of std
169
164
  while std > 0.1
@@ -5,7 +5,7 @@ module Panomosity
5
5
  include Panomosity::Utils
6
6
  extend Panomosity::Utils
7
7
 
8
- attr_accessor :pair, :control_points, :neighborhoods, :type
8
+ attr_accessor :pair, :control_points, :neighborhoods, :type, :generalized_neighborhoods, :similar_neighborhoods
9
9
 
10
10
  class << self
11
11
  attr_accessor :panorama, :logger
@@ -22,8 +22,8 @@ module Panomosity
22
22
  @pairs
23
23
  end
24
24
 
25
- def good_control_points_to_keep(count: 3)
26
- @pairs.map { |pair| pair.good_control_points_to_keep(count: count) }.flatten.uniq(&:raw)
25
+ def select_control_points_with_regional_distance_similarities
26
+ @pairs.map(&:select_control_points_with_regional_distance_similarities).flatten.uniq(&:raw)
27
27
  end
28
28
 
29
29
  def unconnected
@@ -44,6 +44,9 @@ module Panomosity
44
44
  rows = images.map(&:row).uniq.sort
45
45
 
46
46
  @pairs = []
47
+ @horizontal_pairs = []
48
+ @vertical_pairs = []
49
+
47
50
  # horizontal pair creation
48
51
  rows.each do |row|
49
52
  columns.each do |column|
@@ -52,7 +55,9 @@ module Panomosity
52
55
  image_2 = images.find { |i| i.row == row && i.column == column.next }
53
56
  next if @panorama.calibration? && (image_1.nil? || image_2.nil?)
54
57
  control_points = @panorama.control_points.select { |cp| [cp.n1, cp.n2].sort == [image_1.id, image_2.id].sort }
55
- @pairs << Pair.new([image_1, image_2].sort_by(&:id), control_points: control_points, type: :horizontal)
58
+ pair = Pair.new([image_1, image_2].sort_by(&:id), control_points: control_points, type: :horizontal)
59
+ @pairs << pair
60
+ @horizontal_pairs << pair
56
61
  end
57
62
  end
58
63
 
@@ -64,7 +69,9 @@ module Panomosity
64
69
  image_2 = images.find { |i| i.column == column && i.row == row.next }
65
70
  next if @panorama.calibration? && (image_1.nil? || image_2.nil?)
66
71
  control_points = @panorama.control_points.select { |cp| [cp.n1, cp.n2].sort == [image_1.id, image_2.id].sort }
67
- @pairs << Pair.new([image_1, image_2].sort_by(&:id), control_points: control_points, type: :vertical)
72
+ pair = Pair.new([image_1, image_2].sort_by(&:id), control_points: control_points, type: :vertical)
73
+ @pairs << pair
74
+ @vertical_pairs << pair
68
75
  end
69
76
  end
70
77
  end
@@ -150,6 +157,8 @@ module Panomosity
150
157
  @pair = pair
151
158
  @control_points = control_points
152
159
  @neighborhoods = []
160
+ @generalized_neighborhoods = []
161
+ @similar_neighborhoods = []
153
162
  @type = type
154
163
  end
155
164
 
@@ -157,6 +166,10 @@ module Panomosity
157
166
  pair.map(&:id).to_s.gsub(' ', '')
158
167
  end
159
168
 
169
+ def ==(other)
170
+ to_s == other.to_s
171
+ end
172
+
160
173
  def info
161
174
  "#{to_s}(#{type}) image_1 d,e: #{pair.first.d},#{pair.first.e} | image_2 d,e: #{pair.last.d},#{pair.last.e}"
162
175
  end
@@ -218,6 +231,27 @@ module Panomosity
218
231
  end
219
232
  end
220
233
 
234
+ # Distance neighborhoods
235
+ def similar_control_points
236
+ @similar_control_points ||= similar_neighborhoods.map(&:reference).map(&:control_points).flatten.uniq(&:raw)
237
+ end
238
+
239
+ def select_control_points_with_regional_distance_similarities
240
+ # Keep all our control points if we have less than 10
241
+ if control_points.count >= 10
242
+ ratio = similar_control_points.count.to_f / control_points.count
243
+ if ratio < 0.2
244
+ Panomosity.logger.warn "#{to_s} keeping less than 20% (#{(ratio * 100).round(4)}%) of #{control_points.count} control points. Reverting and keeping all control points"
245
+ control_points
246
+ else
247
+ similar_control_points
248
+ end
249
+ else
250
+ Panomosity.logger.debug "Skipping pair #{to_s} since it has fewer than 10 control points"
251
+ control_points
252
+ end
253
+ end
254
+
221
255
  def best_neighborhood
222
256
  @best_neighborhood ||= @neighborhoods.max_by { |n| n.control_points.count }
223
257
  end
@@ -225,5 +259,17 @@ module Panomosity
225
259
  def control_points_of_best_neighborhood
226
260
  best_neighborhood ? best_neighborhood.control_points : []
227
261
  end
262
+
263
+ def attributes
264
+ x_avg, x_std = *calculate_average_and_std(values: control_points.map(&:px), ignore_empty: true)
265
+ y_avg, y_std = *calculate_average_and_std(values: control_points.map(&:py), ignore_empty: true)
266
+ dist_avg, dist_std = *calculate_average_and_std(values: control_points.map(&:pdist), ignore_empty: true)
267
+ i1 = control_points.first.n1
268
+ i2 = control_points.first.n2
269
+ {
270
+ id: [i1, i2], n: i1, N: i2, count: control_points.count, type: type,
271
+ x_avg: x_avg, x_std: x_std, y_avg: y_avg, y_std: y_std, dist_avg: dist_avg, dist_std: dist_std
272
+ }
273
+ end
228
274
  end
229
275
  end
@@ -18,22 +18,25 @@ module Panomosity
18
18
  @logger = Panomosity.logger
19
19
  end
20
20
 
21
+ def calculate_neighborhoods
22
+ GeneralizedNeighborhood.calculate_all(panorama: self, options: options)
23
+ end
24
+
21
25
  def clean_control_points
22
- if calibration?
23
- Pair.calculate_neighborhoods(self, distance: 30)
24
- else
25
- Pair.calculate_neighborhoods(self, distance: 100)
26
- end
27
- control_points_to_keep = Pair.good_control_points_to_keep(count: 2)
26
+ options.merge!(distances: { x1: 30, x2: 30 }) if calibration? && !options[:distances].nil?
27
+ options.merge!(regional_distance_similarities_count: 2) unless options[:regional_distance_similarities_count]
28
+ calculate_neighborhoods
29
+
30
+ control_points_to_keep = Pair.select_control_points_with_regional_distance_similarities
28
31
  bad_control_points = control_points.reject { |cp| control_points_to_keep.map(&:raw).include?(cp.raw) }
29
32
  # far_control_points = control_points.select { |cp| cp.prdist > 50 }
30
33
  control_points_to_clean = bad_control_points.uniq(&:raw)
31
34
 
32
35
  # log warnings
33
36
  control_point_ratio = control_points_to_clean.count.to_f / control_points.count
34
- logger.warn "Removing more than 30% (#{(control_point_ratio*100).round(4)}%) of control points. May potentially cause issues." if control_point_ratio >= 0.3
37
+ logger.warn "Removing more than 30% (#{(control_point_ratio * 100).round(4)}%) of control points. May potentially cause issues." if control_point_ratio >= 0.3
35
38
  control_point_pair_ratio = Pair.without_enough_control_points(ignore_connected: true).count.to_f / Pair.all.count
36
- logger.warn "More than 50% (#{(control_point_pair_ratio*100).round(4)}%) of pairs have fewer than 3 control points. May potentially cause issues." if control_point_pair_ratio >= 0.5
39
+ logger.warn "More than 50% (#{(control_point_pair_ratio * 100).round(4)}%) of pairs have fewer than 3 control points. May potentially cause issues." if control_point_pair_ratio >= 0.5
37
40
 
38
41
  control_points_to_clean
39
42
  end
@@ -41,12 +44,8 @@ module Panomosity
41
44
  def fix_unconnected_image_pairs
42
45
  logger.info 'finding unconnected image pairs'
43
46
 
44
- if calibration?
45
- Pair.calculate_neighborhoods(self, distance: 30)
46
- else
47
- Pair.calculate_neighborhoods(self, distance: 100)
48
- end
49
- Pair.calculate_neighborhood_groups
47
+ options.merge!(distances: { x1: 30, x2: 30 }) if calibration? && !options[:distances].nil?
48
+ calculate_neighborhoods
50
49
 
51
50
  unconnected_image_pairs = Pair.unconnected
52
51
  logger.debug unconnected_image_pairs.map { |i| { type: i.type, pair: i.pair.map(&:id) } }
@@ -92,19 +91,19 @@ module Panomosity
92
91
  def generate_control_points(pair: nil, bad_control_point: nil, message: '')
93
92
  if pair
94
93
  if pair.horizontal?
95
- group = NeighborhoodGroup.horizontal.first
94
+ group = GeneralizedNeighborhood.horizontal.first
96
95
  else
97
- group = NeighborhoodGroup.vertical.first
96
+ group = GeneralizedNeighborhood.vertical.first
98
97
  end
99
98
  else
100
99
  if bad_control_point.conn_type == :horizontal
101
- group = NeighborhoodGroup.horizontal.first
100
+ group = GeneralizedNeighborhood.horizontal.first
102
101
  else
103
- group = NeighborhoodGroup.vertical.first
102
+ group = GeneralizedNeighborhood.vertical.first
104
103
  end
105
104
  end
106
105
 
107
- control_point = ControlPoint.new(group.center.center.attributes)
106
+ control_point = ControlPoint.new(group.center.center.attributes(raw: true))
108
107
 
109
108
  if pair
110
109
  control_point[:n] = pair.first_image.id
@@ -153,8 +152,7 @@ module Panomosity
153
152
  end
154
153
 
155
154
  def diagnose
156
- Pair.calculate_neighborhoods(self)
157
- Pair.calculate_neighborhood_groups
155
+ calculate_neighborhoods
158
156
 
159
157
  recommendations = []
160
158
  messages = []
@@ -184,7 +182,7 @@ module Panomosity
184
182
  end
185
183
 
186
184
  # neighborhood group tests
187
- group_count = NeighborhoodGroup.horizontal.count
185
+ group_count = GeneralizedNeighborhood.horizontal.count
188
186
  if group_count < 5
189
187
  message = <<~MESSAGE
190
188
  Total number of horizontal neighborhood groups is #{group_count} which is very low.
@@ -194,7 +192,7 @@ module Panomosity
194
192
  messages << message
195
193
  end
196
194
 
197
- group_std_avg = calculate_average(values: NeighborhoodGroup.horizontal[0..4].map(&:prdist_std))
195
+ group_std_avg = calculate_average(values: GeneralizedNeighborhood.horizontal[0..4].map(&:dist_std))
198
196
  if group_std_avg > 1.0
199
197
  message = <<~MESSAGE
200
198
  The standard deviation of distances in the top 5 horizontal neighborhood groups is #{group_std_avg} which is high.
@@ -207,7 +205,7 @@ module Panomosity
207
205
  messages << message
208
206
  end
209
207
 
210
- group_control_points = NeighborhoodGroup.horizontal.first.control_points.count
208
+ group_control_points = GeneralizedNeighborhood.horizontal.first.control_points.count
211
209
  total_control_points = Pair.horizontal.map(&:control_points).flatten.uniq(&:raw).count
212
210
  group_control_point_ratio = group_control_points.to_f / total_control_points
213
211
  if group_control_point_ratio < 0.2
@@ -222,7 +220,7 @@ module Panomosity
222
220
  recommendations << 'horizontal'
223
221
  end
224
222
 
225
- group_count = NeighborhoodGroup.vertical.count
223
+ group_count = GeneralizedNeighborhood.vertical.count
226
224
  if group_count < 5
227
225
  message = <<~MESSAGE
228
226
  Total number of vertical neighborhood groups is #{group_count} which is very low.
@@ -232,7 +230,7 @@ module Panomosity
232
230
  messages << message
233
231
  end
234
232
 
235
- group_std_avg = calculate_average(values: NeighborhoodGroup.vertical[0..4].map(&:prdist_std))
233
+ group_std_avg = calculate_average(values: GeneralizedNeighborhood.vertical[0..4].map(&:dist_std))
236
234
  if group_std_avg > 1.0
237
235
  message = <<~MESSAGE
238
236
  The standard deviation of distances in the top 5 vertical neighborhood groups is #{group_std_avg} which is high.
@@ -245,7 +243,7 @@ module Panomosity
245
243
  messages << message
246
244
  end
247
245
 
248
- group_control_points = NeighborhoodGroup.vertical.first.control_points.count
246
+ group_control_points = GeneralizedNeighborhood.vertical.first.control_points.count
249
247
  total_control_points = Pair.vertical.map(&:control_points).flatten.uniq(&:raw).count
250
248
  group_control_point_ratio = group_control_points.to_f / total_control_points
251
249
  if group_control_point_ratio < 0.2
@@ -274,11 +272,24 @@ module Panomosity
274
272
  delta_d: delta_d,
275
273
  delta_e: delta_e,
276
274
  roll: roll,
277
- horizontal: NeighborhoodGroup.horizontal.first.serialize,
278
- vertical: NeighborhoodGroup.vertical.first.serialize
275
+ horizontal: GeneralizedNeighborhood.horizontal.first.attributes,
276
+ vertical: GeneralizedNeighborhood.vertical.first.attributes
279
277
  }
280
278
  }
279
+ rescue => error
280
+ message = "Got error #{error.message} when calculating neighborhoods. Recommending fallback"
281
281
 
282
+ logger.error message
283
+ error.backtrace.each { |line| logger.error line }
284
+
285
+ recommendations = %w(horizontal vertical)
286
+
287
+ diagnostic_report = {
288
+ messages: messages,
289
+ recommendations: recommendations,
290
+ data: {}
291
+ }
292
+ ensure
282
293
  File.open('diagnostic_report.json', 'w+') { |f| f.puts diagnostic_report.to_json }
283
294
 
284
295
  if recommendations.empty?
@@ -301,17 +312,13 @@ module Panomosity
301
312
  calibration_report = JSON.parse(File.read(filename))
302
313
 
303
314
  if @options[:report_type] == 'position'
304
- if calibration?
305
- Pair.calculate_neighborhoods(self, distance: 30)
306
- else
307
- Pair.calculate_neighborhoods(self, distance: 100)
308
- end
309
- Pair.calculate_neighborhood_groups
315
+ options.merge!(distances: { x1: 30, x2: 30 }) if calibration? && !options[:distances].nil?
316
+ calculate_neighborhoods
310
317
 
311
- xh_avg = NeighborhoodGroup.horizontal.first.x_avg
312
- yh_avg = NeighborhoodGroup.horizontal.first.y_avg
313
- xv_avg = NeighborhoodGroup.vertical.first.x_avg
314
- yv_avg = NeighborhoodGroup.vertical.first.y_avg
318
+ xh_avg = GeneralizedNeighborhood.horizontal.first.x_avg
319
+ yh_avg = GeneralizedNeighborhood.horizontal.first.y_avg
320
+ xv_avg = GeneralizedNeighborhood.vertical.first.x_avg
321
+ yv_avg = GeneralizedNeighborhood.vertical.first.y_avg
315
322
  calibration_report['position'] = {
316
323
  xh_avg: xh_avg,
317
324
  yh_avg: yh_avg,
@@ -327,7 +334,39 @@ module Panomosity
327
334
  end
328
335
 
329
336
  def calibration?
330
- !!@input.split(/\n/).find { |line| '#panomosity calibration true' }
337
+ !!@input.split(/\n/).find { |line| line == '#panomosity calibration true' }
338
+ end
339
+
340
+ def save_file(filename)
341
+ logger.info "saving file #{filename}"
342
+
343
+ lines = @input.each_line.map do |line|
344
+ objects = [images, variable, control_points, optimisation_variables].flatten
345
+ object = objects.find { |object| object.raw == line }
346
+ object&.to_s || line
347
+ end.compact
348
+
349
+ File.open(filename, 'w') { |f| lines.each { |line| f.puts line } }
350
+ end
351
+
352
+ def attributes
353
+ calculate_neighborhoods
354
+ control_points = self.control_points.dup
355
+ control_points.each_with_index { |cp, i| cp[:id] = i }
356
+ neighborhoods = GeneralizedNeighborhood.neighborhoods.dup
357
+ neighborhoods.each_with_index { |n, i| n.id = i }
358
+ types = %i(horizontal vertical)
359
+ similar_neighborhoods = types.map { |type| GeneralizedNeighborhood.similar_neighborhoods(type: type) }.flatten
360
+ neighborhoods_by_similar_neighborhood = types.map { |type| GeneralizedNeighborhood.neighborhoods_by_similar_neighborhood(type: type) }.flatten
361
+ {
362
+ images: images.map(&:attributes),
363
+ variable: variable.attributes,
364
+ control_points: control_points.map(&:attributes),
365
+ optimisation_variables: optimisation_variables.map(&:attributes),
366
+ pairs: Pair.all.map(&:attributes),
367
+ similar_neighborhoods: similar_neighborhoods.map(&:attributes),
368
+ neighborhoods_by_similar_neighborhood: neighborhoods_by_similar_neighborhood.map(&:attributes)
369
+ }
331
370
  end
332
371
  end
333
372
  end
@@ -60,5 +60,9 @@ module Panomosity
60
60
  line_values = @@attributes.map { |attribute| "#{attribute}#{self.send(attribute)}" }
61
61
  "p #{line_values.join(' ')}\n"
62
62
  end
63
+
64
+ def attributes
65
+ @attributes.keep_if { |k, _| !%i(raw).include?(k) }
66
+ end
63
67
  end
64
68
  end
@@ -1,5 +1,6 @@
1
1
  require 'logger'
2
2
  require 'json'
3
+ require 'csv'
3
4
 
4
5
  module Panomosity
5
6
  class Runner
@@ -18,12 +19,15 @@ module Panomosity
18
19
  create_calibration_report
19
20
  crop_centers
20
21
  diagnose
22
+ export_control_point_csv
21
23
  fix_conversion_errors
22
24
  fix_unconnected_image_pairs
23
25
  generate_border_line_control_points
24
26
  get_columns_and_rows
25
27
  get_control_point_info
26
28
  get_neighborhood_info
29
+ get_panorama_attributes
30
+ import_control_point_csv
27
31
  merge_image_parameters
28
32
  nona_grid
29
33
  optimize
@@ -73,7 +77,7 @@ module Panomosity
73
77
  optimizer = Optimizer.new(panorama)
74
78
  calibration_report = JSON.parse(@report_file)
75
79
 
76
- if calibration_report.fetch('position')
80
+ if calibration_report.fetch('position', nil)
77
81
  logger.info 'calibration_report.json included position, applying position values'
78
82
  optimizer.run_position_optimizer(xh_avg: calibration_report['position']['xh_avg'],
79
83
  yh_avg: calibration_report['position']['yh_avg'],
@@ -81,7 +85,7 @@ module Panomosity
81
85
  yv_avg: calibration_report['position']['yv_avg'])
82
86
  end
83
87
 
84
- if calibration_report.fetch('roll')
88
+ if calibration_report.fetch('roll', nil)
85
89
  logger.info 'calibration_report.json included roll, applying roll values'
86
90
  optimizer.run_roll_optimizer(apply_roll: calibration_report.fetch('roll'))
87
91
  end
@@ -289,6 +293,25 @@ module Panomosity
289
293
  panorama.diagnose
290
294
  end
291
295
 
296
+ def export_control_point_csv
297
+ logger.info 'exporting control point csv'
298
+ panorama = Panorama.new(@input_file, @options)
299
+ panorama.calculate_neighborhoods
300
+
301
+ filename = 'control_points.csv'
302
+ headers = %w(image_1_name image_2_name image_1_id image_2_id type width height d1 e1 d2 e2 x1 y1 x2 y2 rx ry r)
303
+ CSV.open(filename, 'w+') do |csv|
304
+ csv << headers
305
+ panorama.control_points.each do |cp|
306
+ pair = Pair.all.find { |p| p.control_points.include?(cp) }
307
+ csv << [cp.i1.name, cp.i2.name, cp.i1.id, cp.i2.id, pair.type, cp.i1.w, cp.i1.h, cp.i1.d, cp.i1.e,
308
+ cp.i2.d, cp.i2.e, cp.x1, cp.y1, cp.x2, cp.y2, cp.px, cp.py, cp.pdist]
309
+ end
310
+ end
311
+
312
+ logger.info 'Done. Check for control_points.csv'
313
+ end
314
+
292
315
  def fix_conversion_errors
293
316
  logger.info 'fixing conversion errors'
294
317
  @lines = @input_file.each_line.map do |line|
@@ -459,6 +482,41 @@ module Panomosity
459
482
  panorama.get_neighborhood_info
460
483
  end
461
484
 
485
+ def get_panorama_attributes
486
+ # for printing out the JSON
487
+ logger.level = Logger::UNKNOWN
488
+ logger.info 'getting panorama attributes'
489
+ panorama = Panorama.new(@input_file, @options)
490
+ puts panorama.attributes.to_json
491
+ end
492
+
493
+ def import_control_point_csv
494
+ logger.info 'importing control point csv'
495
+ panorama = Panorama.new(@input_file, @options)
496
+
497
+ filename = @csv || 'control_points.csv'
498
+ csv = CSV.read(filename)
499
+ headers = csv.first
500
+ csv = csv[1..(csv.length - 1)]
501
+ data = csv.map { |s| Hash[headers.zip(s)] }
502
+
503
+ @lines = @input_file.each_line.map do |line|
504
+ cp = panorama.control_points.find { |c| c.raw == line }
505
+ if cp
506
+ kept_cp = data.find do |d|
507
+ cp.i1.id == d['image_1_id'].to_i && cp.i2.id == d['image_2_id'].to_i &&
508
+ cp.x1.round(4) == d['x1'].to_f.round(4) && cp.x2.round(4) == d['x2'].to_f.round(4) &&
509
+ cp.y1.round(4) == d['y1'].to_f.round(4) && cp.y2.round(4) == d['y2'].to_f.round(4)
510
+ end
511
+ cp.to_s if kept_cp
512
+ else
513
+ next line
514
+ end
515
+ end.compact
516
+
517
+ save_file
518
+ end
519
+
462
520
  def merge_image_parameters
463
521
  logger.info 'merging image parameters'
464
522
  control_points = ControlPoint.parse(@compare_file)
@@ -501,7 +559,7 @@ module Panomosity
501
559
  images = Image.parse(@input_file)
502
560
  res = @options[:res] || 'low'
503
561
  columns = images.map(&:column).uniq.sort
504
- columns.each do |column|
562
+ commands = columns.map do |column|
505
563
  @output = "project_nona_c#{column}.pto"
506
564
  @output_file = File.new(@output, 'w')
507
565
  logger.debug "creating file #{@output}"
@@ -515,9 +573,19 @@ module Panomosity
515
573
  end.compact
516
574
  save_file
517
575
  logger.debug "running nona #{@output}"
518
- output = `nona --save-intermediate-images --intermediate-suffix=intermediate -v -m TIFF_m --seam=blend #{@output} -o #{res}_res_stitch_section_c#{column.to_s.rjust(5, '0')}_`
576
+ output = "nona --save-intermediate-images --intermediate-suffix=intermediate -v -m TIFF_m --seam=blend #{@output} -o #{res}_res_stitch_section_c#{column.to_s.rjust(5, '0')}_"
519
577
  logger.debug output
578
+ output
579
+ end
580
+
581
+ logger.debug 'parallelizing'
582
+ if @options[:darwin]
583
+ parallel_command = "cat <<'EOF' | parallel \n#{commands.join("\n")}\nEOF"
584
+ else
585
+ parallel_command = "cat <<'EOF' | parallel -j $(fgrep -c processor /proc/cpuinfo) \n#{commands.join("\n")}\nEOF"
520
586
  end
587
+
588
+ `#{parallel_command}`
521
589
  end
522
590
 
523
591
  def optimize
@@ -1,3 +1,3 @@
1
1
  module Panomosity
2
- VERSION = '0.1.32'
2
+ VERSION = '0.1.37'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panomosity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.32
4
+ version: 0.1.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Garcia
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-12-19 00:00:00.000000000 Z
11
+ date: 2020-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -77,7 +77,9 @@ files:
77
77
  - exe/panomosity
78
78
  - lib/panomosity.rb
79
79
  - lib/panomosity/control_point.rb
80
+ - lib/panomosity/generalized_neighborhood.rb
80
81
  - lib/panomosity/image.rb
82
+ - lib/panomosity/measure.rb
81
83
  - lib/panomosity/neighborhood.rb
82
84
  - lib/panomosity/neighborhood_group.rb
83
85
  - lib/panomosity/optimisation_variable.rb
@@ -108,8 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
110
  - !ruby/object:Gem::Version
109
111
  version: '0'
110
112
  requirements: []
111
- rubyforge_project:
112
- rubygems_version: 2.7.3
113
+ rubygems_version: 3.0.8
113
114
  signing_key:
114
115
  specification_version: 4
115
116
  summary: Wrapper for the PTO file parsing needed for PanoTools.