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