基于OpenCV的车牌识别与分割

基于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;
}

效果如下图:

基于OpenCV的车牌识别与分割

完成灰度化之后在进行二值化, 两次膨胀一次腐蚀, 如下图所示:

基于OpenCV的车牌识别与分割

之后再查找图中轮廓, 计算轮廓的最小外接旋转矩形, 找出面积最大的一个便是车牌.

上述过程代码如下:

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;
}

至此, 车牌查找完成.

车牌分割

对得到的旋转矩形提取感兴趣区域, 然后对区域进行放射变换, 得到进一步的车牌分割图, 如下图所示:

基于OpenCV的车牌识别与分割

代码实现如下:

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;
}

之后对二值化后的车牌进行水平方向灰度值统计, 找出其中垂直方向宽度最大的连续行组, 截取之作为进一步分割出的车牌, 如下图:

基于OpenCV的车牌识别与分割

代码实现如下:

    // 横向投票, 得到列向量, 取最宽
    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));

然后对车牌进行垂直方向投票, 找到其中较宽的部分列组, 分割为每一位字符, 如下图:

基于OpenCV的车牌识别与分割

代码实现如下:

    // 纵向投票, 取出每一个字符
    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;
    }

总结

至此, 可以完成车牌识别并分割, 更换不同的输入测试识别的稳定性, 结果如下:

基于OpenCV的车牌识别与分割

基于OpenCV的车牌识别与分割

基于OpenCV的车牌识别与分割

; 缺点与不足

仅仅依赖色彩特征查找, 易受图中相似颜色干扰, 并且对于倾斜度较大的图片识别效果不佳, 有待加入基于边缘检测的部分组成混合车牌查找与评估.

参考资料

图像的自适应二值化(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/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球