panomosity 0.1.32 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.