车牌识别的整个流程分为车牌位置查找, 车牌分割, 字符分割三部分, 车牌位置查找主要基于色彩空间查找的方法, 车牌分割主要基于位置查找之后的车牌二值图的行列加和统计.
车牌位置查找以目前最常见的蓝色车牌为例, 车牌查找过程首先要进行一次基于色彩的特殊灰度化, 主要原理是将原图进行rgb通道分离, 然后进行通道相减提取蓝色区域, 并与普通的灰度图进行一次加权平均, 得到最终结果, 代码如下:
// 针对蓝色区域的特殊灰度化
//input是输入的原图
//output是输出的灰度图
//rate是基于色彩分割所占比重
void GrayscaleSegmentation(const Mat& input, Mat& output, float rate)
{
Mat result;
if(input.channels() == 1)
{
result = input;
output = result;
}
Mat bgr_channel[3];
split(input, bgr_channel);
Mat b_r = bgr_channel[0] - bgr_channel[2];
Mat b_g = bgr_channel[0] - bgr_channel[1];
Mat gray;
cvtColor(input, gray, COLOR_BGR2GRAY);
result = (b_r / 2 + b_g / 2) * rate + gray * (1 - rate);
output = result;
}
效果如下图:
完成灰度化之后在进行二值化, 两次膨胀一次腐蚀, 如下图所示:
之后再查找图中轮廓, 计算轮廓的最小外接旋转矩形, 找出面积最大的一个便是车牌.
上述过程代码如下:
RotatedRect FindLicense(const Mat& input) //input是输入的原图
{
Mat img = input.clone();
GrayscaleSegmentation(img, img, 0.8);
threshold(img, img, 70, 255, THRESH_BINARY);
dilate(img, img, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 2);
erode(img, img, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 1);
vector> vpp;
findContours(img, vpp, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
RotatedRect max_r_rect;
for(auto& i :vpp)
{
RotatedRect r_rect = minAreaRect(i);
if(r_rect.size.area() > max_r_rect.size.area())
{
max_r_rect = r_rect;
}
}
return max_r_rect;
}
至此, 车牌查找完成.
车牌分割对得到的旋转矩形提取感兴趣区域, 然后对区域进行放射变换, 得到进一步的车牌分割图, 如下图所示:
代码实现如下:
Mat lic_r; Rect lic_rect = r_rect.boundingRect(); //r_rect是车牌的旋转矩形 warpAffine(src(lic_rect),lic_r,getRotationMatrix2D(lic_rect.tl()/2,r_rect.angle-90,1),lic_rect.size());字符分割
字符分割首先对得到的车牌图进行灰度化, 然后使用自适应二值化算法进行二值化, 其代码实现如下:
void AdaptiveThreshold(const Mat& input, Mat& output, double rate)
{
Mat src = input.clone();
int height = (int)sqrt(double(src.rows * (src.cols + src.rows)) / double(src.cols));
int width = src.cols * height / src.rows;
for(int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
int h1 = max(1, i - height / 2);
int h2 = max(1, i + height / 2);
int w1 = min(j - width / 2, src.cols);
int w2 = min(j + width / 2, src.cols);
double avg = 0;
for (int x = h1; x < h2; x++)
for (int y = w1; y < w2; y++)
avg += (double) src.at(x, y);
src.at(i, j) = uint8_t(avg / ((w2 - w1) * (h2 - h1)));
}
for(int i = 0; i< src.rows; i++)
for(int j = 0; j < src.cols; j++)
src.at(i, j) = input.at(i, j) < (src.at(i, j) * rate) ? 0 : 255;
output = src;
}
之后对二值化后的车牌进行水平方向灰度值统计, 找出其中垂直方向宽度最大的连续行组, 截取之作为进一步分割出的车牌, 如下图:
代码实现如下:
// 横向投票, 得到列向量, 取最宽
Mat v_vector(Size(1, 35), CV_32F, Scalar(0));
reduce(src, v_vector, 1, REDUCE_SUM, CV_MAKE_TYPE(CV_32F, 32));
v_vector.convertTo(v_vector, CV_8UC1, 0.01);
threshold(v_vector, v_vector, 50, 255, THRESH_BINARY);
vector> v_vvp;
findContours(v_vector, v_vvp, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
Rect max_rect;
for(auto& i : v_vvp)
{
Rect rect = boundingRect(i);
if(rect.area() > max_rect.area())
{
max_rect = rect;
}
}
src = src(Rect(max_rect.tl(), Point(110, max_rect.br().y)));
resize(src, src, Size(110, 35));
然后对车牌进行垂直方向投票, 找到其中较宽的部分列组, 分割为每一位字符, 如下图:
代码实现如下:
// 纵向投票, 取出每一个字符
Mat h_vector(Size(110, 1), CV_32F, Scalar(0));
reduce(src, h_vector, 0, REDUCE_SUM, CV_MAKE_TYPE(CV_32F, 32));
h_vector.convertTo(h_vector, CV_8UC1, 0.03);
threshold(h_vector, h_vector, 20, 255, THRESH_BINARY);
vector> h_vvp;
findContours(h_vector, h_vvp, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vector result(7);
int index = 7;
for(auto& i : h_vvp)
{
Rect rect = boundingRect(i);
if(rect.area() < 5)
{
continue;
}
Mat character = src(Rect(rect.tl(), Point(rect.br().x, 35)));
resize(character, character, Size(20, 20));
index--;
if(index < 0)
{
break;
}
result[index] = character;
}
总结
至此, 可以完成车牌识别并分割, 更换不同的输入测试识别的稳定性, 结果如下:
缺点与不足仅仅依赖色彩特征查找, 易受图中相似颜色干扰, 并且对于倾斜度较大的图片识别效果不佳, 有待加入基于边缘检测的部分组成混合车牌查找与评估.
参考资料图像的自适应二值化(https://blog.csdn.net/lj501886285/article/details/52425157)
利用Hough变换和先验知识的车牌字符分割算法(https://kns.cnki.net/KXReader/Detail?TIMESTAMP=637456931763232421&DBCODE=CJFD&TABLEName=CJFD2004&FileName=JSJX200401016&RESULT=1&SIGN=c4LdsOAPtniwR9kXPsNeSqq0KHA%3d)
附录(完整代码实现)#include#include using namespace std; using namespace cv; RotatedRect FindLicense(const Mat& input); void SplitCharacters(const Mat& input, vector & output); void GrayscaleSegmentation(const Mat& input, Mat& output, float rate); void AdaptiveThreshold(const Mat& input, Mat& output, double rate); int main(int argc, char** argv) { Mat src = imread("img/3.png"); RotatedRect r_rect = FindLicense(src); Mat lic_r; Rect lic_rect = r_rect.boundingRect(); warpAffine(src(lic_rect), lic_r, getRotationMatrix2D(lic_rect.tl()/2, r_rect.angle - 90, 1), lic_rect.size()); vector characters; SplitCharacters(lic_r, characters); imshow("src", src); imshow("lic", lic_r); if(characters.size() == 7) { imshow("0", characters[0]); imshow("1", characters[1]); imshow("2", characters[2]); imshow("3", characters[3]); imshow("4", characters[4]); imshow("5", characters[5]); imshow("6", characters[6]); } waitKey(); return 0; } RotatedRect FindLicense(const Mat& input) { Mat img = input.clone(); GrayscaleSegmentation(img, img, 0.8); threshold(img, img, 70, 255, THRESH_BINARY); dilate(img, img, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 2); erode(img, img, getStructuringElement(MORPH_RECT, Size(3, 3)), Point(-1, -1), 1); vector > vpp; findContours(img, vpp, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); // TODO: 添加边缘检测与色彩检测共同评估, 或需要将色彩检测与边缘检测分离 // Mat o1, o2; // Mat kernel = (Mat_ (2, 2) << 1, 0, 0, -1); // filter2D(output, o1, -1, kernel); // kernel = (Mat_ (2, 2) << 0, 1, -1, 0); // filter2D(output, o2, -1, kernel); // output = o1+o2; RotatedRect max_r_rect; for(auto& i :vpp) { RotatedRect r_rect = minAreaRect(i); if(r_rect.size.area() > max_r_rect.size.area()) { max_r_rect = r_rect; } } return max_r_rect; } void SplitCharacters(const Mat& input, vector & output) { // 归一化 Mat src = input.clone(); resize(src, src, Size(110, 35)); cvtColor(src, src, COLOR_BGR2GRAY); AdaptiveThreshold(src, src, 1.2); // 横向投票, 得到列向量, 取最宽 Mat v_vector(Size(1, 35), CV_32F, Scalar(0)); reduce(src, v_vector, 1, REDUCE_SUM, CV_MAKE_TYPE(CV_32F, 32)); // NOLINT(hicpp-signed-bitwise) v_vector.convertTo(v_vector, CV_8UC1, 0.01); // NOLINT(hicpp-signed-bitwise) threshold(v_vector, v_vector, 50, 255, THRESH_BINARY); vector > v_vvp; findContours(v_vector, v_vvp, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); Rect max_rect; for(auto& i : v_vvp) { Rect rect = boundingRect(i); if(rect.area() > max_rect.area()) { max_rect = rect; } } src = src(Rect(max_rect.tl(), Point(110, max_rect.br().y))); resize(src, src, Size(110, 35)); // 纵向投票, 取出每一个字符 Mat h_vector(Size(110, 1), CV_32F, Scalar(0)); reduce(src, h_vector, 0, REDUCE_SUM, CV_MAKE_TYPE(CV_32F, 32)); // NOLINT(hicpp-signed-bitwise) h_vector.convertTo(h_vector, CV_8UC1, 0.03); // NOLINT(hicpp-signed-bitwise) threshold(h_vector, h_vector, 20, 255, THRESH_BINARY); vector > h_vvp; findContours(h_vector, h_vvp, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); vector result(7); int index = 7; for(auto& i : h_vvp) { Rect rect = boundingRect(i); if(rect.area() < 5) { continue; } Mat character = src(Rect(rect.tl(), Point(rect.br().x, 35))); resize(character, character, Size(20, 20)); index--; if(index < 0) { break; } result[index] = character; } // 输出 result.swap(output); } // 针对蓝色区域的特殊灰度化 void GrayscaleSegmentation(const Mat& input, Mat& output, float rate) { Mat result; if(input.channels() == 1) { result = input; output = result; } Mat bgr_channel[3]; split(input, bgr_channel); Mat b_r = bgr_channel[0] - bgr_channel[2]; Mat b_g = bgr_channel[0] - bgr_channel[1]; Mat gray; cvtColor(input, gray, COLOR_BGR2GRAY); result = (b_r / 2 + b_g / 2) * rate + gray * (1 - rate); output = result; } void AdaptiveThreshold(const Mat& input, Mat& output, double rate) { Mat src = input.clone(); int height = (int)sqrt(double(src.rows * (src.cols + src.rows)) / double(src.cols)); int width = src.cols * height / src.rows; for(int i = 0; i < src.rows; i++) for (int j = 0; j < src.cols; j++) { int h1 = max(1, i - height / 2); int h2 = max(1, i + height / 2); int w1 = min(j - width / 2, src.cols); int w2 = min(j + width / 2, src.cols); double avg = 0; for (int x = h1; x < h2; x++) for (int y = w1; y < w2; y++) avg += (double) src.at (x, y); src.at (i, j) = uint8_t(avg / ((w2 - w1) * (h2 - h1))); } for(int i = 0; i< src.rows; i++) for(int j = 0; j < src.cols; j++) src.at (i, j) = input.at (i, j) < (src.at (i, j) * rate) ? 0 : 255; output = src; }



