36459-vm/omr_read_sheet.py
2026-05-27 14:29:58 +05:30

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))