OpenCV實現(xiàn)車牌字符分割(C++)
之前的車牌定位中已經(jīng)獲取到了車牌的位置,并且對車牌進(jìn)行了提取。我們最終的目的是進(jìn)行車牌識別,在這之前需要將字符進(jìn)行分割,方便對每一個字符進(jìn)行識別,最后將其拼接后便是完整的車牌號碼。關(guān)于車牌定位可以看這篇文章: OpenCV車牌定位(C++) ,本文使用的圖片也是來自這里。
先來看一看原圖:

最左邊的漢字本來是 滬,截取時只獲得了右邊一點點的部分,這與原圖和獲取方法都有關(guān),對于 川、滬… 這一類左右分開的字會經(jīng)常發(fā)生這類問題,對方法進(jìn)行優(yōu)化后可以解決,這里暫時不進(jìn)行討論。
后面的字都是完整的,字符分割的過程不會受影響。首先來一波常規(guī)操作,為了更方便處理,將其變成灰度圖片:

分割的方法不止一種,最簡單的就是多加點人工成分,按照大致寬度再微調(diào)進(jìn)行截取,但是這樣看似最快其實成本最高,只適用于單一的圖片,因此這種容錯低且不夠自動的方法就不考慮了。
目前我使用了兩種不同的方法,一種是進(jìn)行邊緣檢測再檢測輪廓,根據(jù)字符的輪廓特點篩選出字符;另一種就是像素值判斷,主要根據(jù)像素數(shù)量使用水平映射截取寬度,垂直映射因為高度基本一致就不需要了,方法于水平映射一樣。
兩種方法我都寫在后面,根據(jù)需要自行復(fù)制。如果要使用像素值進(jìn)行判斷的話,就需要再將灰度圖轉(zhuǎn)換成二值化圖片,使用閾值分割就行了。若使用第一種用輪廓分割的方法,灰度圖和二值化圖片都可以,結(jié)果沒什么區(qū)別。
檢測輪廓進(jìn)行分割
邊緣檢測
對圖像進(jìn)行邊緣檢測,這里采用的是 Canny 邊緣檢測,處理后的結(jié)果如下:

可以看到每個字的邊緣都被描繪出來了,接下來就將每個字的輪廓獲取出來。
檢測輪廓
直接使用 findContours() 將所有輪廓提取出來,再將其在原圖中畫出來看看效果:

可以看到不僅僅是每個字被框出來了,還有內(nèi)部以及圖像中表現(xiàn)特殊部分的輪廓也有,接下來我們就根據(jù)每個字的大致大小篩選出我們想要的結(jié)果:

這樣看起來是不是就成功了,然后根據(jù)輪廓位置將每個字提取出來就行了,不過在這里每個輪廓的前后順序不一定是圖像中的位置,這里我使用每個輪廓左上角橫坐標(biāo) x 的大小來排序。
完整代碼:
#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);
// 邊緣檢測,提取輪廓
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());
// 保存符號邊框的序號
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;
}
}
// 按符號順序提取
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 的像素個數(shù)超過5個時,認(rèn)為此列是有數(shù)字的,記錄每列像素是否大于 5,產(chǎn)生一個數(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;
}
}
}
// 對數(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ù)的長度來計算字符的寬度,最后用寬度的大小來篩選字符。
// 確認(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;
}
}
}
// 對數(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];
}
// 減去最大值、最小值,計算平均值用字符寬度來篩選
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++;
}
}
// 取后面三個值的平均值
int avgre = (int)((new_roi_width[count - 3] + new_roi_width[count - 4] + new_roi_width[count - 5]) / 3.0);
// 字母位置信息確認(rèn),用寬度來篩選
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;
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Objective-C的內(nèi)省(Introspection)用法小結(jié)
這篇文章主要介紹了Objective-C的內(nèi)省(Introspection)用法,這是面向?qū)ο笳Z言和環(huán)境的一個強大特性,需要的朋友可以參考下2014-07-07
QT布局管理詳解QVBoxLayout與QHBoxLayout及QGridLayout的使用
在這篇文章中,你將知道水平布局、垂直布局、網(wǎng)格布局如何輕松上手,以純代碼方式展示。對齊方式,大小設(shè)置,圖片頭像匹配標(biāo)簽,布局器里面的組件大小隨意切換大小,認(rèn)真看完這篇文章,QT布局管理器熟練使用2022-06-06
c++中string類成員函數(shù)c_str()的用法
c_str()函數(shù)返回一個指向正規(guī)c字符串的指針,內(nèi)容和string類的本身對象是一樣的,通過string類的c_str()函數(shù)能夠把string對象轉(zhuǎn)換成c中的字符串的樣式2013-09-09
C語言詳細(xì)講解strcpy strcat strcmp函數(shù)的模擬實現(xiàn)
這篇文章主要介紹了怎樣用C語言模擬實現(xiàn)strcpy與strcat和strcmp函數(shù),strcpy()函數(shù)是C語言中的一個復(fù)制字符串的庫函數(shù),strcat()函數(shù)的功能是實現(xiàn)字符串的拼接,strcmp()函數(shù)作用是比較字符串str1和str2是否相同2022-05-05
c語言網(wǎng)絡(luò)編程-標(biāo)準(zhǔn)步驟(比較簡單)
這篇文章主要介紹了c語言網(wǎng)絡(luò)編程-標(biāo)準(zhǔn)步驟(比較簡單),需要的朋友可以參考下2014-01-01

