77 lines
2.0 KiB
Python
77 lines
2.0 KiB
Python
import sys
|
|
import cv2
|
|
import numpy as np
|
|
|
|
# Usage: python omr_read_sheet.py path_to_image
|
|
if len(sys.argv) < 2:
|
|
print("ERROR: No image path given")
|
|
sys.exit(1)
|
|
|
|
image_path = sys.argv[1]
|
|
|
|
# Read image
|
|
image = cv2.imread(image_path)
|
|
if image is None:
|
|
print("ERROR: Cannot read image")
|
|
sys.exit(1)
|
|
|
|
# Convert to gray + blur + threshold (invert so marks become white)
|
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
blur = cv2.GaussianBlur(gray, (5, 5), 0)
|
|
_, thresh = cv2.threshold(
|
|
blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU
|
|
)
|
|
|
|
# NOTE:
|
|
# We assume a simple grid layout.
|
|
# You will have to adjust these values for your printed template.
|
|
|
|
NUM_QUESTIONS = 20
|
|
NUM_OPTIONS = 4 # A,B,C,D
|
|
|
|
# These should be tuned based on your sheet
|
|
# Whole answer area as rectangle (x_start, y_start, x_end, y_end)
|
|
# For first test, take a scan and open in any image viewer to note pixel coords
|
|
ANS_X_START = 100
|
|
ANS_X_END = 1000
|
|
ANS_Y_START = 200
|
|
ANS_Y_END = 1400
|
|
|
|
height = ANS_Y_END - ANS_Y_START
|
|
width = ANS_X_END - ANS_X_START
|
|
|
|
row_height = height / NUM_QUESTIONS
|
|
col_width = width / NUM_OPTIONS
|
|
|
|
answers = []
|
|
|
|
for q in range(NUM_QUESTIONS):
|
|
row_top = int(ANS_Y_START + q * row_height)
|
|
row_bottom = int(ANS_Y_START + (q + 1) * row_height)
|
|
|
|
bubble_scores = []
|
|
for o in range(NUM_OPTIONS):
|
|
col_left = int(ANS_X_START + o * col_width)
|
|
col_right = int(ANS_X_START + (o + 1) * col_width)
|
|
|
|
# Region of interest for this bubble
|
|
roi = thresh[row_top:row_bottom, col_left:col_right]
|
|
|
|
# Score = number of white pixels (since inverted)
|
|
score = cv2.countNonZero(roi)
|
|
bubble_scores.append(score)
|
|
|
|
# Decide which option is filled
|
|
max_score = max(bubble_scores)
|
|
max_index = bubble_scores.index(max_score)
|
|
|
|
# threshold: if mark very light, treat as blank
|
|
if max_score < 200: # you can tune this number
|
|
# No proper mark detected
|
|
answers.append("")
|
|
else:
|
|
answers.append("ABCD"[max_index])
|
|
|
|
# Print as comma-separated string => PHP will read this
|
|
print(",".join(answers))
|