基于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
相关产品推荐
相关产品推荐

