require 'mork/npatch' require 'mork/magicko' module Mork # @private class Mimage include Extensions attr_reader :rm attr_reader :choxq # choices per question attr_reader :status def initialize(path, grom) @mack = Magicko.new path @grom = grom.set_page_size @mack.width, @mack.height @choxq = [(0...@grom.max_choices_per_question).to_a] * grom.max_questions @rm = {} # registration mark centers @valid = register end def valid? @valid end def low_contrast? @rm.any? { |k,v| v < @grom.reg_min_contrast } end def status { tl: @rm[:tl][:status], tr: @rm[:tr][:status], br: @rm[:br][:status], bl: @rm[:bl][:status] } end def set_ch(choxq) @choxq = choxq.map { |ncho| (0...ncho).to_a } # if set_ch is called more than once, discard memoization @marked_choices = @choice_mean_darkness = nil end def choice_mean_darkness @choice_mean_darkness ||= begin itemator(@choxq) { |q,c| reg_pixels.average @grom.choice_cell_area(q, c) } end end def marked @marked_choices ||= begin choice_mean_darkness.map do |cho| [].tap do |choices| cho.map.with_index do |drk, c| choices << c if drk < choice_threshold end end end end end def barcode_bits @barcode_bits ||= begin @grom.barcode_bits.times.map do |b| reg_pixels.average(@grom.barcode_bit_area b+1) < barcode_threshold end end end def overlay(what, where) areas = case where when :barcode @grom.barcode_areas barcode_bits when :cal @grom.calibration_cell_areas when :marked choice_cell_areas marked when :all choice_cell_areas @choxq when :max @grom.choice_cell_areas.flatten when Array choice_cell_areas where else fail ArgumentError, 'Invalid overlay argument “where”' end round = where != :barcode @mack.send what, areas, round end # write the underlying MiniMagick::Image to disk; # if no file name is given, image is processed in-place; # if the 2nd arg is false, then stretching is not applied def save(fname=nil, reg=true) pp = reg ? @rm : nil @mack.save fname, pp end def save_registration(fname) each_corner { |c| @mack.plus @rm[c][:x], @rm[c][:y], 30 } each_corner { |c| @mack.outline [@grom.rm_crop_area(c)], false } @mack.save fname, nil end # ============================================================# private # # ============================================================# def itemator(cells) cells.map.with_index do |cho, q| cho.map { |c| yield q, c } end end def choice_cell_areas(cells) if cells.length > @grom.max_questions fail ArgumentError, 'Maximum number of responses exceeded' end if cells.any? { |q| q.any? { |c| c >= @grom.max_choices_per_question } } fail ArgumentError, 'Maximum number of choices exceeded' end itemator(cells) { |q,c| @grom.choice_cell_area q, c }.flatten end def each_corner [:tl, :tr, :br, :bl].each { |c| yield c } end def choice_threshold @choice_threshold ||= begin dcm = choice_mean_darkness.flatten.min (cal_cell_mean-dcm) * @grom.choice_threshold + dcm end end def cal_cell_mean m = @grom.calibration_cell_areas.map { |c| reg_pixels.average c } m.inject(:+) / m.length.to_f end def barcode_threshold @barcode_threshold ||= (paper_white + ink_black) / 2 end def ink_black reg_pixels.average @grom.ink_black_area end def paper_white reg_pixels.average @grom.paper_white_area end def reg_pixels @reg_pixels ||= NPatch.new @mack.registered_bytes(@rm), @mack.width, @mack.height end # find the XY coordinates of the 4 registration marks, # plus the stdev of the search area as quality control def register each_corner { |c| @rm[c] = rm_centroid_on c } @rm.all? { |k,v| v[:status] == :ok } end # returns the centroid of the dark region within the given area # in the XY coordinates of the entire image def rm_centroid_on(corner) c = @grom.rm_crop_area(corner) p = @mack.rm_patch(c, @grom.rm_blur, @grom.rm_dilate) # byebug n = NPatch.new(p, c.w, c.h) cx, cy, sd = n.centroid st = (cx < 2) or (cy < 2) or (cy > c.h-2) or (cx > c.w-2) status = st ? :edgy : :ok return {x: cx+c.x, y: cy+c.y, sd: sd, status: status} end end end