Skip to content

Instantly share code, notes, and snippets.

@rwjblue
Created October 3, 2011 18:22
Show Gist options
  • Save rwjblue/1259829 to your computer and use it in GitHub Desktop.
Save rwjblue/1259829 to your computer and use it in GitHub Desktop.
Mark Sense Form Parser
begin
require 'oily_png'
rescue LoadError
require 'chunky_png'
end
require 'bigdecimal'
class ChunkyPNG::Image
def neighbors(x,y)
[[x, y-1], [x+1, y], [x, y+1], [x-1, y]].select do |xy|
include_xy?(*xy)
end
end
end
class MarkSenseForm
attr_accessor :image, :image_path, :left_anchor_coordinates, :right_anchor_coordinates, :angle, :slope
def initialize(options)
@image_path = options[:path]
@initial_left_anchor_coordinates = options[:left_anchor_coordinates]
@initial_right_anchor_coordinates = options[:right_anchor_coordinates]
@anchor_search_size = options[:anchor_search_size] || 300
@dark_threshold = options[:dark_threshold] || 35
@light_threshold = options[:light_threshold] || 600
@image = ChunkyPNG::Image.from_file(@image_path)
end
def pixel_darkness(color)
ChunkyPNG::Color.r(color) + ChunkyPNG::Color.g(color) + ChunkyPNG::Color.b(color)
end
def is_dark?(color)
pixel_darkness(color) < @dark_threshold
end
def is_light?(color)
pixel_darkness(color) > @light_threshold
end
# author: Jeff Kreeftmeijer
# from blog article: Pure Ruby colored blob detection
# http://jeffkreeftmeijer.com/2011/pure-ruby-colored-blob-detection/
def label_recursively(image, areas, label, x, y)
image[x,y] = label
(areas[label] ||= []) << [x,y]
image.neighbors(x,y).each do |xy|
if image[*xy] == -1
areas[label] << xy
label_recursively(image, areas, label, *xy)
end
end
end
def get_anchor_box_coordinates(image)
working_image = image.dup
working_image.pixels.map! do |pixel|
is_dark?(pixel) ? -1 : 0
end
areas, label = {}, 0
working_image.height.times do |y|
working_image.row(y).each_with_index do |pixel, x|
label_recursively(working_image, areas, label += 1, x, y) if pixel == -1
end
end
area = areas.values.max { |result, area_label| result.length <=> area_label.length }
x, y = area.map{ |xy| xy[0] }, area.map{ |xy| xy[1] }
[x.min, y.min, x.max, y.max]
end
def left_anchor_coordinates
@left_anchor_coordinates ||= get_anchor_box_coordinates(@image.crop(0, 0, @anchor_search_size, @anchor_search_size))
end
def right_anchor_coordinates
return @right_anchor_coordinates if @right_anchor_coordinates
@right_anchor_coordinates = get_anchor_box_coordinates(@image.crop(@image.width - @anchor_search_size, 0, @anchor_search_size, @anchor_search_size))
@right_anchor_coordinates = [ @image.width - @anchor_search_size + @right_anchor_coordinates[0], @right_anchor_coordinates[1], @image.width - @anchor_search_size + @right_anchor_coordinates[2], @right_anchor_coordinates[3]]
@right_anchor_coordinates
end
def calculate
x1, y1 = left_anchor_coordinates[2,2]
x2, y2 = right_anchor_coordinates[2,2]
@slope = ((y2 - y1).to_f / (x2 - x1).to_f)
@angle = Math.atan(@slope * 100)
@sin_slope = Math.sin(@slope)
@cos_slope = Math.cos(@slope)
@cached_points = {}
end
def corrected_point(x,y)
calculate unless @angle
return @cached_points[[x,y]] if @cached_points[[x,y]]
x_rotated = (x * @cos_slope) - (y * @sin_slope)
y_rotated = (x * @sin_slope) + (y * @cos_slope)
if @initial_left_anchor_coordinates
x_rotated += @left_anchor_coordinates[2] - @initial_left_anchor_coordinates[2]
y_rotated += @left_anchor_coordinates[3] - @initial_left_anchor_coordinates[3]
end
@cached_points[[x,y]] = [x_rotated.round, y_rotated.round]
@cached_points[[x,y]]
end
def straighten(open_straightened = false)
calculate unless @angle
`/usr/local/bin/convert #{@image_path} -rotate #{-@angle} straightened_#{@image_path}`
`open straightened_#{@image_path}` if open_straightened
end
def rectangle_from_point_and_margin(x, y, margin)
[x - margin/2, y - margin/2, margin, margin]
end
def percentage_filled(x, y, padding = 25)
x, y = corrected_point(x, y)
working_image = @image.crop(x - padding, y - padding, padding * 2, padding * 2)
light_pixels = working_image.pixels.select{|color| is_light?(color) }
BigDecimal.new(light_pixels.length.to_s) / BigDecimal.new(working_image.pixels.length.to_s)
end
def draw_rectangle(x,y, padding=25, color = ChunkyPNG::Color(:green))
x, y = corrected_point(x, y)
@image.rect(x - padding, y - padding, x + padding, y + padding, color)
end
end
if __FILE__ == $0
left_anchor_coordinates = [125, 110, 135, 133]
right_anchor_coordinates = [1126, 111, 1165, 149]
image = MarkSenseForm.new(:path => 'test_1_rotated.png',
:left_anchor_coordinates => left_anchor_coordinates,
:right_anchor_coordinates => right_anchor_coordinates)
puts image.percentage_filled(255, 534).to_s('F')
image.draw_rectangle(255, 534)
image.image.save('test_1_annotated.png')
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment