Class: Sqed::BoundaryFinder

Inherits:
Object
  • Object
show all
Defined in:
lib/sqed/boundary_finder.rb

Overview

Sqed Boundary Finders find boundaries on images and return co-ordinates of those boundaries. They do not return derivative images. Finders operate on cropped images, i.e. only the “stage”.

Direct Known Subclasses

ColorLineFinder, CrossFinder, StageFinder

Defined Under Namespace

Classes: ColorLineFinder, CrossFinder, StageFinder

Constant Summary collapse

THUMB_SIZE =

Problemantic (e.g. seven slot) seem to resolve at ~360

100
COLOR_DELTA =

color (e.g. red) must be this much be *COLOR_DELTA > than other values (e.g. blue/green)

1.3

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**opts) ⇒ BoundaryFinder

Returns a new instance of BoundaryFinder.

Raises:



29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/sqed/boundary_finder.rb', line 29

def initialize(**opts)
  # image: image, layout: layout, use_thumbnail: true
  @use_thumbnail = opts[:use_thumbnail]
  @use_thumbnail = true if @use_thumbnail.nil?
  @layout = opts[:layout]
  @image = opts[:image]

  raise Sqed::Error, 'No layout provided.' if layout.nil?
  raise Sqed::Error, 'No image provided.' if image.nil? || image.class.name != 'Magick::Image'

  true
end

Instance Attribute Details

#boundariesObject (readonly)

Returns a Sqed::Boundaries instance initialized to the number of sections in the passed layout.



20
21
22
# File 'lib/sqed/boundary_finder.rb', line 20

def boundaries
  @boundaries
end

#imageObject (readonly)

the passed image



14
15
16
# File 'lib/sqed/boundary_finder.rb', line 14

def image
  @image
end

#layoutObject (readonly)

a symbol from SqedConfig::LAYOUTS



17
18
19
# File 'lib/sqed/boundary_finder.rb', line 17

def layout
  @layout
end

#original_imageObject (readonly)

when we compute using a derived thumbnail we temporarily store the full size image here



27
28
29
# File 'lib/sqed/boundary_finder.rb', line 27

def original_image
  @original_image
end

#use_thumbnailObject (readonly)

Whether to compress the original image to a thumbnail when finding boundaries at certain steps of the processing

Returns:

  • Boolean



24
25
26
# File 'lib/sqed/boundary_finder.rb', line 24

def use_thumbnail
  @use_thumbnail
end

Class Method Details

.color_boundary_finder(**opts) ⇒ Array

image: image, sample_subdivision_size: nil, sample_cutoff_factor: nil, scan: :rows, boundary_color: :green)

Parameters:

  • image

    the image to sample

  • sample_subdivision_size

    an Integer, the distance in pixels b/w samples

  • sample_cutoff_factor: (0.0-1.0)

    if provided over-rides the default cutoff calculation by reducing the number of pixels required to be considered a border hit

    - for example, if you have an image of height 100 pixels, and a sample_subdivision_size of 10, and a sample_cutoff_factor of .8
      then only posititions with 8 ((100/10)*.8) or more hits
    - when nil the cutoff defaults to the maximum of the pairwise difference between hit counts
    
  • scan

    (:rows|:columns), :rows finds vertical borders, :columns finds horizontal borders

Returns:

  • (Array)

    the x or y position returned as a start, mid, and end coordinate that represent the width of the colored line that completely divides the image, e.g. [9, 15, 16]



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/sqed/boundary_finder.rb', line 126

def self.color_boundary_finder(**opts)
  image = opts[:image]
  sample_subdivision_size = opts[:sample_subdivision_size]
  sample_cutoff_factor = opts[:sample_cutoff_factor]
  scan = opts[:scan] || :rows
  boundary_color = opts[:boundary_color] || :green

  image_width = image.send(scan)
  sample_subdivision_size = get_subdivision_size(image_width) if sample_subdivision_size.nil?

  attempts = 0
  while attempts < 5 do
    samples_to_take = (image_width / sample_subdivision_size).to_i - 1
    border_hits = sample_border(image, boundary_color, samples_to_take, sample_subdivision_size, scan)

    break if border_hits.select{|k,v| v > 1}.size > 2 || sample_subdivision_size == 1

    sample_subdivision_size = (sample_subdivision_size.to_f / 2.0).to_i
    attempts += 1
  end

  return nil if border_hits.length < 2

  if sample_cutoff_factor.nil?
    cutoff = max_difference(border_hits.values)
    cutoff = border_hits.values.first - 1 if cutoff == 0 # difference of two identical things is 0
  else
    cutoff = (samples_to_take * sample_cutoff_factor).to_i
  end

  frequency_stats(border_hits, cutoff)
end

.derivative(array) ⇒ Object



262
263
264
# File 'lib/sqed/boundary_finder.rb', line 262

def self.derivative(array)
  (0..array.length - 2).map { |i| array[i + 1] - array[i] }
end

.derivative_signs(array) ⇒ Object



258
259
260
# File 'lib/sqed/boundary_finder.rb', line 258

def self.derivative_signs(array)
  (0..array.length - 2).map { |i| (array[i + 1] - array[i]) <=> 0 }
end

.frequency_stats(frequency_hash, sample_cutoff = 0) ⇒ Object

return [Array]

the start, mid, endpoint position of all (pixel) positions that have a count greater than the cutoff


210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/sqed/boundary_finder.rb', line 210

def self.frequency_stats(frequency_hash, sample_cutoff = 0)

  return nil if sample_cutoff.nil? || sample_cutoff < 1
  hit_ranges = []

  frequency_hash.each do |position, count|
    if count >= sample_cutoff
      hit_ranges.push(position)
    end
  end

  case hit_ranges.size
  when 1
    c = hit_ranges[0]
    hit_ranges = [c - 1, c, c + 1]
  when 2
    hit_ranges.sort!
    c1 = hit_ranges[0]
    c2 = hit_ranges[1]
    hit_ranges = [c1, c2, c2 + (c2 - c1)]
  when 0
    return nil
  end

  # we have to sort because the keys (positions) we examined came unordered from a hash originally
  hit_ranges.sort!

  # return the position exactly in the middle of the array
  [hit_ranges.first, hit_ranges[(hit_ranges.length / 2).to_i], hit_ranges.last]
end

.get_subdivision_size(image_width) ⇒ Object

return [Integer, nil]

sample more with small images, less with large images
we want to return larger numbers (= faster sampling)


87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/sqed/boundary_finder.rb', line 87

def self.get_subdivision_size(image_width)
  case image_width
  when nil
    nil
  when 0..140
    6
  when 141..640
    12
  when 641..1000
    16
  when 1001..3000
    60
  when 3001..6400
    80
  else
    140
  end
end

.is_black?(pixel) ⇒ Boolean

Returns:

  • (Boolean)


203
204
205
206
# File 'lib/sqed/boundary_finder.rb', line 203

def self.is_black?(pixel)
  black_threshold = 65535 * 0.15 #tune for black
  (pixel.red < black_threshold) &&  (pixel.blue < black_threshold) &&  (pixel.green < black_threshold)
end

.is_blue?(pixel) ⇒ Boolean

Returns:

  • (Boolean)


195
196
197
# File 'lib/sqed/boundary_finder.rb', line 195

def self.is_blue?(pixel)
  (pixel.blue > pixel.red*COLOR_DELTA) && (pixel.blue > pixel.green*COLOR_DELTA)
end

.is_green?(pixel) ⇒ Boolean

Returns:

  • (Boolean)


191
192
193
# File 'lib/sqed/boundary_finder.rb', line 191

def self.is_green?(pixel)
  (pixel.green > pixel.red*COLOR_DELTA) && (pixel.green > pixel.blue*COLOR_DELTA)
end

.is_red?(pixel) ⇒ Boolean

Returns:

  • (Boolean)


199
200
201
# File 'lib/sqed/boundary_finder.rb', line 199

def self.is_red?(pixel)
  (pixel.red > pixel.blue*COLOR_DELTA) && (pixel.red > pixel.green*COLOR_DELTA)
end

.max_difference(array) ⇒ Object



241
242
243
# File 'lib/sqed/boundary_finder.rb', line 241

def self.max_difference(array)
  array.max - array.min
end

.max_pairwise_difference(array) ⇒ Object

Returns an Integer, the maximum of the pairwise differences of the values in the array For example, given

[1,2,3,9,6,2,0]

The resulting pairwise array is

[1,1,6,3,4,2]

The max (value returned) is

6


254
255
256
# File 'lib/sqed/boundary_finder.rb', line 254

def self.max_pairwise_difference(array)
  (0..array.length - 2).map{|i| (array[i] - array[i + 1]).abs }.max
end

.sample_border(image, boundary_color, samples_to_take, sample_subdivision_size, scan) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/sqed/boundary_finder.rb', line 159

def self.sample_border(image, boundary_color, samples_to_take, sample_subdivision_size, scan)
  border_hits = {}

  (0..samples_to_take).each do |s|
    # Create a sample image a single pixel tall
    if scan == :rows
      j = image.crop(0, s * sample_subdivision_size, image.columns, 1, true)
    elsif scan == :columns
      j = image.crop(s * sample_subdivision_size, 0, 1, image.rows, true)
    else
      raise Sqed::Error
    end

    j.each_pixel do |pixel, c, r|
      index = (scan == :rows) ? c : r

      # Our hit metric is dirt simple, if there is some percentage more of the boundary_color than the others, count + 1 for that column
      if send("is_#{boundary_color}?", pixel)
        # we have already hit that column previously, increment
        if border_hits[index]
          border_hits[index] += 1
          # initialize the newly hit column 1
        else
          border_hits[index] = 1
        end
      end
    end
  end

  border_hits
end

Instance Method Details

#height_factorObject



76
77
78
# File 'lib/sqed/boundary_finder.rb', line 76

def height_factor
  image.rows.to_f / thumbnail_height.to_f
end

#longest_thumbnail_axisObject



47
48
49
# File 'lib/sqed/boundary_finder.rb', line 47

def longest_thumbnail_axis
  image.columns > image.rows ? :width : :height
end

#thumbnailObject



68
69
70
# File 'lib/sqed/boundary_finder.rb', line 68

def thumbnail
  image.thumbnail(thumbnail_width, thumbnail_height)
end

#thumbnail_heightObject



51
52
53
54
55
56
57
# File 'lib/sqed/boundary_finder.rb', line 51

def thumbnail_height
  if longest_thumbnail_axis == :height
    THUMB_SIZE
  else
    (image.rows.to_f * (THUMB_SIZE.to_f / image.columns.to_f)).round.to_i
  end
end

#thumbnail_widthObject



59
60
61
62
63
64
65
# File 'lib/sqed/boundary_finder.rb', line 59

def thumbnail_width
  if longest_thumbnail_axis == :width
    THUMB_SIZE
  else
    (image.columns.to_f * (THUMB_SIZE.to_f / image.rows.to_f)).round.to_i
  end
end

#width_factorObject



72
73
74
# File 'lib/sqed/boundary_finder.rb', line 72

def width_factor
  image.columns.to_f / thumbnail_width.to_f
end

#zoom_boundariesObject



80
81
82
# File 'lib/sqed/boundary_finder.rb', line 80

def zoom_boundaries
  boundaries.zoom(width_factor, height_factor )
end