前言
? 文章開始,瞎說一點其他的東西,真的是很離譜,找了至少兩三個小時,就一個簡單的需求:
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)); // 設置隨機數種子// 定義數字圖像尺寸:30x50digitWidth = 30;digitHeight = 50;hog = cv::HOGDescriptor(cv::Size(digitWidth, digitHeight), // winSizecv::Size(10, 10), // blockSizecv::Size(5, 5), // blockStridecv::Size(5, 5), // cellSize9 // 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 1Mat 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中幫忙解釋一下就可以了。
接下來,就到了我們的經典環節:
參考
1、opencv 數字識別 數碼管