识别的内容一般是试题模板的题号和手写的答题答案(数字)
明白了,你的场景更加聚焦:识别试题模板中的题号 + 识别手写的数字答案。这是一个相对明确且可落地的任务,我为你提供一个精简实用的方案。
一、技术选型建议
| 子任务 | 推荐方案 | 理由 |
|---|---|---|
| 题号识别 | PaddleOCR / Tesseract | 题号是印刷体,OCR识别准确率高 |
| 手写数字识别 | CNN分类器(轻量级)或 PaddleOCR数字版 | 手写数字是有限类别(0-9,可能含小数点/负号),分类模型更精准 |
| 区域定位 | 模板匹配 或 YOLO检测 | 如果试卷版式固定,用模板匹配即可;版式多变则用YOLO |
二、精简方案:模板匹配 + 手写数字CNN
如果你的试卷模板固定(同一份试卷多次使用,只是学生手写答案不同),这是最高效的方式:
步骤1:建立模板坐标映射
# 手动标注一次模板中各题号的坐标区域
template_regions = {
1: {"bbox": [x1, y1, x2, y2], "type": "handwritten"},
2: {"bbox": [x1, y1, x2, y2], "type": "handwritten"},
3: {"bbox": [x1, y1, x2, y2], "type": "handwritten"},
# 题号本身的位置(如果需要识别题号是否匹配)
"question_numbers": {
1: [x1, y1, x2, y2],
2: [x1, y1, x2, y2],
}
}
步骤2:题号识别(验证模板对齐)
import cv2
import pytesseract
def recognize_question_number(img, bbox):
x1, y1, x2, y2 = bbox
roi = img[y1:y2, x1:x2]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
# 二值化增强
_, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV)
# OCR识别题号
text = pytesseract.image_to_string(thresh, config='--psm 7 -c tessedit_char_whitelist=0123456789')
return text.strip()
步骤3:手写数字识别(轻量CNN模型)
训练一个针对手写数字的CNN分类器(使用MNIST或自有数据):
import tensorflow as tf
from tensorflow import keras
# 构建简单CNN模型
model = keras.Sequential([
keras.layers.Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1)),
keras.layers.MaxPooling2D(2,2),
keras.layers.Conv2D(64, (3,3), activation='relu'),
keras.layers.MaxPooling2D(2,2),
keras.layers.Flatten(),
keras.layers.Dense(128, activation='relu'),
keras.layers.Dense(10, activation='softmax') # 0-9十个类别
])
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
# 使用MNIST数据集训练
mnist = keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
x_train = x_train.reshape(-1,28,28,1)
x_test = x_test.reshape(-1,28,28,1)
model.fit(x_train, y_train, epochs=5)
model.save('handwritten_digit_model.h5')
步骤4:集成识别流程
def process_answer_sheet(image_path, template_regions):
img = cv2.imread(image_path)
results = {}
# 1. 可选:验证题号是否匹配(确保试卷对齐)
for q_id, bbox in template_regions["question_numbers"].items():
recognized_num = recognize_question_number(img, bbox)
if recognized_num != str(q_id):
print(f"警告:题号{q_id}识别为{recognized_num},可能存在对齐偏差")
# 2. 识别手写答案区域
for q_id, region in template_regions.items():
if q_id == "question_numbers":
continue
x1, y1, x2, y2 = region["bbox"]
roi = img[y1:y2, x1:x2]
# 预处理手写区域
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# 提取单个数字(如有多个数字)
digits = extract_digits(binary) # 通过轮廓检测切分数字
# 识别每个数字
answer = ""
for digit_img in digits:
digit_img = cv2.resize(digit_img, (28,28))
digit_img = digit_img.reshape(1,28,28,1) / 255.0
pred = model.predict(digit_img, verbose=0)
answer += str(np.argmax(pred))
results[q_id] = answer
return results
def extract_digits(binary_img):
"""通过轮廓检测切分单个数字"""
contours, _ = cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
digits = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
if w * h > 100: # 过滤噪声
digit = binary_img[y:y+h, x:x+w]
digits.append(digit)
# 按x坐标排序
digits.sort(key=lambda d: cv2.boundingRect(d)[0])
return digits
三、替代方案:PaddleOCR直接识别(更简单)
如果不想训练CNN模型,PaddleOCR本身对数字识别效果也很好:
from paddleocr import PaddleOCR
ocr = PaddleOCR(use_angle_cls=False, lang='en') # 英文模式,数字识别更好
def recognize_digit_region(roi):
result = ocr.ocr(roi, cls=False)
if result and result[0]:
text = result[0][0][1][0] # 提取识别的文本
# 只保留数字
digits = ''.join([c for c in text if c.isdigit()])
return digits
return ""
优点:无需训练,开箱即用
缺点:对于潦草手写数字,准确率可能不如定制CNN
四、针对手写数字识别的优化技巧
| 问题 | 优化方法 |
|---|---|
| 数字粘连 | 使用形态学腐蚀操作分离粘连数字 |
| 数字倾斜 | 计算最小外接矩形,进行仿射变换校正 |
| 噪声干扰 | 使用中值滤波去噪,设置最小轮廓面积过滤小噪点 |
| 多位数识别 | 轮廓检测后按x坐标排序,逐个识别后拼接 |
| 识别结果纠错 | 结合上下文(如答案应在0-100范围内)进行后处理 |
五、数据收集建议
如果选择训练自己的CNN模型,建议收集真实场景的手写数字:
- 收集100-200张真实答题卡图片
- 切出手写数字区域,保存为单张图片
- 人工标注每个数字的真实值
- 数据增强:旋转±15°,缩放,添加轻微噪声
- 用增强后的数据(可扩充到1000+张)微调MNIST预训练模型
六、完整工作流总结
答题卡图像
↓
透视变换校正(可选)
↓
模板匹配/坐标映射
↓
┌─────────────────┐
│ 题号区域OCR验证 │ → 确认试卷对齐
└─────────────────┘
↓
┌─────────────────┐
│ 手写答案区域 │
│ - 预处理二值化 │
│ - 轮廓检测切分 │
│ - CNN逐数字识别 │
└─────────────────┘
↓
结果拼接与校验
↓
输出 {题号: 答案}
你的试卷模板是完全固定的(每次布局一样),还是会有变化?如果是完全固定的,用模板坐标映射是最省事的,我可以帮你写一个完整的识别脚本。如果是多变的,可能需要加入YOLO做区域检测,但工作量会大一些。
本文来自博客园,作者:AceFenix,转载请注明原文链接:https://www.cnblogs.com/ukzq/p/19758997

浙公网安备 33010602011771号