基于OpenCV的车牌识别与分割
车牌识别的整个流程分为车牌位置查找, 车牌分割, 字符分割三部分, 车牌位置查找主要基于色彩空间查找的方法, 车牌分割主要基于位置查找之后的车牌二值图的行列加和统计.
车牌位置查找
以目前最常见的蓝色车牌为例, 车牌查找过程首先要进行一次基于色彩的特殊灰度化, 主要原理是将原图进行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;
}
Original: https://blog.csdn.net/qq_35872656/article/details/122993813
Author: CastleJ
Title: 基于OpenCV的车牌识别与分割
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/642204/
转载文章受原作者版权保护。转载请注明原作者出处!