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 =
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.



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

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 'No layout provided.' if layout.nil?
  raise '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.



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

def boundaries
  @boundaries
end

#imageObject (readonly)

the passed image



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

def image
  @image
end

#layoutObject (readonly)

a symbol from SqedConfig::LAYOUTS



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

def layout
  @layout
end

#original_imageObject (readonly)

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



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

def original_image
  @original_image
end

#use_thumbnailObject (readonly)

Whether to compress the original image to a thumbnail when finding boundaries



22
23
24
# File 'lib/sqed/boundary_finder.rb', line 22

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]



124
125
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
# File 'lib/sqed/boundary_finder.rb', line 124

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



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

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

.derivative_signs(array) ⇒ Object



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

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


208
209
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
# File 'lib/sqed/boundary_finder.rb', line 208

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)


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

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)


201
202
203
204
# File 'lib/sqed/boundary_finder.rb', line 201

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)


193
194
195
# File 'lib/sqed/boundary_finder.rb', line 193

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

.is_green?(pixel) ⇒ Boolean

Returns:

  • (Boolean)


189
190
191
# File 'lib/sqed/boundary_finder.rb', line 189

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

.is_red?(pixel) ⇒ Boolean

Returns:

  • (Boolean)


197
198
199
# File 'lib/sqed/boundary_finder.rb', line 197

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

.max_difference(array) ⇒ Object



251
252
253
# File 'lib/sqed/boundary_finder.rb', line 251

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


247
248
249
# File 'lib/sqed/boundary_finder.rb', line 247

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



157
158
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
# File 'lib/sqed/boundary_finder.rb', line 157

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
    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



74
75
76
# File 'lib/sqed/boundary_finder.rb', line 74

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

#longest_thumbnail_axisObject



45
46
47
# File 'lib/sqed/boundary_finder.rb', line 45

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

#thumbnailObject



66
67
68
# File 'lib/sqed/boundary_finder.rb', line 66

def thumbnail
  image.thumbnail(thumbnail_width, thumbnail_height)
end

#thumbnail_heightObject



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

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



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

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



70
71
72
# File 'lib/sqed/boundary_finder.rb', line 70

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

#zoom_boundariesObject



78
79
80
# File 'lib/sqed/boundary_finder.rb', line 78

def zoom_boundaries
  boundaries.zoom(width_factor, height_factor )
end