最近开始接触 C++ 了,就拿一个 OpenCV 小项目来练练手。在车牌自动识别系统中,从汽车图像的获取到车牌字符处理是一个复杂的过程,本文就以一个简单的方法来处理车牌定位。
我国的汽车牌照一般由七个字符和一个点组成,车牌字符的高度和宽度是固定的,分别为90mm和45mm,七个字符之间的距离也是固定的12mm,点分割符的直径是10mm。
使用的图片是从百度上随便找的(侵删),展示一下原图和灰度图:
#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("license.jpg");
Mat gray_img;
// 生成灰度图像
cvtColor(img, gray_img, CV_BGR2GRAY);
// 在窗口中显示游戏原画
imshow("原图", img);
imshow("灰度图", gray_img);
waitKey(0);
return 0;
}
灰度图像的每一个像素都是由一个数字量化的,而彩色图像的每一个像素都是由三个数字组成的向量量化的,使用灰度图像会更方便后续的处理。
图像降噪
每一副图像都包含某种程度的噪声,在大多数情况下,需要平滑技术(也常称为滤波或者降噪技术)进行抑制或者去除,这些技术包括基于二维离散卷积的高斯平滑、均值平滑、基于统计学方法的中值平滑等。这里采用基于二维离散卷积的高斯平滑对灰度图像进行降噪处理,处理后的图像效果如下:
形态学处理
完成了高斯去噪以后,为了后面更加准确的提取车牌的轮廓,我们需要对图像进行形态学处理,在这里,我们对它进行开运算,处理后如下所示:
开运算呢就是先进行 erode 再进行 dilate 的过程就是开运算,它具有消除亮度较高的细小区域、在纤细点处分离物体,对于较大物体,可以在不明显改变其面积的情况下平滑其边界等作用。
erode 操作也就是腐蚀操作,类似于卷积,也是一种邻域运算,但计算的不是加权求和,而是对邻域中的像素点按灰度值进行排序,然后选择该组的最小值作为输出的灰度值。
dilate 操作就是膨胀操作,与腐蚀操作类似,膨胀是取每一个位置邻域内的最大值。既然是取邻域内的最大值,那么显然膨胀后的输出图像的总体亮度的平均值比起原图会有所上升,而图像中较亮物体的尺寸会变大;相反,较暗物体的尺寸会减小,甚至消失。
阈值分割
完成初步的形态学处理以后,我们需要对图像进行阈值分割,我们在这里采用了 Otsu 阈值处理,处理后的效果如下所示:
对图像进行数字处理时,我们需要把图像分成若干个特定的、具有独特性质的区域,每一个区域代表一个像素的集合,每一个集合又代表一个物体,而完成该过程的技术通常称为图像分割,它是从图像处理到图像分析的关键步骤。其实这个过程不难理解,就好比我们人类看景物一样,我们所看到的世界是由许许多多的物体组合而成的,就像教室是由人、桌子、书本、黑板等等组成。我们通过阈值处理,就是希望能够从背景中分离出我们的研究对象。
边缘检测
经过Otsu阈值分割以后,我们要对图像进行边缘检测,我们这里采用的是Canny边缘检测,处理后的结果如下:
接下来再进行一次闭运算和开运算,填充白色物体内细小黑色空洞的区域并平滑其边界,处理后的效果如下:
这个时候,车牌的轮廓已经初步被选出来了,只是还有一些白色块在干扰。
上述过程的代码:
// 得出轮廓
bool contour(Mat image, vector<vector<Point>> &contours, vector<Vec4i> &hierarchy) {
Mat img_gau, img_open, img_seg, img_edge;
// 高斯模糊
GaussianBlur(image, img_gau, Size(7, 7), 0, 0);
// 开运算
Mat element = getStructuringElement(MORPH_RECT, Size(23, 23));
morphologyEx(img_gau, img_open, MORPH_OPEN, element);
addWeighted(img_gau, 1, img_open, -1, 0, img_open);
// 阈值分割
threshold(img_open, img_seg, 0, 255, THRESH_BINARY + THRESH_OTSU);
// 边缘检测
Canny(img_seg, img_edge, 200, 100);
element = getStructuringElement(MORPH_RECT, Size(22, 22));
morphologyEx(img_edge, img_edge, MORPH_CLOSE, element);
morphologyEx(img_edge, img_edge, MORPH_OPEN, element);
findContours(img_edge, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
return true;
}
选取轮廓
现在我们已经有了轮廓,我们需要筛选出车牌所在的那个轮廓,由于车牌宽和高的比例是固定的,依据这个几何特征,我们进行筛选,效果如图:
代码如下:
// 车牌轮廓点
Point2f(*choose_contour(vector<vector<Point>> contours))[2] {
int size = (int)contours.size();
int i_init = 0;
static Point2f contour[4][2];
Point2f (*contours_result)[2] = new Point2f[size][2];
for (int i = 0; i < size; i++){
// 获取边框数据
RotatedRect number_rect = minAreaRect(contours[i]);
Point2f rect_point[4];
number_rect.points(rect_point);
float width = rect_point[0].x - rect_point[1].x;
float height = rect_point[0].y - rect_point[3].y;
// 用宽高比筛选
if (width < height) {
float temp = width;
width= height;
height = temp;
}
float ratio = width / height;
if (2.5 < ratio && ratio < 5.5) {
contours_result[i_init][0] = rect_point[0];
contours_result[i_init][1] = rect_point[2];
i_init++;
}
}
return contours_result;
}
// 截取车牌区域
int license_gain(Point2f (*choose_license)[2], Mat img) {
int size = (int)(_msize(choose_license) / sizeof(choose_license[0]));
// 绘制方框
for (int i = 0; i < size; i++) {
if ((int)choose_license[i][0].x > 1 && (int)choose_license[i][0].y > 1) {
int x = (int)choose_license[i][1].x;
int y = (int)choose_license[i][1].y;
int height = (int)(choose_license[i][0].x) - (int)(choose_license[i][1].x);
int width = (int)(choose_license[i][0].y) - (int)(choose_license[i][1].y);
//cout << height << " " << width << endl;
Rect choose_rect(x, y, height, width);
Mat number_img = img(choose_rect);
rectangle(img, choose_license[i][0], choose_license[i][1], Scalar(0, 0, 255), 2, 1, 0);
imshow("车牌单独显示" + to_string(i), number_img);
}
}
imshow("绘制方框", img);
return 0;
}
最后的 main 函数:
int main() {
// 读入原图
Mat img = imread("license.jpg");
Mat gray_img;
// 生成灰度图像
cvtColor(img, gray_img, CV_BGR2GRAY);
// 得出轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
contour(gray_img, contours, hierarchy);
// 截取车牌
Point2f (*choose_license)[2] = choose_contour(contours);
license_gain(choose_license, img);
delete [] choose_license;
waitKey(0);
return 0;
}