惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

W
WeLiveSecurity
博客园 - 【当耐特】
Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
IT之家
IT之家
Cloudbric
Cloudbric
The Register - Security
The Register - Security
小众软件
小众软件
PCI Perspectives
PCI Perspectives
G
Google Developers Blog
AI
AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News
Google DeepMind News
Google DeepMind News
宝玉的分享
宝玉的分享
Recent Commits to openclaw:main
Recent Commits to openclaw:main
量子位
TaoSecurity Blog
TaoSecurity Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
F
Full Disclosure
N
Netflix TechBlog - Medium
博客园_首页
Last Week in AI
Last Week in AI
A
Arctic Wolf
B
Blog RSS Feed
J
Java Code Geeks
C
Cybersecurity and Infrastructure Security Agency CISA
I
InfoQ
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
NISL@THU
NISL@THU
MyScale Blog
MyScale Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Jina AI
Jina AI
有赞技术团队
有赞技术团队
S
Schneier on Security
L
Lohrmann on Cybersecurity
P
Privacy & Cybersecurity Law Blog
T
Threat Research - Cisco Blogs
P
Palo Alto Networks Blog
S
Security @ Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
Security Latest
Security Latest
Vercel News
Vercel News
博客园 - 司徒正美
Webroot Blog
Webroot Blog
Hacker News: Ask HN
Hacker News: Ask HN
A
About on SuperTechFans

博客园 - ace--碳水化合物

English1 Tidio 官方帮助中心 ERP 相关系统介绍 蝉翼定理 选配法 利润最大化公式 写在失业的一段话 排列组合-组合 在线简历 概率论与数理统计学习章节 绝对值 比例五大定理 算术平均值与几何平均值 等价符号 公因数与最大公因数 容斥原理 英语2026 机器学习,深度学习,强化学习 分别是什么以及区别是什么 深度学习 监督学习 FAQ IVR TTS选择 k 近邻算法 kaldi 平稳核函数(stationary kernel) Karush-Kuhn-Tucker 条件 凸二次规划(convex quadratic programming) FunASR+FreeSwitch做坐席客服系统记录 freeswitch原理图 客服系统,第三方平台,对话转录音,这些可以不依赖第三方做么 STUN服务器 Gun.js原理 游戏升级记 10 OBB 边界问题 关于yolo26是否可以通过结合java开发 opencv和yolo是一回事情吗 pytorch TorchVision - ace--碳水化合物 PyTorch 和 FashionMNIST 的关系 java record 游戏升级记 9 VibeVoice实现90分钟、多角色播客生成,拓展语音合成新边界 同 WiFi 下用 Claude Code 控制另一台 Windows 电脑 Numpy 1 游戏升级记 8 开源claudecode前端 github star 9k+ 向量数据库skill 游戏升级记 7 游戏升级记 6 关系型数据库,向量数据库,ES,缓存,列式数据库,时序数据库,图数据库等的区别和共同点列举table 多Agent可视化 Agent设计模式 中文 Hermes+Obsidian+LLM wkii,构建AI知识库 Claude Code 前端工程泄露代码开源 游戏升级记 5 ai记忆 Rag 1 游戏升级记 4 游戏升级记 3 游戏升级记 2 AI学习路线 关于hermes agent安装 1 游戏升级记 1 代码迷踪 十三 代码迷踪 十二 ai智能体工程 Rust vs Go 微信小程序实名认证 怎样制作一个街机游戏 跟Claude code说 深入思考 它会思考更深入. googlebusiness profile设置统一白名单的操作,要弹窗模式的 该怎样利用ollama自己训练一个编程大模型 中华AI智能体编程一站式基站构想 打包网站到exe和app opencv怎么训练 在线SaaS系统做接口版本滚动更新 stripe相关支付流程 流程示意图 error: linker `link.exe` not found | = note: program not found - rust 给我一份关于stripe平台的使用开发说明 c++学习记20260219 奥数-平面几何经典定理 奥数-组合数学 奥数-几何 奥数-代数 奥数-数论
识别的内容一般是试题模板的题号和手写的答题答案(数字)
ace--碳水化合物 · 2026-03-23 · via 博客园 - ace--碳水化合物

明白了,你的场景更加聚焦:识别试题模板中的题号 + 识别手写的数字答案。这是一个相对明确且可落地的任务,我为你提供一个精简实用的方案。

一、技术选型建议

题号识别 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模型,建议收集真实场景的手写数字:

  1. 收集100-200张真实答题卡图片
  2. 切出手写数字区域,保存为单张图片
  3. 人工标注每个数字的真实值
  4. 数据增强:旋转±15°,缩放,添加轻微噪声
  5. 用增强后的数据(可扩充到1000+张)微调MNIST预训练模型

六、完整工作流总结

答题卡图像
    ↓
透视变换校正(可选)
    ↓
模板匹配/坐标映射
    ↓
┌─────────────────┐
│ 题号区域OCR验证 │ → 确认试卷对齐
└─────────────────┘
    ↓
┌─────────────────┐
│ 手写答案区域    │
│ - 预处理二值化  │
│ - 轮廓检测切分  │
│ - CNN逐数字识别 │
└─────────────────┘
    ↓
结果拼接与校验
    ↓
输出 {题号: 答案}

你的试卷模板是完全固定的(每次布局一样),还是会有变化?如果是完全固定的,用模板坐标映射是最省事的,我可以帮你写一个完整的识别脚本。如果是多变的,可能需要加入YOLO做区域检测,但工作量会大一些。