require 'mork/grid_omr' require 'mork/mimage' require 'mork/mimage_list' module Mork # Optical mark recognition of a response sheet that was: 1) generated # with SheetPDF, 2) printed on plain paper, 3) filled out by a responder, # and 4) acquired as a image file. # # The sheet is automatically registered upon object creation, after which it # is possible to perform queries, as well as save a copy of the scanned # image with various overlays superimposed, highlighting the expected correc # choices, the actually marked ones, etc. class SheetOMR # @param path [String] the required path/filename to the saved image # (.jpg, .jpeg, .png, or .pdf) # @param choices [Fixnum, Array] the questions/choices we want scored, as # an optional named argument. Scoring is done on all available questions, # if the argument is omitted, on the first N questions, If an integer # is passed, or on the indicated questions, if the argument is an array. # @param layout [String, Hash] the sheet description. Use a string to # specify the path/filename of a YAML file containing the parameters, # or directly a hash of parameters. See the README file for a full listing # of the available parameters. def initialize(path, choices: nil, layout: nil) raise IOError, "File '#{path}' not found" unless File.exists? path grom = GridOMR.new layout nitems = case choices when NilClass [grom.max_choices_per_question] * grom.max_questions when Fixnum [grom.max_choices_per_question] * choices when Array choices end @mim = Mimage.new path, nitems, grom end # True if sheet registration completed successfully # # @return [Boolean] def valid? @mim.valid? end # Registration status for each of the four corners # # @return [Hash] { tl: Symbol, tr: Symbol, br: Symbol, bl: Symbol } where # symbol is either `:ok` or `:edgy`, meaning that the centroid was found # to be too close to the edge of the search area to be considered reliable def status @mim.status end # Sheet barcode as an integer # # @return [Fixnum] def barcode return if not_registered barcode_string.to_i(2) end # Sheet barcode as a binary-like string # # @return [String] a string of 0s and 1s; the string is `barcode_bits` # bits long, with most significant bits to the left def barcode_string return if not_registered @mim.barcode_bits.map do |b| b ? '1' : '0' end.join.reverse end # True if the specified question/choice cell has been marked # # @param question [Fixnum] the question number, zero-based # @param choice [Fixnum] the choice number, zero-based # @return [Boolean] def marked?(question, choice) return if not_registered @mim.marked[question][choice] end # The set of choice indices marked on the response sheet # # @return [Array] an array of arrays of integers; each element contains # the (zero-based) list of marked choices for the corresponding question. # For example, the following `marked_choices` array: `[[0], [], [3,4]]` # indicates that the responder has marked the first choice for the first # question, none for the second, and the fourth and fifth choices for the # third question. # # Note that only the questions/choices indicated via the `choices` argument # during object creation are evaluated. def marked_choices return if not_registered @mim.marked_int end # The set of letters marked on the response sheet. At this time, only the # latin sequence 'A, B, C...' is supported. # # @return [Array] an array of arrays of 1-character strings; each element # contains the list of letters marked for the corresponding question. # # Note that only the questions/choices indicated via the `choices` argument # during object creation are evaluated. def marked_letters return if not_registered marked_choices.map do |q| q.map { |cho| (65+cho).chr } end end # Marked choices as boolean values # # @return [Array] an array of arrays of true/false values corresponding to # marked vs unmarked choice cells. def marked_logicals return if not_registered @mim.marked end # Apply an overlay on the image # # @param what [Symbol] the overlay type, choose from `:outline`, `:check`, # `:highlight` # @param where [Array, Symbol] where to apply the overlay. Either an array # of arrays of (zero-based) indices to specify target cells, or one of # the following symbols: `:marked`: all marked cells, among those # specified by the `choices` argument during object creation # (this is the default); `:all`: all cells in `choices`; # `:max`: maximum number of cells allowed by the layout (can be larger # than `:all`); `:barcode`: the dark barcode elements; `:cal` the # calibration cells def overlay(what, where=:marked) return if not_registered @mim.overlay what, where end # Saves a copy of the source image after registration; # the output image will also contain any previously applied overlays. # # @param fname [String] the path/filename of the target image, including # the extension (`.jpg`, `.png`) def save(fname) return if not_registered @mim.save(fname, true) end # Saves a copy of the original image with overlays showing the crop areas # used to localize the registration marks and the detected registration # mark centers. # # @param fname [String] the path/filename of the target image, including # the extension (`.jpg`, `.png`) def save_registration(fname) @mim.save_registration fname end # ============================================================# private # # ============================================================# def not_registered unless valid? puts "---=={ Unregistered image. Reason: '#{@mim.status.inspect}' }==---" true end end end end # # write_raw(output_path_file_name) # # # # writes out a copy of the source image before registration; # # the output image will also contain any previously applied overlays # # if the argument is omitted, the image is created in-place, # # i.e. the original source image is overwritten. # def write_raw(fname=nil) # @mim.write(fname, false) # end # # Array of arrays of marked choices. # # # # @param questions [Fixnum, Range, or Array] look for the first n questions # # If the argument is omitted, all available choices are evaluated. # # @return [Array] The list of marked choices as an array (one element per # # question) of arrays (the indices of all marked choices for the question) # def mark_array(questions = nil) # return if not_registered # x = question_range questions # byebug # x.collect do |q| # [].tap do |cho| # (0...@grom.max_choices_per_question).each do |c| # cho << c if marked?(q, c) # end # end # end # end # # Array of arrays of the characters corresponding to marked choices. # # At this time, only the latin sequence 'A, B, C...' is supported. # # # # @param questions [Fixnum, Range, Array] same as for `mark_array` # # @return [Array] The list of marked choices as an array (one element per # # question) of arrays (the indices of all marked choices for the question) # def mark_char_array(questions = nil) # return if not_registered # question_range(questions).collect do |q| # [].tap do |cho| # (0...@grom.max_choices_per_question).each do |c| # cho << (65+c).chr if marked?(q, c) # end # end # end # end # # Array of logical arrays of marked choices # # # # @param [Fixnum, Range, Array] # def mark_logical_array(r = nil) # return if not_registered # question_range(r).collect do |q| # (0...@grom.max_choices_per_question).collect {|c| marked?(q, c)} # end # end # def question_range(r) # # TODO: help text: although not API, people need to know this! # if r.nil? # (0...@nitems.length) # elsif r.is_a? Fixnum # (0...r) # elsif r.is_a? Array # r # else # raise "Invalid argument" # end # end # def outline(cells) # return if not_registered # raise "Invalid ‘cells’ argument" unless cells.kind_of? Array # @mim.outline cells # end # def cross(cells) # return if not_registered # raise "Invalid ‘cells’ argument" unless cells.kind_of? Array # @mim.cross cells # end # def cross_marked # return if not_registered # @mim.cross mark_array # end # def highlight_all_choices # return if not_registered # @mim.highlight_all_choices # end # def highlight_marked # return if not_registered # @mim.highlight_cells mark_array # end # def highlight_barcode # return if not_registered # @mim.highlight_barcode barcode_string # end # def barcode_bit_string(i) # @mim.barcode_bit?(i) ? "1" : "0" # end