- 前言
- 一、车牌检测
- 1.1.图像预处理
- 1.2.轮廓提取
- 1.3.功能效果
- 1.4.功能源码
- 二、字符切割
- 2.1.图像预处理
- 2.2.轮廓提取
- 2.3.功能效果
- 2.4.功能源码
- 三、字符识别
- 3.1.读取文件
- 3.2.字符匹配
- 3.3.功能源码
- 四、效果显示
- 五、源码
- 总结
- freetype库配置
前言
本文将使用OpenCV C++ 进行车牌号识别。
一、车牌检测
原图如图所示。本案例的需求是进行车牌号码识别。所以,首先我们得定位车牌所在的位置,然后将车牌切割出来。接下来我们就来看看是如何实现。
首先经过一些常规的图像预处理,我们可以提取出图像的大致轮廓。然后根据轮廓的特征进一步确定我们所需要查找的轮廓。在这里,不同的图像需要根据本身图像特征设定预处理算法。所以,本案例的一个缺点就是不具有鲁棒性,只针对特定需求。
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所在位置。
//自定义车牌结构体
struct License
{
Mat mat; //ROI图片
Rect rect; //ROI所在矩形
};
//使用 RETR_EXTERNAL 找到最外轮廓 vector1.3.功能效果>contours; findContours(open, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); vector >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 }; } } } }
如图为从汽车上定位到的车牌,并将其切割出来以便下面的识别工作。
//获取车牌所在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>contours;
findContours(open, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vector>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);
经过灰度、阈值、形态学操作后的图像如下图所示。
接下来我们进行轮廓提取就可以提取出车牌上的每一个字符了。
vector>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三、字符识别 3.1.读取文件&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 >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; }
如图所示,为模板图像以及对应的label。我们需要读取文件,进行匹配。在这里我使用UTF8ToGB函数实现读取txt文件,目的是为了在控制台显示中文时,不会出现乱码情况。
//读取文件 图片 bool Read_Data(string filename,vector&dataset) { vector 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, vector3.2.字符匹配&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; }
在这里,我的思路是:使用一个for循环,将我们切割出来的字符与现有的模板进行匹配。而这个匹配算法是求两张图像的像素差,以此来判断图像的相似程度。具体是使用OpenCV absdiff函数计算两张图像的像素差.。
如图为使用absdiff得到的效果图。接下来,我们只需要计算图像中灰度值为0的像素点个数就可以了。像素点个数最少的那个label即为我们的匹配结果。当然,此方法肯定是会存在误识别的情况的。进行字符匹配的方法还有:模板匹配,基于Hu矩轮廓匹配。大家可以试试。
//识别车牌字符 bool License_Recognition(vector四、效果显示&Character_ROI, vector &result_index) { string filename = "data/"; vector 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&Character_ROI,vector &result_index) { rectangle(src, License_ROI.rect, Scalar(0, 255, 0), 2); vector 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#include #include //文本读写 #include //控制台输出中文乱码 #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 >contours; findContours(open, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); vector >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 &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 >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 &dataset) { vector 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 &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 (i, j) == 0) { count++; } } } return count; } else { return -1; } } //识别车牌字符 bool License_Recognition(vector &Character_ROI, vector &result_index) { string filename = "data/"; vector 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 &Character_ROI,vector &result_index) { rectangle(src, License_ROI.rect, Scalar(0, 255, 0), 2); vector 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 Character_ROI; if (Get_Character_ROI(License_ROI, Character_ROI)) { vector 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、字符识别。我们将得到的字符与我们准备好的模板一一进行匹配。匹配算法有很多,大家可以自行尝试。我这里使用的是基于两幅图像的像素差进行图像比对。
需要说明的是:本案例是根据特定图像、特定需求设定的算法。并不具有鲁棒性。所有在图像预处理阶段很重要。我们需要提取出我们需要的图像特征,这样才能够进行后续的工作。所以本案例也只是使用传统的图像处理手段实现车牌识别功能。将大致流程作了一个说明,这里只提供一个参考作用!!!
注:关于有很多小伙伴提出的问题“ “ft2build.h": No such file or directory”。这是因为由于OpenCV putText 不支持显示中文,在本案例中,我为了显示中文,故编译了freetype库。如果大家觉得有需要的话,可以自行编译配置环境。如果觉得麻烦的话,将源码中的中文显示函数注释掉也是可以直接运行的。
freetype库配置freetype库下载地址:http://download.savannah.gnu.org/releases/freetype/
下载解压后,选择合适vs版本进行编译就可以啦!!!
编译好之后,像配置OpenCV环境一样,将include、lib文件配置在vs环境中就可以了
欢迎大家点赞、关注,可私信找我领取完整源码、模板图像以及测试图像!!!
欢迎大家交流学习!!!



