当前位置: 首页 > news >正文

使用 C++ 和 OpenCV 构建智能答题卡识别系统

使用 C++ 和 OpenCV 构建智能答题卡识别系统 📝

本文将引导你如何使用 C++ 和强大的计算机视觉库 OpenCV,从零开始创建一个可以自动批改选择题答题卡的程序。我们将涵盖从图像预处理、轮廓定位到最终答案判定的完整流程。


核心技术与原理 🧠

光学标记识别 (OMR) 的核心思想是利用计算机视觉技术,在一张扫描的图像中定位并识别出被标记(如填涂)的区域。整个流程可以分解为以下几个关键步骤:

  1. 图像预处理: 将输入的彩色或灰度图像转换为二值图像,使其更容易被程序分析。
  2. 轮廓检测: 找到图像中的关键轮廓,首先是整个答题卡的轮廓,然后是每个选项的轮廓(通常是圆形或矩形)。
  3. 透视变换: 如果答题卡图像是倾斜的,我们需要将其校正为一个标准的“鸟瞰图”,以确保后续处理的准确性。
  4. 选项定位与识别: 在校正后的图像上,定位每个选项(A, B, C, D)的位置。
  5. 答案判定: 通过计算每个选项区域内的非零像素(黑色像素)数量,来判断哪个选项被填涂。
  6. 自动评分: 将识别出的学生答案与标准答案进行比对,计算总分。

<center>一个简化的 OMR 处理流程图。</center>


步骤一:环境配置 🛠️

在开始之前,请确保你的开发环境中已经安装了 C++ 编译器 (如 G++) 和 OpenCV 库。

在 Ubuntu/Debian 上安装 OpenCV:

sudo apt-get update
sudo apt-get install build-essential cmake libopencv-dev

步骤二:图像预处理与轮廓定位

我们的首要任务是找到答题卡在图像中的准确位置。

1. 加载与预处理

我们从加载图像开始,然后进行高斯模糊以减少噪声,再使用 Canny 算法进行边缘检测,最后通过膨胀和腐蚀操作来连接断开的边缘。

关键代码片段 (main.cpp):

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>using namespace cv;
using namespace std;// 用于对轮廓按面积大小进行排序的辅助函数
bool compareContourAreas(const vector<Point>& contour1, const vector<Point>& contour2) {return contourArea(contour1) > contourArea(contour2);
}int main() {string image_path = "answer_sheet.jpg"; // 你的答题卡图片路径Mat image = imread(image_path);if (image.empty()) {cout << "Could not read the image: " << image_path << endl;return 1;}Mat gray, blurred, edged;cvtColor(image, gray, COLOR_BGR2GRAY);GaussianBlur(gray, blurred, Size(5, 5), 0);Canny(blurred, edged, 75, 200);// 显示边缘检测结果 (可选)// imshow("Edged", edged);// waitKey(0);// ... 后续代码
}

2. 寻找答题卡轮廓

在边缘图像上,我们可以寻找轮廓。答题卡通常是图像中最大的矩形轮廓。

// ... 接上文
vector<vector<Point>> contours;
findContours(edged.clone(), contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);// 按面积对轮廓进行排序
sort(contours.begin(), contours.end(), compareContourAreas);// 假设最大的轮廓就是我们的答题卡
vector<Point> paperContour;
for (const auto& c : contours) {double peri = arcLength(c, true);vector<Point> approx;approxPolyDP(c, approx, 0.02 * peri, true);// 如果轮廓有4个顶点,我们认为它就是答题卡if (approx.size() == 4) {paperContour = approx;break;}
}

步骤三:透视变换(图像校正)📐

找到四个顶点后,我们可以对图像进行透视变换,得到一个完美的矩形俯视图。

关键代码片段:

#include <algorithm> // for std::sort// 对四个顶点进行排序:左上, 右上, 右下, 左下
Point2f sortPoints(const vector<Point>& pts) {// ... 实现排序逻辑 ...// 通常基于 x+y 和 x-y 的值来排序// 返回一个包含四个有序点的 Point2f 向量
}// ... 接上文
if (paperContour.empty()) {cout << "Could not find paper contour." << endl;return 1;
}// 获取四个顶点并排序
Point2f src_pts[] = { /* 排序后的 paperContour 顶点 */ };
Point2f dst_pts[] = {{0.0f, 0.0f}, {500.0f, 0.0f}, {500.0f, 700.0f}, {0.0f, 700.0f}}; // 目标图像尺寸Mat transformMatrix = getPerspectiveTransform(src_pts, dst_pts);
Mat warped;
warpPerspective(image, warped, transformMatrix, Size(500, 700));// 显示校正后的图像 (可选)
// imshow("Warped", warped);
// waitKey(0);

步骤四:选项识别与评分 ✅

现在我们在校正后的、标准化的 warped 图像上工作。

1. 定位并分析选项气泡

首先,我们将校正后的图像再次进行二值化处理。

Mat warpedGray, thresh;
cvtColor(warped, warpedGray, COLOR_BGR2GRAY);
threshold(warpedGray, thresh, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

接下来,我们再次寻找轮廓,但这次是在二值化的 thresh 图像上。这些轮廓将代表所有的选项气泡。

vector<vector<Point>> bubbleContours;
findContours(thresh.clone(), bubbleContours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);vector<vector<Point>> questionBubbles;
// 遍历所有轮廓,筛选出符合条件的选项气泡(比如基于宽高比和面积)
for (const auto& c : bubbleContours) {Rect r = boundingRect(c);float ar = r.width / (float)r.height;if (r.width >= 20 && r.height >= 20 && ar >= 0.9 && ar <= 1.1) {questionBubbles.push_back(c);}
}

2. 将气泡分组并判定答案

我们将所有识别出的气泡按其 y 坐标排序,然后按 x 坐标排序,这样就可以将它们与具体的问题和选项对应起来。假设每行有 5 个选项气泡(例如,一个题号加 A,B,C,D)。

// 按y坐标对气泡进行排序
sort(questionBubbles.begin(), questionBubbles.end(), [](const vector<Point>& a, const vector<Point>& b) {return boundingRect(a).y < boundingRect(b).y;
});map<int, int> correct_answers; // <题号, 正确答案索引(0-3)>
correct_answers[0] = 1; // 第1题答案是 B
correct_answers[1] = 3; // 第2题答案是 D
// ... 其他答案int total_correct = 0;
// 每5个气泡为一组进行处理
for (size_t i = 0; i < questionBubbles.size(); i += 5) {// 对当前行的5个气泡按x坐标排序vector<vector<Point>> row(questionBubbles.begin() + i, questionBubbles.begin() + i + 5);sort(row.begin(), row.end(), [](const vector<Point>& a, const vector<Point>& b) {return boundingRect(a).x < boundingRect(b).x;});int bubbled_index = -1;int max_filled = 0;// 遍历当前行的每个选项 (A,B,C,D),跳过第一个(题号)for (size_t j = 1; j < row.size(); ++j) {Mat mask = Mat::zeros(thresh.size(), CV_8UC1);drawContours(mask, row, j, Scalar(255), -1);Mat masked_bubble;bitwise_and(thresh, thresh, masked_bubble, mask);int filled_pixels = countNonZero(masked_bubble);if (filled_pixels > max_filled) {max_filled = filled_pixels;bubbled_index = j - 1; // 索引为 0-3}}// 判定对错int question_num = i / 5;if (correct_answers[question_num] == bubbled_index) {total_correct++;}
}cout << "Total Correct: " << total_correct << endl;

总结与提升 🚀

我们成功地使用 C++ 和 OpenCV 实现了一个基本的答题卡自动评分系统。这个项目完美地展示了计算机视觉在自动化任务中的强大能力。

可以进一步优化的方向:

  • 鲁棒性: 提高对不同光照条件、纸张质量和填涂工具(铅笔、钢笔)的适应性。
  • GUI 界面: 使用 Qt 或其他 GUI 框架为程序创建一个用户友好的界面,允许用户上传图片并查看结果。
  • 多选题和判断题: 扩展逻辑以支持多种题型。
  • 性能优化: 对计算密集型步骤进行优化,以更快地处理图像。

希望这个教程能帮助你开启计算机视觉的学习之旅!

相关文章:

  • 编程学习网站大全(C++/OpenCV/QT方向)—— 资源导航与深度评测
  • 【Lua热更新知识】学习三 XLua学习
  • JavaEE-SpringBoot
  • JavaEE-Maven
  • Leetcode-11 2 的幂
  • 解决华为云服务器无法ping通github问题
  • 智能体商业化:创建-接入-封装成小程序/网站/H5
  • 第二部分-静态路由实验
  • 聊天室项目多进程纯C版
  • 公司网络变差的解决方法(固定IP地址冲突)
  • 关于界面存在AB测试后UI刷新空白的问题
  • Redis:set类型和zset类型
  • 汽车制造通信革新:网关模块让EtherCAT成功对接CCLINK
  • gitlab相关操作
  • Redis GEO 底层实现(结合源码分析)
  • Redis的主从复制底层实现
  • 【编译工具】(调试)Chrome DevTools + Postman:调试组合如何让我的开发效率提升400%?
  • Guava常用工具类使用教程
  • 《Redis》持久化
  • Oracle线上故障问题解决
  • 网站开发后台数据库怎么搞/长春网站公司哪家好
  • layui做的网站/自己的网站怎么在百度上面推广
  • weui.css做网站/优化大师win10下载
  • 专业的网站设计/推广普通话手抄报模板可打印
  • 网站个人备案做论坛/营销型网站建设流程
  • 网站开发项目的规划与设计文档/韩国vs加纳分析比分