OpenCV實(shí)現(xiàn)車牌字符分割(C++)
之前的車牌定位中已經(jīng)獲取到了車牌的位置,并且對(duì)車牌進(jìn)行了提取。我們最終的目的是進(jìn)行車牌識(shí)別,在這之前需要將字符進(jìn)行分割,方便對(duì)每一個(gè)字符進(jìn)行識(shí)別,最后將其拼接后便是完整的車牌號(hào)碼。關(guān)于車牌定位可以看這篇文章: OpenCV車牌定位(C++) ,本文使用的圖片也是來(lái)自這里。
先來(lái)看一看原圖:
最左邊的漢字本來(lái)是 滬,截取時(shí)只獲得了右邊一點(diǎn)點(diǎn)的部分,這與原圖和獲取方法都有關(guān),對(duì)于 川、滬… 這一類左右分開的字會(huì)經(jīng)常發(fā)生這類問(wèn)題,對(duì)方法進(jìn)行優(yōu)化后可以解決,這里暫時(shí)不進(jìn)行討論。
后面的字都是完整的,字符分割的過(guò)程不會(huì)受影響。首先來(lái)一波常規(guī)操作,為了更方便處理,將其變成灰度圖片:
分割的方法不止一種,最簡(jiǎn)單的就是多加點(diǎn)人工成分,按照大致寬度再微調(diào)進(jìn)行截取,但是這樣看似最快其實(shí)成本最高,只適用于單一的圖片,因此這種容錯(cuò)低且不夠自動(dòng)的方法就不考慮了。
目前我使用了兩種不同的方法,一種是進(jìn)行邊緣檢測(cè)再檢測(cè)輪廓,根據(jù)字符的輪廓特點(diǎn)篩選出字符;另一種就是像素值判斷,主要根據(jù)像素?cái)?shù)量使用水平映射截取寬度,垂直映射因?yàn)楦叨然疽恢戮筒恍枰?,方法于水平映射一樣?/p>
兩種方法我都寫在后面,根據(jù)需要自行復(fù)制。如果要使用像素值進(jìn)行判斷的話,就需要再將灰度圖轉(zhuǎn)換成二值化圖片,使用閾值分割就行了。若使用第一種用輪廓分割的方法,灰度圖和二值化圖片都可以,結(jié)果沒什么區(qū)別。
檢測(cè)輪廓進(jìn)行分割
邊緣檢測(cè)
對(duì)圖像進(jìn)行邊緣檢測(cè),這里采用的是 Canny 邊緣檢測(cè),處理后的結(jié)果如下:
可以看到每個(gè)字的邊緣都被描繪出來(lái)了,接下來(lái)就將每個(gè)字的輪廓獲取出來(lái)。
檢測(cè)輪廓
直接使用 findContours() 將所有輪廓提取出來(lái),再將其在原圖中畫出來(lái)看看效果:
可以看到不僅僅是每個(gè)字被框出來(lái)了,還有內(nèi)部以及圖像中表現(xiàn)特殊部分的輪廓也有,接下來(lái)我們就根據(jù)每個(gè)字的大致大小篩選出我們想要的結(jié)果:
這樣看起來(lái)是不是就成功了,然后根據(jù)輪廓位置將每個(gè)字提取出來(lái)就行了,不過(guò)在這里每個(gè)輪廓的前后順序不一定是圖像中的位置,這里我使用每個(gè)輪廓左上角橫坐標(biāo) x 的大小來(lái)排序。
完整代碼:
#include <iostream> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc.hpp> #include <opencv2/imgproc/types_c.h> #include <map> using namespace std; using namespace cv; int main() { Mat img = imread("number.jpg"); Mat gray_img; // 生成灰度圖像 cvtColor(img, gray_img, CV_BGR2GRAY); // 高斯模糊 Mat img_gau; GaussianBlur(gray_img, img_gau, Size(3, 3), 0, 0); // 閾值分割 Mat img_seg; threshold(img_gau, img_seg, 0, 255, THRESH_BINARY + THRESH_OTSU); // 邊緣檢測(cè),提取輪廓 Mat img_canny; Canny(img_seg, img_canny, 200, 100); vector<vector<Point>> contours; vector<Vec4i> hierarchy; findContours(img_canny, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE, Point()); int size = (int)(contours.size()); // 保存符號(hào)邊框的序號(hào) vector<int> num_order; map<int, int> num_map; for (int i = 0; i < size; i++) { // 獲取邊框數(shù)據(jù) Rect number_rect = boundingRect(contours[i]); int width = number_rect.width; int height = number_rect.height; // 去除較小的干擾邊框,篩選出合適的區(qū)域 if (width > img.cols/10 && height > img.rows/2) { rectangle(img_seg, number_rect.tl(), number_rect.br(), Scalar(255, 255, 255), 1, 1, 0); num_order.push_back(number_rect.x); num_map[number_rect.x] = i; } } // 按符號(hào)順序提取 sort(num_order.begin(), num_order.end()); for (int i = 0; i < num_order.size(); i++) { Rect number_rect = boundingRect(contours[num_map.find(num_order[i])->second]); Rect choose_rect(number_rect.x, 0, number_rect.width, gray_img.rows); Mat number_img = gray_img(choose_rect); imshow("number" + to_string(i), number_img); // imwrite("number" + to_string(i) + ".jpg", number_img); } imshow("添加方框", gray_img); waitKey(0); return 0; }
像素值判斷進(jìn)行分割
分割方法:首先判斷每一列的像素值大于 0 的像素個(gè)數(shù)超過(guò)5個(gè)時(shí),認(rèn)為此列是有數(shù)字的,記錄每列像素是否大于 5,產(chǎn)生一個(gè)數(shù)組。
// 確認(rèn)為 1 的像素 int pixrow[1000]; for (int i = 0; i < roi_col - 1; i++) { for (int j = 0; j < roi_row - 1; j++) { pix = img_threadhold.at<uchar>(j, i); pixrow[i] = 0; if (pix > 0) { pixrow[i] = 1; break; } } } // 對(duì)數(shù)組進(jìn)行濾波,減少突變概率 for (int i = 2; i < roi_col - 1 - 2; i++) { if ((pixrow[i - 1] + pixrow[i - 2] + pixrow[i + 1] + pixrow[i + 2]) >= 3) { pixrow[i] = 1; } else if ((pixrow[i - 1] + pixrow[i - 2] + pixrow[i + 1] + pixrow[i + 2]) <= 1) { pixrow[i] = 0; } }
之后記錄像素為 0 和 1 所連續(xù)的長(zhǎng)度來(lái)計(jì)算字符的寬度,最后用寬度的大小來(lái)篩選字符。
// 確認(rèn)字符位置 int count = 0; bool flage = false; for (int i = 0; i < roi_col - 1; i++) { pix = pixrow[i]; if (pix == 1 && !flage) { flage = true; position1[count] = i; continue; } if (pix == 0 && flage) { flage = false; position2[count] = i; count++; } if (i == (roi_col - 2) && flage) { flage = false; position2[count] = i; count++; } }
分割出的結(jié)果:
完整代碼:
#include <iostream> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc.hpp> #include <opencv2/imgproc/types_c.h> using namespace std; using namespace cv; int main() { Mat img = imread("number.jpg"); Mat gray_img; // 生成灰度圖像 cvtColor(img, gray_img, CV_BGR2GRAY); // 高斯模糊 Mat img_gau; GaussianBlur(gray_img, img_gau, Size(3, 3), 0, 0); // 閾值分割 Mat img_threadhold; threshold(img_gau, img_threadhold, 0, 255, THRESH_BINARY + THRESH_OTSU); // 判斷字符水平位置 int roi_col = img_threadhold.cols, roi_row = img_threadhold.rows, position1[50], position2[50], roi_width[50]; uchar pix; // 確認(rèn)為 1 的像素 int pixrow[1000]; for (int i = 0; i < roi_col - 1; i++) { for (int j = 0; j < roi_row - 1; j++) { pix = img_threadhold.at<uchar>(j, i); pixrow[i] = 0; if (pix > 0) { pixrow[i] = 1; break; } } } // 對(duì)數(shù)組進(jìn)行濾波,減少突變概率 for (int i = 2; i < roi_col - 1 - 2; i++) { if ((pixrow[i - 1] + pixrow[i - 2] + pixrow[i + 1] + pixrow[i + 2]) >= 3) { pixrow[i] = 1; } else if ((pixrow[i - 1] + pixrow[i - 2] + pixrow[i + 1] + pixrow[i + 2]) <= 1) { pixrow[i] = 0; } } // 確認(rèn)字符位置 int count = 0; bool flage = false; for (int i = 0; i < roi_col - 1; i++) { pix = pixrow[i]; if (pix == 1 && !flage) { flage = true; position1[count] = i; continue; } if (pix == 0 && flage) { flage = false; position2[count] = i; count++; } if (i == (roi_col - 2) && flage) { flage = false; position2[count] = i; count++; } } // 記錄所有字符寬度 for (int n = 0; n < count; n++) { roi_width[n] = position2[n] - position1[n]; } // 減去最大值、最小值,計(jì)算平均值用字符寬度來(lái)篩選 int max = roi_width[0], max_index = 0; int min = roi_width[0], min_index = 0; for (int n = 1; n < count; n++) { if (max < roi_width[n]) { max = roi_width[n]; max_index = n; } if (min > roi_width[n]) { min = roi_width[n]; min_index = n; } } int index = 0; int new_roi_width[50]; for (int i = 0; i < count; i++) { if (i == min_index || i == max_index) {} else { new_roi_width[index] = roi_width[i]; index++; } } // 取后面三個(gè)值的平均值 int avgre = (int)((new_roi_width[count - 3] + new_roi_width[count - 4] + new_roi_width[count - 5]) / 3.0); // 字母位置信息確認(rèn),用寬度來(lái)篩選 int licenseX[10], licenseW[10], licenseNum = 0; int countX = 0; for (int i = 0; i < count; i++) { if (roi_width[i] >(avgre - 8) && roi_width[i] < (avgre + 8)) { licenseX[licenseNum] = position1[i]; licenseW[licenseNum] = roi_width[i]; licenseNum++; countX++; continue; } if (roi_width[i] > (avgre * 2 - 10) && roi_width[i] < (avgre * 2 + 10)) { licenseX[licenseNum] = position1[i]; licenseW[licenseNum] = roi_width[i]; licenseNum++; } } // 截取字符 Mat number_img = Mat(Scalar(0)); for (int i = 0; i < countX; i++) { Rect choose_rect(licenseX[i], 0, licenseW[i], gray_img.rows); number_img = gray_img(choose_rect); imshow("number" + to_string(i), number_img); // imwrite("number" + to_string(i) + ".jpg", number_img); } waitKey(0); return 0; }
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- C++ ReSharper2021激活碼永久有效
- C++中實(shí)現(xiàn)OpenCV圖像分割與分水嶺算法
- c++ 基于opencv 識(shí)別、定位二維碼
- C++ opencv實(shí)現(xiàn)車道線識(shí)別
- 基于OpenCV和C++ 實(shí)現(xiàn)圖片旋轉(zhuǎn)
- c++ 形狀類Shape(派生出圓類Circle和矩形類Rectangle)
- OpenCV實(shí)現(xiàn)車牌定位(C++)
- C++中typeid實(shí)現(xiàn)原理詳解
- Visual Studio Code運(yùn)行C++代碼時(shí)顯示CLOCKS_PER_SEC未定義的問(wèn)題及解決方法
- C++ 實(shí)現(xiàn)PE文件特征碼識(shí)別的步驟
相關(guān)文章
C++ Strassen算法代碼的實(shí)現(xiàn)
這篇文章主要介紹了C++ Strassen算法代碼的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03WM_CLOSE、WM_DESTROY、WM_QUIT及各種消息投遞函數(shù)詳解
這篇文章主要介紹了WM_CLOSE、WM_DESTROY、WM_QUIT及各種消息投遞函數(shù),有助于讀者更好的理解windows程序的消息機(jī)制,需要的朋友可以參考下2014-07-07C++ Qt開發(fā)之使用QHostInfo查詢主機(jī)地址
Qt 是一個(gè)跨平臺(tái)C++圖形界面開發(fā)庫(kù),利用Qt可以快速開發(fā)跨平臺(tái)窗體應(yīng)用程序,本文將重點(diǎn)介紹如何運(yùn)用QHostInfo組件實(shí)現(xiàn)對(duì)主機(jī)地址查詢功能,希望對(duì)大家有所幫助2024-03-03QT實(shí)現(xiàn)簡(jiǎn)單計(jì)算器功能
這篇文章主要為大家詳細(xì)介紹了QT實(shí)現(xiàn)簡(jiǎn)單計(jì)算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04C與C++動(dòng)態(tài)分配二維數(shù)組的實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇C與C++動(dòng)態(tài)分配二維數(shù)組的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12