UNPKG

nfv

Version:

[Updated to use ndv package instead dv] FormVision is a node.js library for extracting data from scanned forms

82 lines (75 loc) 3.37 kB
dv = require 'ndv' binarize = require './filters/binarize' # Width of the quiet zone around a checkbox. QUIETZONE_WIDTH = 10 # Spline points for mapping score to confidence. LOWER_UNCERTAIN_SCORE = 0.02 # confidence 0 LOWER_CERTAIN_SCORE = 0.10 # confidence 1 UPPER_CERTAIN_SCORE = 1.5 # confidence 1 UPPER_UNCERTAIN_SCORE = 4 # confidence 0 # Detect potentially checked checkboxes. detectCandidates = (binarizedImage) -> candidates = [] for candidate in binarizedImage.dilate(3, 3).connectedComponents(8) if 0.5 < candidate.width / candidate.height < 5 and 10 < candidate.width < (binarizedImage.width / 3) candidates.push candidate else if candidate.width > (binarizedImage.width / 3) # This is probably a decorative box on the form. However, discarding it may conceal a mark # which just happens to touch its border. Open image section and scan for more candidates. for innerCandidate in binarizedImage.crop(candidate).erode(3, 3).dilate(5, 5).connectedComponents(8) if 0.5 < innerCandidate.width / innerCandidate.height < 2 and 20 < innerCandidate.width < (binarizedImage.width / 3) innerCandidate.x += candidate.x innerCandidate.y += candidate.y candidates.push innerCandidate return candidates # Classify potentially checked checkboxes by computing a weighted score for filling. scoreCandidate = (binarizedImage, candidate) -> # Crop candidate from image with quiet zone. overscannedCandidate = x: candidate.x - QUIETZONE_WIDTH y: candidate.y - QUIETZONE_WIDTH width: candidate.width + QUIETZONE_WIDTH * 2 height: candidate.height + QUIETZONE_WIDTH * 2 candidateImage = binarizedImage.crop overscannedCandidate # Aggressively close image, then apply weighting which favors the center of the checkbox imageWithGapsClosed = candidateImage.dilate(31, 31).erode(31, 31) distanceImage = imageWithGapsClosed.distanceFunction(8) imageWithWeightedPixels = distanceImage.and(candidateImage.invert().toGray()) score = 0 for value, index of imageWithWeightedPixels.histogram() score += index * value return score # Interpret score as result. This is heuristically measured. scoreToCheckState = (score) => lerp = (x, zeroAt, oneAt) -> (x - zeroAt) / (oneAt - zeroAt) checked = LOWER_UNCERTAIN_SCORE < score < UPPER_UNCERTAIN_SCORE confidence = switch when score < LOWER_UNCERTAIN_SCORE lerp(score, LOWER_UNCERTAIN_SCORE, 0) when LOWER_UNCERTAIN_SCORE <= score < LOWER_CERTAIN_SCORE lerp(score, LOWER_UNCERTAIN_SCORE, LOWER_CERTAIN_SCORE) when LOWER_CERTAIN_SCORE <= score < UPPER_CERTAIN_SCORE 1.0 when UPPER_CERTAIN_SCORE <= score < UPPER_UNCERTAIN_SCORE lerp(score, UPPER_UNCERTAIN_SCORE, UPPER_CERTAIN_SCORE) else 0.0 confidence = Math.round(confidence * 92 + 5) return [checked, confidence] # Find filled checkboxes in image. This process is pretty prone to words, thus it assumes all # words have been removed from the image. module.exports.findCheckboxes = (image) -> marks = [] clearedImage = new dv.Image image binarizedImage = binarize image candidates = detectCandidates binarizedImage for box in candidates score = scoreCandidate binarizedImage, box [checked, confidence] = scoreToCheckState score if confidence > 0 marks.push box: box checked: checked confidence: confidence clearedImage.clearBox box return [marks, clearedImage]