前言
文章开始,瞎说一点其他的东西,真的是很离谱,找了至少两三个小时,就一个简单的需求:
1、利用OpenCV 在Windows进行抓图
2、利用OpenCV 进行连续数字的检测。
3、使用C++,Qt
3、将检测的结果显示出来
就这么简单的需求,结果网上找了各种版本硬是找不到,要是代码可能没啥问题,但是运行不了,你这运行不了,我怎么知道你到底能不能用,我代码调半天能用了,结果你跟我说最后效果不好,为啥呢?
因为图像识别这种东西,很取决于你的外部环境的,一定你的外部环境变量,你的数字的背景啥的变了,那么你的代码肯定就要做相应的调整,这种不像深度学习能够自己学习的,实际只能靠你自己一步一步的去调试验证效果怎么样,最终得到适合你的。
所以,我下面会给出我这个程序的打包的可直接验证效果的版本,你如果不是一个想调代码的人,或是你不是一个有耐心的人,或者你跟我的识别环境不一致, 那么我估计我的代码你也用不了,也不必去下载了。可继续找下一个了。
但如果你说,只要我代码能让你运行起来,那么你就能够花精力把它调出来,实在不行,你让AI 帮你把它调出来,这都是没问题的,因为目前的运行方式很简单,只要你确保环境跟我一致,基本就没啥问题。
环境:
Windows 10
Qt 12.8 MSVC2015
OpenCV 4.5.5(我带的这个opencv 是用VS2015编译出来的,如果没有MSVC2015 ,那么就只能靠你自己去下载一个MinGW 之类的,或是你自己对应版本的OpenCV了)
运行现象:
因为这个是采用那个SVM首先进行模型训练的,我的模型,每个数字只放了一张或是两张,训练量太小了,出来的效果就比较不好,而且,若要进行这个识别,肯定要注意以下几点:
1、摄像头与数字的距离一定是固定的,然后外部光源也是固定的,不能说一会亮一会不亮的,这是不合理的。
2、需要拍摄更多组的照片以及数字来进行训练,甚至该模型可以采用自训练的方式,来进行优化,但我这个版本就没有做到这个点了,这个点有需要的可以来进行优化。后面对这个方面如果我有进行优化,我会来跟贴的。
3、可以对捕抓到的数字再进行一些处理,增大SVM训练的量,这样可能效果就会稳定很多了,我上面这个摄像头是手拿着的,所以会一直飘,我觉得应该也是比较正常的,毕竟只用了一天时间,搞出了这个demo,那效果肯定会有差强人意的地方。
可运行程序
通过网盘分享的文件:NumberRecognitionTool.zip
链接: https://pan.baidu.com/s/1hr8VqU2x17pIQ561hy8nQw?pwd=1111 提取码: 1111
我有试了一下,是可以运行的,如果不能运行可以留言下,我看下是什么原因。
如下,我会把我的核心代码给贴上去,如果有环境的,直接改一改运行就可以了。如果还觉得有点懒的话,可以直接下载我上传的资源文件,那里面我会把dll,啥的,都给你打包好,直接运行即可。不过要花费点积分就是了,如果又没有积分的话,可以加我qq,或者私信我,我可以直接发你。qq在主页有。
https://download.csdn.net/download/qq_43211060/90468759?spm=1001.2014.3001.5501
我也下载了好一些往上的资源,我也不知道有没有用,反正我没用上,如果有需要的话,也可以一起发给你们。希望能对你们有帮助。
正文
一、代码
处理的核心代码:
void CDataRecognitionMgr::InitSVM()
{
srand((unsigned)time(0)); // 设置随机数种子
// 定义数字图像尺寸:30x50
digitWidth = 30;
digitHeight = 50;
hog = cv::HOGDescriptor(
cv::Size(digitWidth, digitHeight), // winSize
cv::Size(10, 10), // blockSize
cv::Size(5, 5), // blockStride
cv::Size(5, 5), // cellSize
9 // nbins
);
descriptorSize = (int)hog.getDescriptorSize();
// ==========================
// 1. 从外部加载模板图像,并生成数据增强后的训练样本
// ==========================
vector<Mat> trainImages;
vector<int> trainLabels;
const int numAugmentations = 100; // 每个数字至少生成 100 个训练样本
for (int digit = 0; digit < 10; digit++) {
// 模板图像存放在指定目录下(根据需要调整路径与图片格式)
string folderPattern = "./img/Mod/" + to_string(digit) + "/*.png";
vector<String> files;
glob(folderPattern, files, false);
if (files.empty()) {
cout << "未找到数字 " << digit << " 的模板图片,请检查文件夹: " << folderPattern << endl;
continue;
}
// 生成数据增强样本
for (int i = 0; i < numAugmentations; i++) {
// 随机选择一个模板图片
int idx = rand() % files.size();
Mat img = imread(files[idx], IMREAD_GRAYSCALE);
if (img.empty()) {
cout << "加载图片失败: " << files[idx] << endl;
continue;
}
// 对模板图像进行增强处理
Mat augImg = augmentImage(img, digitWidth, digitHeight);
trainImages.push_back(augImg);
trainLabels.push_back(digit);
}
}
int totalSamples = (int)trainImages.size();
if (totalSamples == 0) {
cout << "未生成任何训练样本,请检查模板图像路径与数据增强处理!" << endl;
return;
}
cout << "生成的训练样本总数: " << totalSamples << endl;
// ==========================
// 2. 构造训练数据矩阵
// ==========================
Mat trainingFeatures(totalSamples, descriptorSize, CV_32F);
Mat trainingLabelsMat(totalSamples, 1, CV_32S);
for (int i = 0; i < totalSamples; i++) {
vector<float> descriptors;
hog.compute(trainImages[i], descriptors);
for (int j = 0; j < descriptorSize; j++) {
trainingFeatures.at<float>(i, j) = descriptors[j];
}
trainingLabelsMat.at<int>(i, 0) = trainLabels[i];
}
// ==========================
// 3. 使用 SVM(RBF 核)训练分类器
// ==========================
svm = SVM::create();
svm->setType(SVM::C_SVC);
svm->setKernel(SVM::RBF);
svm->setC(2.0);
svm->setGamma(0.005);
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 1000, 1e-6));
cout << "开始训练 SVM..." << endl;
svm->train(trainingFeatures, ml::ROW_SAMPLE, trainingLabelsMat);
cout << "SVM 训练完成。" << endl;
}
void CDataRecognitionMgr::HandlerImage(const QImage &_oImg)
{
#if 1
Mat mat = _ImageToMat(_oImg);
Mat matGray;
cvtColor(mat, matGray, COLOR_BGR2GRAY);
Mat testImgThresh;
threshold(matGray, testImgThresh, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
// imshow("testImgThresh",testImgThresh);
Mat struct1;
struct1=getStructuringElement(0,Size(2,2));//矩形结构元素
Mat erodeSrc;//存放腐蚀后的图像
erode(testImgThresh, erodeSrc,struct1);
Mat morphKernel = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(erodeSrc, testImgThresh, MORPH_OPEN, morphKernel);
morphologyEx(erodeSrc, testImgThresh, MORPH_CLOSE, morphKernel);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(testImgThresh, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
qDebug() << "---> contours:"<<contours.size();
if (contours.size() < 10)
{
return;
}
vector<Rect> digitROIs;
for (const auto& contour : contours) {
Rect bbox = boundingRect(contour);
// 根据尺寸过滤噪声与无效区域
qDebug() << "---> bbox.width:"<<bbox.width<<";bbox.height:"<<bbox.height;
if (bbox.width > 20 && bbox.height > 20 && bbox.width < 200 && bbox.height < 200) {
digitROIs.push_back(bbox);
}
}
// 3. 分割粘连区域
int avgWidth = 90; // 假设单个数字的平均宽度,可根据实际情况调整
for (size_t i = 0; i < digitROIs.size(); i++) {
if (digitROIs[i].width > 1.5 * avgWidth) { // 判断是否为粘连区域
// 提取粘连区域的二值图像
Mat roiImg = testImgThresh(digitROIs[i]);
// 计算垂直投影
Mat projection(1, roiImg.cols, CV_32F);
reduce(roiImg, projection, 0, REDUCE_SUM, CV_32F);
// 寻找分割点(局部最小值)
int splitPos = -1;
float minVal = numeric_limits<float>::max();
for (int j = 1; j < projection.cols - 1; j++) {
float val = projection.at<float>(0, j);
if (val < projection.at<float>(0, j - 1) && val < projection.at<float>(0, j + 1) && val < minVal) {
minVal = val;
splitPos = j;
}
}
// 根据分割点分割边界框
if (splitPos > 0) {
Rect leftROI(digitROIs[i].x, digitROIs[i].y, splitPos, digitROIs[i].height);
Rect rightROI(digitROIs[i].x + splitPos, digitROIs[i].y, digitROIs[i].width - splitPos, digitROIs[i].height);
// 替换原始粘连区域
digitROIs.erase(digitROIs.begin() + i);
digitROIs.insert(digitROIs.begin() + i, leftROI);
digitROIs.insert(digitROIs.begin() + i + 1, rightROI);
i--; // 重新检查新插入的区域
}
}
}
// 按 x 坐标排序(从左到右)
sort(digitROIs.begin(), digitROIs.end(), [](const Rect& a, const Rect& b) {
return a.x < b.x;
});
cout << "检测到的轮廓数量: " << digitROIs.size() << endl;
for (const auto& roi : digitROIs) {
cout << "边界框: " << roi << endl;
}
string recognized = "";
for (const auto& roi : digitROIs) {
Mat digitROI = testImgThresh(roi);
Mat digitResized;
resize(digitROI, digitResized, Size(digitWidth, digitHeight));
vector<float> descriptors;
hog.compute(digitResized, descriptors);
Mat sample(1, descriptorSize, CV_32F);
for (int j = 0; j < descriptorSize; j++) {
sample.at<float>(0, j) = descriptors[j];
}
int predicted = (int)svm->predict(sample);
recognized.push_back('0' + predicted);
}
QString str = QString::fromStdString(recognized);
emit SIGNAL_DATA_NUM(str);
cout << "识别结果1: " << recognized << endl;
#endif
}
InitSVM
基本就是训练的标准流程了,那么比较核心的还是下面这个函数,这个函数HandlerImage
可能就需要你进行一些调整:
首先先进行基本的图像处理,由于某些打印的会出现说数字粘在一起的情况,那么就得采用这个分割粘连区域进行局部处理,才能分割出来,我这份代码试了两种情况,都还可以,一个是会粘着的,一个是不会粘着的。
其他你需要更详细的,可以将这两个函数放到AI中帮忙解释一下就可以了。
接下来,就到了我们的经典环节: