You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Python OpenCV的OMR答题卡扫描识别错误排查请求

排查你的OMR答题卡扫描器代码问题

我帮你仔细梳理了这段代码,找到了几个导致结果不符合预期的核心问题,咱们一步步拆解并修正:

1. 预处理步骤冗余且逻辑错误

你代码里重复调用了5次adaptiveThreshold,还多次重复调用Canny,甚至错误地把阈值处理的参数(THRESH_BINARY_INV | cv.THRESH_OTSU)传给了Canny——这完全是误用API,会直接破坏图像的特征,导致后续的圆检测完全不准。

比如这段完全多余且错误的代码:

# 重复5次自适应阈值,完全没必要,会让图像过度处理
img1 = cv.adaptiveThreshold(img1,255,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY,11,2)
img1 = cv.adaptiveThreshold(img1,255,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY,11,2)
...
# Canny的参数用法错误,这里混淆了阈值处理的参数
img1 = cv.Canny(img1,0,255,cv.THRESH_BINARY_INV | cv.THRESH_OTSU)[1]

2. 圆检测的参数与逻辑问题

你用的HoughCircles参数param2=50太高了,会过滤掉很多真实的填涂圆;而且你的循环for itr in range(2)处理左右两列,但answers的收集逻辑在循环外面,导致只保留了最后一列的圆数据,答案顺序完全混乱。

3. 答案判定逻辑完全不合理

你用circles[0][i][0]+circles[0][i][1]+circles[0][i][2](圆心坐标+半径的和)来判断哪个选项被选中,这个值和选项是否填涂没有任何关联,完全是错误的判定逻辑。正确的OMR判定应该是统计每个选项区域内的像素密度(填涂区域的像素值更暗/亮,取决于阈值方向),找到每个题目中填涂最密集的选项。


修正后的完整代码

我重构了核心逻辑,用更鲁棒的区域分割+像素统计方法替代圆检测,这也是OMR系统的常规做法:

import cv2 as cv
import numpy as np

# 读取图像
gray_img = cv.imread("H:/omr_test_01.png", 0)
color_img = cv.imread("H:/omr_test_01.png")
correct_answers = ['B','E','A','C','B']

# 裁剪答题卡有效区域(坐标和原代码保持一致)
crop_region = gray_img[157:975, 42:330]

# 统一预处理:高斯模糊降噪 + 自适应阈值反相(让填涂区域变为白色)
blurred = cv.GaussianBlur(crop_region, (5, 5), 0)
thresh = cv.adaptiveThreshold(blurred, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY_INV, 11, 2)

# 分割每个题目区域:5个题目,按高度均分
question_height_steps = np.linspace(0, crop_region.shape[0], 6, dtype=np.int32)
all_questions = []
for i in range(5):
    # 提取单个题目区域
    question_area = thresh[question_height_steps[i]:question_height_steps[i+1], :]
    # 分割左右两列选项:左列A-C,右列D-E
    left_col = question_area[:, 0:120]
    right_col = question_area[:, 167:288]
    
    # 分割左列3个选项
    left_option_steps = np.linspace(0, left_col.shape[0], 4, dtype=np.int32)
    options = []
    for j in range(3):
        options.append(left_col[left_option_steps[j]:left_option_steps[j+1], :])
    # 分割右列2个选项
    right_option_steps = np.linspace(0, right_col.shape[0], 3, dtype=np.int32)
    for j in range(2):
        options.append(right_col[right_option_steps[j]:right_option_steps[j+1], :])
    
    all_questions.append(options)

# 判定每个题目的选中选项:统计白色像素数,最多的就是填涂项
final_answers = []
for question_options in all_questions:
    # 统计每个选项的白色像素数量
    pixel_counts = [cv.countNonZero(opt) for opt in question_options]
    # 找到像素数最多的选项索引
    selected_idx = np.argmax(pixel_counts)
    # 映射为A-E
    final_answers.append(chr(ord('A') + selected_idx))

# 计算得分并展示
score = sum(1 for pred, true in zip(final_answers, correct_answers) if pred == true)
font = cv.FONT_HERSHEY_SIMPLEX
cv.putText(color_img, f'Marks:{score}/5', (20, 450), font, 2, (0, 0, 255), 3, cv.LINE_AA)

# 输出结果
print("检测到的答案:", final_answers)
print("得分:", score)
cv.imshow('OMR Result', color_img)
cv.waitKey(0)
cv.destroyAllWindows()

关键修正总结

  • 简化了预处理流程,用合理的模糊+自适应阈值反相,保留填涂区域的特征
  • 替换了不稳定的圆检测,改用区域分割+像素统计的方法,更适合OMR场景
  • 修正了答案判定逻辑,用填涂区域的像素数量作为判定依据,逻辑合理
  • 正确分割了每个题目和选项的区域,确保答案顺序和题目对应

运行这段修正后的代码,应该能得到你预期的['B', 'E', 'A', 'C', 'B']和满分5的结果。

内容的提问来源于stack exchange,提问作者Lohith

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:40:33