OpenCV C++案例實戰十《車牌號識別》
- 前言
- 一、車牌檢測
- 1.1.影像預處理
- 1.2.輪廓提取
- 1.3.功能效果
- 1.4.功能原始碼
- 二、字符切割
- 2.1.影像預處理
- 2.2.輪廓提取
- 2.3.功能效果
- 2.4.功能原始碼
- 三、字符識別
- 3.1.讀取檔案
- 3.2.字符匹配
- 3.3.功能原始碼
- 四、效果顯示
- 五、原始碼
- 總結
前言
本文將使用OpenCV C++ 進行車牌號識別,
一、車牌檢測

原圖如圖所示,本案例的需求是進行車牌號碼識別,所以,首先我們得定位車牌所在的位置,然后將車牌切割出來,接下來我們就來看看是如何實作,
1.1.影像預處理
首先經過一些常規的影像預處理,我們可以提取出影像的大致輪廓,然后根據輪廓的特征進一步確定我們所需要查找的輪廓,在這里,不同的影像需要根據本身影像特征設定預處理演算法,所以,本案例的一個缺點就是不具有魯棒性,只針對特定需求,
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
Mat thresh;
threshold(gray, thresh, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
//使用形態學開操作去除一些小輪廓
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
Mat open;
morphologyEx(thresh, open, MORPH_OPEN, kernel);

如圖為經過二值化后的影像,接下來我們就可以使用findContours尋找我們需要的輪廓,根據影像的輪廓特征就可以定位到車牌所在位置,然后將其從原圖中切割出來,以便后續的識別作業,在這里,我定義了一個License結構體,用于存盤ROI影像,以及其相對于原圖所在位置,這樣在后續的繪制作業中,我們就可以定位到ROI所在位置,
1.2.輪廓提取
//自定義車牌結構體
struct License
{
Mat mat; //ROI圖片
Rect rect; //ROI所在矩形
};
//使用 RETR_EXTERNAL 找到最外輪廓
vector<vector<Point>>contours;
findContours(open, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vector<vector<Point>>conPoly(contours.size());
for (int i = 0; i < contours.size(); i++)
{
double area = contourArea(contours[i]);
double peri = arcLength(contours[i], true);
//根據面積篩選出可能屬于車牌區域的輪廓
if (area > 1000)
{
//使用多邊形近似,進一步確定車牌區域輪廓
approxPolyDP(contours[i], conPoly[i], 0.02*peri, true);
if (conPoly[i].size() == 4)
{
//計算矩形區域寬高比
Rect box = boundingRect(contours[i]);
double ratio = double(box.width) / double(box.height);
if (ratio > 2 && ratio < 4)
{
//截取ROI區域
Rect rect = boundingRect(contours[i]);
License_ROI = { src(rect),rect };
}
}
}
}
1.3.功能效果

如圖為從汽車上定位到的車牌,并將其切割出來以便下面的識別作業,
1.4.功能原始碼
//獲取車牌所在ROI區域--車牌定位
bool Get_License_ROI(Mat src, License &License_ROI)
{
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
Mat thresh;
threshold(gray, thresh, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
//使用形態學開操作去除一些小輪廓
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
Mat open;
morphologyEx(thresh, open, MORPH_OPEN, kernel);
//使用 RETR_EXTERNAL 找到最外輪廓
vector<vector<Point>>contours;
findContours(open, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vector<vector<Point>>conPoly(contours.size());
for (int i = 0; i < contours.size(); i++)
{
double area = contourArea(contours[i]);
double peri = arcLength(contours[i], true);
//根據面積篩選出可能屬于車牌區域的輪廓
if (area > 1000)
{
//使用多邊形近似,進一步確定車牌區域輪廓
approxPolyDP(contours[i], conPoly[i], 0.02*peri, true);
if (conPoly[i].size() == 4)
{
//計算矩形區域寬高比
Rect box = boundingRect(contours[i]);
double ratio = double(box.width) / double(box.height);
if (ratio > 2 && ratio < 4)
{
//截取ROI區域
Rect rect = boundingRect(contours[i]);
License_ROI = { src(rect),rect };
}
}
}
}
if (License_ROI.mat.empty())
{
return false;
}
return true;
}
二、字符切割
2.1.影像預處理
通過剛才的車牌定位,我們已經將車牌從原圖中切割出來了,接下來,我們還需要將車牌上的字符一一切割出來,以便進行后續的識別作業,同理,我們也需要對車牌做同樣的預處理操作,
Mat gray;
cvtColor(License_ROI.mat, gray, COLOR_BGR2GRAY);
Mat thresh;
threshold(gray, thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
Mat close;
morphologyEx(thresh, close, MORPH_CLOSE, kernel);
經過灰度、閾值、形態學操作后的影像如下圖所示,

2.2.輪廓提取
接下來我們進行輪廓提取就可以提取出車牌上的每一個字符了,
vector<vector<Point>>contours;
findContours(close, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
for (int i = 0; i < contours.size(); i++)
{
double area = contourArea(contours[i]);
//由于我們篩選出來的輪廓是無序的,故后續我們需要將字符重新排序
if (area > 200)
{
Rect rect = boundingRect(contours[i]);
//計算外接矩形寬高比
double ratio = double(rect.height) / double(rect.width);
if (ratio > 1)
{
Mat roi = License_ROI.mat(rect);
resize(roi, roi, Size(50, 100), 1, 1, INTER_LINEAR);
Character_ROI.push_back({ roi ,rect });
}
}
}
如圖為切割出來的字符,不過這里有一個小問題就是,我們切割出來的字符并不是按車牌號碼那樣順序排列,所以,在這里我們還得對其重新進行排序,使其按車牌順序排列,
//冒泡排序
for (int i = 0; i < Character_ROI.size()-1; i++)
{
for (int j = 0; j < Character_ROI.size() - 1 - i; j++)
{
if (Character_ROI[j].rect.x > Character_ROI[j + 1].rect.x)
{
License temp = Character_ROI[j];
Character_ROI[j] = Character_ROI[j + 1];
Character_ROI[j + 1] = temp;
}
}
}
2.3.功能效果

2.4.功能原始碼
//獲取車牌每一個字符ROI區域
bool Get_Character_ROI(License &License_ROI, vector<License>&Character_ROI)
{
Mat gray;
cvtColor(License_ROI.mat, gray, COLOR_BGR2GRAY);
Mat thresh;
threshold(gray, thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
Mat close;
morphologyEx(thresh, close, MORPH_CLOSE, kernel);
vector<vector<Point>>contours;
findContours(close, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
for (int i = 0; i < contours.size(); i++)
{
double area = contourArea(contours[i]);
//由于我們篩選出來的輪廓是無序的,故后續我們需要將字符重新排序
if (area > 200)
{
Rect rect = boundingRect(contours[i]);
//計算外接矩形寬高比
double ratio = double(rect.height) / double(rect.width);
if (ratio > 1)
{
Mat roi = License_ROI.mat(rect);
resize(roi, roi, Size(50, 100), 1, 1, INTER_LINEAR);
Character_ROI.push_back({ roi ,rect });
}
}
}
//將篩選出來的字符輪廓 按照其左上角點坐標從左到右依次順序排列
//冒泡排序
for (int i = 0; i < Character_ROI.size()-1; i++)
{
for (int j = 0; j < Character_ROI.size() - 1 - i; j++)
{
if (Character_ROI[j].rect.x > Character_ROI[j + 1].rect.x)
{
License temp = Character_ROI[j];
Character_ROI[j] = Character_ROI[j + 1];
Character_ROI[j + 1] = temp;
}
}
}
if (Character_ROI.size() != 7)
{
return false;
}
return true;
}
三、字符識別
3.1.讀取檔案

如圖所示,為模板影像以及對應的label,我們需要讀取檔案,進行匹配,在這里我使用UTF8ToGB函式實作讀取txt檔案,目的是為了在控制臺顯示中文時,不會出現亂碼情況,
//讀取檔案 圖片
bool Read_Data(string filename,vector<Mat>&dataset)
{
vector<String>imagePathList;
glob(filename, imagePathList);
if (imagePathList.empty())return false;
for (int i = 0; i < imagePathList.size(); i++)
{
Mat image = imread(imagePathList[i]);
resize(image, image, Size(50, 100), 1, 1, INTER_LINEAR);
dataset.push_back(image);
}
return true;
}
//讀取檔案 標簽
bool Read_Data(string filename, vector<string>&data_name)
{
fstream fin;
fin.open(filename, ios::in);
if (!fin.is_open())
{
cout << "can not open the file!" << endl;
return false;
}
string s;
while (std::getline(fin, s))
{
string str = UTF8ToGB(s.c_str()).c_str();
data_name.push_back(str);
}
fin.close();
return true;
}
3.2.字符匹配
在這里,我的思路是:使用一個for回圈,將我們切割出來的字符與現有的模板進行匹配,而這個匹配演算法是求兩張影像的像素差,以此來判斷影像的相似程度,具體是使用OpenCV absdiff函式計算兩張影像的像素差.,


如圖為使用absdiff得到的效果圖,接下來,我們只需要計算影像中灰度值為0的像素點個數就可以了,像素點個數最少的那個label即為我們的匹配結果,當然,此方法肯定是會存在誤識別的情況的,進行字符匹配的方法還有:模板匹配,基于Hu矩輪廓匹配,大家可以試試,
3.3.功能原始碼
//識別車牌字符
bool License_Recognition(vector<License>&Character_ROI, vector<int>&result_index)
{
string filename = "data/";
vector<Mat>dataset;
if (!Read_Data(filename, dataset)) return false;
for (int i = 0; i < Character_ROI.size(); i++)
{
Mat roi_gray;
cvtColor(Character_ROI[i].mat, roi_gray, COLOR_BGR2GRAY);
Mat roi_thresh;
threshold(roi_gray, roi_thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);
int minCount = 1000000;
int index = 0;
for (int j = 0; j < dataset.size(); j++)
{
Mat temp_gray;
cvtColor(dataset[j], temp_gray, COLOR_BGR2GRAY);
Mat temp_thresh;
threshold(temp_gray, temp_thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);
//計算兩張圖片的像素差,以此判斷兩張圖片是否相同
Mat dst;
absdiff(roi_thresh, temp_thresh, dst);
int count = pixCount(dst);
if (count < minCount)
{
minCount = count;
index = j;
}
}
result_index.push_back(index);
}
return true;
}
四、效果顯示
//顯示最終效果
bool Draw_Result(Mat src, License &License_ROI, vector<License>&Character_ROI,vector<int>&result_index)
{
rectangle(src, License_ROI.rect, Scalar(0, 255, 0), 2);
vector<string>data_name;
if (!Read_Data("data_name.txt", data_name))return false;
for (int i = 0; i < Character_ROI.size(); i++)
{
cout << data_name[result_index[i]] << " ";
//putText 中文顯示會亂碼,所以采用下面代碼
CvxText text("C://Windows/Fonts/方正粗黑宋簡體.ttf");//字體
string str = data_name[result_index[i]]; //string 轉 char
const char*msg = str.data();
IplImage *temp; //Mat 轉 IplImage
temp = &IplImage(src);
text.putText(temp, msg, Point(License_ROI.rect.x + Character_ROI[i].rect.x, License_ROI.rect.y + Character_ROI[i].rect.y),Scalar(0,0,255));
}
return true;
}
在這里,為了使用putText顯示中文,我這里加了一些額外的代碼,如果需要使用putText顯示中文效果的朋友可以自行百度一下如何配置環境,
最終效果如圖所示:



五、原始碼
#include<iostream>
#include<opencv2/opencv.hpp>
#include<fstream> //文本讀寫
#include<Windows.h> //控制臺輸出中文亂碼
#include"CvxText.h" //putText顯示中文亂碼
using namespace std;
using namespace cv;
//自定義車牌結構體
struct License
{
Mat mat; //ROI圖片
Rect rect; //ROI所在矩形
};
//獲取車牌所在ROI區域--車牌定位
bool Get_License_ROI(Mat src, License &License_ROI)
{
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
Mat thresh;
threshold(gray, thresh, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
//使用形態學開操作去除一些小輪廓
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
Mat open;
morphologyEx(thresh, open, MORPH_OPEN, kernel);
//使用 RETR_EXTERNAL 找到最外輪廓
vector<vector<Point>>contours;
findContours(open, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vector<vector<Point>>conPoly(contours.size());
for (int i = 0; i < contours.size(); i++)
{
double area = contourArea(contours[i]);
double peri = arcLength(contours[i], true);
//根據面積篩選出可能屬于車牌區域的輪廓
if (area > 1000)
{
//使用多邊形近似,進一步確定車牌區域輪廓
approxPolyDP(contours[i], conPoly[i], 0.02*peri, true);
if (conPoly[i].size() == 4)
{
//計算矩形區域寬高比
Rect box = boundingRect(contours[i]);
double ratio = double(box.width) / double(box.height);
if (ratio > 2 && ratio < 4)
{
//截取ROI區域
Rect rect = boundingRect(contours[i]);
License_ROI = { src(rect),rect };
}
}
}
}
if (License_ROI.mat.empty())
{
return false;
}
return true;
}
//獲取車牌每一個字符ROI區域
bool Get_Character_ROI(License &License_ROI, vector<License>&Character_ROI)
{
Mat gray;
cvtColor(License_ROI.mat, gray, COLOR_BGR2GRAY);
Mat thresh;
threshold(gray, thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
Mat close;
morphologyEx(thresh, close, MORPH_CLOSE, kernel);
vector<vector<Point>>contours;
findContours(close, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
for (int i = 0; i < contours.size(); i++)
{
double area = contourArea(contours[i]);
//由于我們篩選出來的輪廓是無序的,故后續我們需要將字符重新排序
if (area > 200)
{
Rect rect = boundingRect(contours[i]);
//計算外接矩形寬高比
double ratio = double(rect.height) / double(rect.width);
if (ratio > 1)
{
Mat roi = License_ROI.mat(rect);
resize(roi, roi, Size(50, 100), 1, 1, INTER_LINEAR);
Character_ROI.push_back({ roi ,rect });
}
}
}
//將篩選出來的字符輪廓 按照其左上角點坐標從左到右依次順序排列
//冒泡排序
for (int i = 0; i < Character_ROI.size()-1; i++)
{
for (int j = 0; j < Character_ROI.size() - 1 - i; j++)
{
if (Character_ROI[j].rect.x > Character_ROI[j + 1].rect.x)
{
License temp = Character_ROI[j];
Character_ROI[j] = Character_ROI[j + 1];
Character_ROI[j + 1] = temp;
}
}
}
if (Character_ROI.size() != 7)
{
return false;
}
return true;
}
//從txt檔案中讀取中文,防止亂碼
string UTF8ToGB(const char* str)
{
string result;
WCHAR *strSrc;
LPSTR szRes;
//獲得臨時變數的大小
int i = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
strSrc = new WCHAR[i + 1];
MultiByteToWideChar(CP_UTF8, 0, str, -1, strSrc, i);
//獲得臨時變數的大小
i = WideCharToMultiByte(CP_ACP, 0, strSrc, -1, NULL, 0, NULL, NULL);
szRes = new CHAR[i + 1];
WideCharToMultiByte(CP_ACP, 0, strSrc, -1, szRes, i, NULL, NULL);
result = szRes;
delete[]strSrc;
delete[]szRes;
return result;
}
//讀取檔案 圖片
bool Read_Data(string filename,vector<Mat>&dataset)
{
vector<String>imagePathList;
glob(filename, imagePathList);
if (imagePathList.empty())return false;
for (int i = 0; i < imagePathList.size(); i++)
{
Mat image = imread(imagePathList[i]);
resize(image, image, Size(50, 100), 1, 1, INTER_LINEAR);
dataset.push_back(image);
}
return true;
}
//讀取檔案 標簽
bool Read_Data(string filename, vector<string>&data_name)
{
fstream fin;
fin.open(filename, ios::in);
if (!fin.is_open())
{
cout << "can not open the file!" << endl;
return false;
}
string s;
while (std::getline(fin, s))
{
string str = UTF8ToGB(s.c_str()).c_str();
data_name.push_back(str);
}
fin.close();
return true;
}
//計算像素點個數
int pixCount(Mat image)
{
int count = 0;
if (image.channels() == 1)
{
for (int i = 0; i < image.rows; i++)
{
for (int j = 0; j < image.cols; j++)
{
if (image.at<uchar>(i, j) == 0)
{
count++;
}
}
}
return count;
}
else
{
return -1;
}
}
//識別車牌字符
bool License_Recognition(vector<License>&Character_ROI, vector<int>&result_index)
{
string filename = "data/";
vector<Mat>dataset;
if (!Read_Data(filename, dataset)) return false;
for (int i = 0; i < Character_ROI.size(); i++)
{
Mat roi_gray;
cvtColor(Character_ROI[i].mat, roi_gray, COLOR_BGR2GRAY);
Mat roi_thresh;
threshold(roi_gray, roi_thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);
int minCount = 1000000;
int index = 0;
for (int j = 0; j < dataset.size(); j++)
{
Mat temp_gray;
cvtColor(dataset[j], temp_gray, COLOR_BGR2GRAY);
Mat temp_thresh;
threshold(temp_gray, temp_thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);
//計算兩張圖片的像素差,以此判斷兩張圖片是否相同
Mat dst;
absdiff(roi_thresh, temp_thresh, dst);
int count = pixCount(dst);
if (count < minCount)
{
minCount = count;
index = j;
}
}
result_index.push_back(index);
}
return true;
}
//顯示最終效果
bool Draw_Result(Mat src, License &License_ROI, vector<License>&Character_ROI,vector<int>&result_index)
{
rectangle(src, License_ROI.rect, Scalar(0, 255, 0), 2);
vector<string>data_name;
if (!Read_Data("data_name.txt", data_name))return false;
for (int i = 0; i < Character_ROI.size(); i++)
{
cout << data_name[result_index[i]] << " ";
//putText 中文顯示會亂碼,所以采用下面代碼
CvxText text("C://Windows/Fonts/方正粗黑宋簡體.ttf");//字體
string str = data_name[result_index[i]]; //string 轉 char
const char*msg = str.data();
IplImage *temp; //Mat 轉 IplImage
temp = &IplImage(src);
text.putText(temp, msg, Point(License_ROI.rect.x + Character_ROI[i].rect.x, License_ROI.rect.y + Character_ROI[i].rect.y),Scalar(0,0,255));
}
return true;
}
int main()
{
Mat src = imread("car.jpg");
if (src.empty())
{
cout << "No image!" << endl;
system("pause");
return -1;
}
License License_ROI;
if (Get_License_ROI(src, License_ROI))
{
vector<License>Character_ROI;
if (Get_Character_ROI(License_ROI, Character_ROI))
{
vector<int>result_index;
if (License_Recognition(Character_ROI, result_index))
{
Draw_Result(src, License_ROI, Character_ROI,result_index);
}
else
{
cout << "未能識別字符!" << endl;
system("pause");
return -1;
}
}
else
{
cout << "未能切割出字符!" << endl;
system("pause");
return -1;
}
}
else
{
cout << "未定位到車牌位置!" << endl;
system("pause");
return -1;
}
imshow("src", src);
waitKey(0);
system("pause");
return 0;
}
總結
本文使用OpenCV C++進行車牌號識別,關鍵步驟有以下幾點,
1、車牌定位,案例需求是進行車牌識別,那么我們就得知道車牌在什么位置,將車牌找到之后,需要將車牌切割出來,作為一個整體進行下面作業,
2、字符分割,我們得到了車牌,需要將車牌上的字符一一分割出來才能進行下面的識別作業,有個小細節就是需要將字符重新排序,
3、字符識別,我們將得到的字符與我們準備好的模板一一進行匹配,匹配演算法有很多,大家可以自行嘗試,我這里使用的是基于兩幅影像的像素差進行影像比對,
需要說明的是:本案例是根據特定影像、特定需求設定的演算法,并不具有魯棒性,所有在影像預處理階段很重要,我們需要提取出我們需要的影像特征,這樣才能夠進行后續的作業,所以本案例也只是使用傳統的影像處理手段實作車牌識別功能,將大致流程作了一個說明,這里只提供一個參考作用!!!
歡迎大家交流學習!!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/388073.html
標籤:其他
