class Iguvium::CV

Performs all the computer vision job except table composition. Edge detection is performed using simplified two-directional version of Canny edge detection operator applied to rows and columns as integer vectors

Constants

Recognized

Keeps recognized data

Attributes

blurred[R]
image[R]

Public Class Methods

new(image) click to toggle source

Prepares image for recognition: initial blur @param image [ChunkyPNG::Image] from {Iguvium::Image.read}

# File lib/iguvium/cv.rb, line 41
def initialize(image)
  @blurred = blur(image)
  @image = to_narray(image).to_a
end

Public Instance Methods

recognize() click to toggle source

@return [Recognized]

lines most probably forming table cells and tables' outer borders as boxes
# File lib/iguvium/cv.rb, line 50
def recognize
  Recognized.new(lines, boxes)
  # {
  #   lines: lines,
  #   boxes: boxes
  # }
end

Private Instance Methods

array_border(array, width, value = 0) click to toggle source
# File lib/iguvium/cv.rb, line 128
def array_border(array, width, value = 0)
  hl = Array.new width, Array.new(array.first.count + width * 2, value)
  hl + array.map { |row| [value] * width + row + [value] * width } + hl
end
blur(image) click to toggle source

END OF FLIPPER CODE

# File lib/iguvium/cv.rb, line 119
def blur(image)
  convolve(to_narray(image), GAUSS).to_a
end
border(narray, width, value = 0) click to toggle source
# File lib/iguvium/cv.rb, line 133
def border(narray, width, value = 0)
  NArray[*array_border(narray.to_a, width, value)]
end
box(coord_array) click to toggle source
# File lib/iguvium/cv.rb, line 185
def box(coord_array)
  ax, bx = coord_array.map(&:last).minmax
  ay, by = coord_array.map(&:first).minmax
  [ax..bx, flip_range(ay..by)]
end
boxes() click to toggle source
# File lib/iguvium/cv.rb, line 70
def boxes
  return @boxes if @boxes

  brightest = image.flatten.max
  @boxes = Labeler.new(
    # image.map { |row| row.map { |pix| 255 - pix } }
    image.map { |row| row.map { |pix| pix < brightest } }
  ).clusters.map { |cluster| box cluster }.sort_by { |xrange, yrange| [yrange.begin, xrange.begin] }
end
convolve(narray, conv, border_value = 255) click to toggle source
# File lib/iguvium/cv.rb, line 123
def convolve(narray, conv, border_value = 255)
  narray = border(narray, conv.shape.first / 2, border_value)
  Convolver.convolve(narray, conv).ceil
end
edges(vector) click to toggle source
# File lib/iguvium/cv.rb, line 171
def edges(vector)
  Array
    .new(vector.count)
    .tap { |ary| minimums(vector).each { |i| ary[i] = 1 } }
end
flip_line(line) click to toggle source
# File lib/iguvium/cv.rb, line 104
def flip_line(line)
  y = line.last
  y = if y.is_a?(Numeric)
        flip_y y
      elsif y.is_a?(Range)
        flip_range y
      else
        raise ArgumentError, 'WTF?!'
      end

  [line.first, y]
end
flip_range(range) click to toggle source
# File lib/iguvium/cv.rb, line 100
def flip_range(range)
  flip_y(range.end)..flip_y(range.begin)
end
flip_y(coord) click to toggle source

START OF FLIPPER CODE

# File lib/iguvium/cv.rb, line 95
def flip_y(coord)
  @height ||= image.count
  @height - coord - 1
end
horizontal_scan(image) click to toggle source
# File lib/iguvium/cv.rb, line 177
def horizontal_scan(image)
  image.map { |row| edges row }
end
horizontals(threshold = 3) click to toggle source
# File lib/iguvium/cv.rb, line 87
def horizontals(threshold = 3)
  Matrix
    .rows(convolve(NArray[*vertical_scan(blurred)], HORIZONTAL, 0).to_a)
    .map { |pix| pix < threshold ? nil : pix }
    .to_a
end
lines() click to toggle source
# File lib/iguvium/cv.rb, line 60
def lines
  @lines ||=
    {
      vertical: Labeler.new(verticals)
                       .lines
                       .map { |line| flip_line line },
      horizontal: Labeler.new(horizontals).lines.map { |line| flip_line line }
    }
end
minimums(ary) click to toggle source

def minimums_old(ary)

ary.each_cons(2)
   .each_with_index
   .map { |(a, b), i| [i + 1, a <=> b] }
   .slice_when { |a, b| a.last != -1 && b.last == -1 }
   .to_a
   .map { |seq| seq.reverse.detect do |a| a.last == 1 end&.first }
   .compact

end

# File lib/iguvium/cv.rb, line 158
def minimums(ary)
  # This ugly piece of code takes ~200 ms per page scan to run vs ~700 ms for the prettier old one
  i = 0
  mins = []
  local = 0
  while i + 2 < ary.length
    local = i + 1 if ary[i] > ary[i + 1]
    mins << local if ary[i] >= ary[i + 1] && ary[i + 1] < ary[i + 2]
    i += 1
  end
  mins.uniq
end
to_narray(image) click to toggle source
# File lib/iguvium/cv.rb, line 137
def to_narray(image)
  palette = image.pixels.uniq
  # Precalculation looks stupid but spares up to 0.35 seconds on calculation depending on colorspace width
  dict = palette.zip(
    palette.map { |color| ChunkyPNG::Color.grayscale_teint ChunkyPNG::Color.compose(color, 0xffffffff) }
  ).to_h
  NArray[
    image.pixels.map { |color| dict[color] }
  ].reshape(image.width, image.height)
end
vertical_scan(image) click to toggle source
# File lib/iguvium/cv.rb, line 181
def vertical_scan(image)
  image.transpose.map { |row| edges row }.transpose
end
verticals(threshold = 3) click to toggle source
# File lib/iguvium/cv.rb, line 80
def verticals(threshold = 3)
  Matrix
    .rows(convolve(NArray[*horizontal_scan(blurred)], VERTICAL, 0).to_a)
    .map { |pix| pix < threshold ? nil : pix }
    .to_a
end