OpenCV自学笔记17. 基于SVM和神经网络的车牌识别(一)

版权声明:拥抱开源,欢迎转载,转载请保留原文链接~ https://blog.csdn.net/u010429424/article/details/75322182

基于SVM和神经网络的车牌识别(一)

本系列文章参考自《深入理解OpenCV实用计算机视觉项目解析》仅作学习用途

图像预处理

本篇用到的测试图片为:

这里写图片描述

Step1. 首先,读入并显示图像,代码如下:

string in = "images/2715DTZ.jpg";
   Mat image = imread(in, IMREAD_GRAYSCALE); // IMREAD_GRAYSCALE的值就是0
   if( image.empty() ){ 
    return -1; 
}
imshow("【原始图】", image);

上述代码把原始图像转变为单通道的灰度图,程序运行的效果如下:

这里写图片描述

Step2. 读入图像后,需要对图像进行均值滤波去噪

blur(image, image, Size(5, 5));
imshow("【去噪后】", image);

效果如下:

这里写图片描述

车牌的一个明显特征是,存在大量的竖直边,为了找出这些竖直边,采用Sobel滤波器对图像的水平方向求导

// 部分参数说明:
// CV_8U是目标图像的深度
// 1表示对x方向求导,0表示不对y方向求导
// 3是核kernel的大小 
Sobel(image, image, CV_8U, 1, 0, 3, 1, 0);
imshow("【sobel滤波】", image);

效果如下,左边是对x方向求导(作为对比,右边是对y方向求导):

这里写图片描述

Sobel滤波后,利用otsu(大津法)算法对图像进行自动阈值化,得到一幅最优化的二值图像
关于otsu算法,可见:http://blog.csdn.net/liyuanbhu/article/details/49387483

threshold(image, image, 0, 255, CV_THRESH_OTSU);
imshow("【otsu阈值化】", image);

otsu后的图像如下:

这里写图片描述

然后使用闭运算(先膨胀再腐蚀)连通近邻的区域,说白了就是将亮的区域尽量连在一起

Mat element = getStructuringElement(MORPH_RECT, Size(17, 3));
morphologyEx(image, image, CV_MOP_CLOSE, element);
imshow("【闭运算】", image);

闭运算后的图像如下:

这里写图片描述

可以看到,经过闭运算后的图像,白色的部分被连在了一起,车牌刚好处于连通区域内,此时使用轮廓检测,并提取矩形区域,就可以粗略提取出车牌可能存在的区域

// 定义存储轮廓的向量
vector<vector<Point>> contours;

// 部分参数说明;
// CV_RETR_EXTERNAL 只检测外轮廓
// CV_CHAIN_APPROX_SIMPLE 只存储水平,垂直,对角直线的起始点
findContours(image, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);

for (int i = 0; i < contours.size(); i++) {
    drawContours(image, contours, i, Scalar(255), 2);     // 绘制轮廓
}
imshow("【轮廓提取】", image);

轮廓提取的结果如下:

这里写图片描述

提取轮廓后,依次遍历每一个轮廓,通过面积、宽高比来验证是否为车牌区域,首先绘制出这些矩形区域
修改上面的代码,增加绘制矩形的代码(RotatedRect旋转矩形的绘制没有现成的函数,需要通过绘制直线完成)

vector<vector<Point>> contours;
findContours(image, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
for (int i = 0; i < contours.size(); i++) {
    drawContours(image, contours, i, Scalar(255), 1); // 绘制轮廓
    // ------------------------绘制矩形 start-------------------------------------
    RotatedRect rect = minAreaRect(contours[i]);
    Point2f vertices[4];
    rect.points(vertices);
    for (int i = 0; i < 4; i++) {
        line(image, vertices[i], vertices[(i+1)%4], Scalar(255), 2);
    }
    // ------------------------绘制矩形 end---------------------------------------
}
imshow("【轮廓提取】", image);

绘制矩形的结果如下:

这里写图片描述

下面编写验证矩形的函数,验证的原则是:

  1. 车牌宽高比为520/110=4.727272,误差不超过40%

  2. 车牌高度的范围在15~125之间

验证代码如下:

bool verify(RotatedRect rect) {
    float error = 0.4;
    const float aspect = 4.7272;
    int min = 15 * aspect * 15; // 面积下限
    int max = 125 * aspect * 125; // 面积上限

    float rmin = aspect - aspect * error; // 宽高比下限
    float rmax = aspect + aspect * error; // 宽高比上限

    int area = rect.size.width * rect.size.height; // 计算面积
    float r = rect.size.width / rect.size.height;  // 计算宽高比
    r = r < 1 ? 1 / r : r;

    return area >= min && area <= max && r >= rmin && r <= rmax;
}

在上述绘制矩形的代码后,加入验证代码

vector<vector<Point>> contours;
findContours(image, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
map<int, RotatedRect> _map; // 注意,新加入了一个map

for (int i = 0; i < contours.size(); i++) {
    drawContours(image, contours, i, Scalar(255), 1); // 绘制轮廓

    // ------------------------绘制矩形 start-------------------------------------
    RotatedRect rect = minAreaRect(contours[i]);
    Point2f vertices[4];
    rect.points(vertices);
    for (int i = 0; i < 4; i++) {
        line(image, vertices[i], vertices[(i + 1) % 4], Scalar(255), 2);
    }
    // ------------------------绘制矩形 end---------------------------------------

    // 加入验证代码
    if (verify(rect)) {
        _map[i] = rect;
    }
}

随后添加几行代码,把通过验证的矩形都绘制出来

// 绘制通过验证的矩形
map<int, RotatedRect>::iterator iter;
iter = _map.begin();
while (iter != _map.end()) {
    RotatedRect rect = iter->second;
    Point2f vertices[4];
    rect.points(vertices);
    for (int j = 0; j < 4; j++) {
        line(image, vertices[j], vertices[(j + 1) % 4], Scalar(255), 10);
    }
    iter++;
}
imshow("【通过验证】", image);

绘制通过验证的矩形,结果如下(粗线表示通过验证的矩形,仔细看一共6个粗线矩形):

这里写图片描述

那么,如何从这些通过验证的矩形中,得到车牌所在的那个矩形呢?

《深入理解OpenCV实用计算机视觉项目解析》一书中采用漫水填充算法,利用车牌背景为白色这一特性,获取最优的矩形。需要创建图像掩码、随机的种子点等等,设定很多参数,总之程序很复杂。

在这里,我提出一个不成熟的观点,思想超级简单,经过测试也可以成功提取车牌。

我们知道,车牌所在的区域是一个近似矩形的区域,用轮廓提取的方法,我们得到了这些近似矩形区域的轮廓。通过观察发现,车牌的轮廓是这些轮廓中最方方正正的,看起来最像矩形的。如果我们定义一个标准,衡量这些轮廓的矩形化程度,这样就可以选出最接近矩形的那一个。仿照圆度的定义,我把这个衡量标准暂且叫做“矩形度”,矩形度的定义如下:

这里写图片描述

其中,w/h 为宽高比,在之前已经定义,其大小为4.7272

则车牌轮廓的矩形度标准为:4 * (4.7272 + 1/4.7272) + 8 = 27.75

我们认为如果一个轮廓的矩形度越接近27.75,说明它就越像车牌。当然如果这个方法不准确,还可以通过位置,角度等信息进一步改进,或者就像书中那样,使用漫水填充算法

将上述思想转化为代码:

// 绘制通过验证的矩形
// ----------------定义矩形度计算用到的变量和常量 start---------------------
int min_diff = 100000;
int index = 0;
const float square = 27.75;
// ----------------定义矩形度计算用到的变量和常量 end-----------------------
map<int, RotatedRect>::iterator iter;
iter = _map.begin();
while (iter != _map.end()) {

    RotatedRect rect = iter->second;
    Point2f vertices[4];
    rect.points(vertices);
    for (int j = 0; j < 4; j++) {
        line(image, vertices[j], vertices[(j + 1) % 4], Scalar(255), 10);
    }

    // -----------------矩形度计算 start-------------------------
    int perimeter = arcLength(contours[iter->first], true);
    int area = contourArea(contours[iter->first]);
    if (area != 0) {
    int squareness = perimeter * perimeter / area;
    float diff = abs(squareness - square);
        if (diff < min_diff) {
            min_diff = diff;
            index = iter->first;
        }
    }
    // -----------------矩形度计算 end-------------------------
    iter++;
}

imshow("【通过验证】", image);

为了显示方便,我在原始图像上绘制结果,这部分代码不贴出来了,直接看效果吧:

这里写图片描述

最后一步,图像切割,把检测到的车牌切割出来,效果如下:

// 图像切割
Mat image_crop;

// 始终保持宽 > 高
Size rect_size = rect.size;
if (rect_size.width < rect_size.height) {
    swap(rect_size.width, rect_size.height);
}

getRectSubPix(image3, rect.size, rect.center, image_crop);
imshow("【切割后的车牌】", image_crop);

这里写图片描述

最后,本篇的完整代码如下:

#include <iostream>
#include <map>
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"

using namespace std;
using namespace cv;


bool verify(RotatedRect rect) {
    float error = 0.4;
    const float aspect = 4.7272;
    int min = 15 * aspect * 15; // 面积下限
    int max = 125 * aspect * 125; // 面积上限

    float rmin = aspect - aspect * error; // 宽高比下限
    float rmax = aspect + aspect * error; // 宽高比上限

    int area = rect.size.width * rect.size.height; // 计算面积
    float r = rect.size.width / rect.size.height;  // 计算宽高比
    r = r < 1 ? 1 / r : r;

    return area >= min && area <= max && r >= rmin && r <= rmax;
}


int main()
{
    string in = "images/2715DTZ.jpg";

    Mat image = imread(in, IMREAD_GRAYSCALE);
    Mat image2 = imread(in);
    Mat image3 = imread(in, IMREAD_GRAYSCALE);

    if (image.empty()){
        return -1;
    }
    imshow("【原始图】", image);

    blur(image, image, Size(5, 5));
    imshow("【去噪后】", image);


    Sobel(image, image, CV_8U, 1, 0, 3, 1, 0);
    imshow("【sobel滤波】", image);

    threshold(image, image, 0, 255, CV_THRESH_OTSU);
    imshow("【otsu阈值化】", image);

    Mat element = getStructuringElement(MORPH_RECT, Size(17, 3));
    morphologyEx(image, image, CV_MOP_CLOSE, element);
    imshow("【闭运算】", image);


    vector<vector<Point>> contours;
    findContours(image, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
    map<int, RotatedRect> _map;

    for (int i = 0; i < contours.size(); i++) {
        drawContours(image, contours, i, Scalar(255), 1); // 绘制轮廓

        // 绘制矩形
        RotatedRect rect = minAreaRect(contours[i]);
        Point2f vertices[4];
        rect.points(vertices);
        for (int i = 0; i < 4; i++) {
            line(image, vertices[i], vertices[(i + 1) % 4], Scalar(255), 2);
        }

        // 验证
        if (verify(rect)) {
            _map[i] = rect;
        }
    }
    imshow("【轮廓提取】", image);


    // 绘制通过验证的矩形
    int min_diff = 100000;
    int index = 0;
    const float square = 27.75;

    map<int, RotatedRect>::iterator iter;
    iter = _map.begin();
    while (iter != _map.end()) {

        RotatedRect rect = iter->second;
        Point2f vertices[4];
        rect.points(vertices);
        for (int j = 0; j < 4; j++) {
            line(image, vertices[j], vertices[(j + 1) % 4], Scalar(255), 10);
        }

        // 选择最接近的矩形
        int perimeter = arcLength(contours[iter->first], true);
        int area = contourArea(contours[iter->first]);
        if (area != 0) {
        int squareness = perimeter * perimeter / area;

        float diff = abs(squareness - square);
            if (diff < min_diff) {
                min_diff = diff;
                index = iter->first;
            }
        }
        iter++;
    }

    imshow("【通过验证】", image);


    // 绘制最接近的矩形
    RotatedRect rect = _map[index];
    Point2f vertices[4];
    rect.points(vertices);
    for (int i = 0; i < 4; i++) {
        cout << " asdf" << endl;
        line(image2, vertices[i], vertices[(i + 1) % 4], Scalar(0, 255, 0), 10);
    }
    imshow("【最接近的矩形】", image2);


    // 图像切割
    Mat image_crop;

    // 始终保持宽 > 高
    Size rect_size = rect.size;
    if (rect_size.width < rect_size.height) {
        swap(rect_size.width, rect_size.height);
    }
    getRectSubPix(image3, rect.size, rect.center, image_crop);
    imshow("【切割后的车牌】", image_crop);

    waitKey();
    return 0;
}

参考

otsu算法:http://blog.csdn.net/liyuanbhu/article/details/49387483
OpenCV findContour函数: http://blog.csdn.net/yin1203014/article/details/44462791
RotatedRect :http://blog.csdn.net/u012507022/article/details/51684776
漫水填充算法:http://blog.csdn.net/poem_qianmo/article/details/28261997

在下一篇文章,将对车牌号图片进行分类和识别,拜~

系列文章

OpenCV自学笔记17. 基于SVM和神经网络的车牌识别(一)
OpenCV自学笔记18. 基于SVM和神经网络的车牌识别(二)
OpenCV自学笔记19. 基于SVM和神经网络的车牌识别(三)
OpenCV自学笔记20. 基于SVM和神经网络的车牌识别(四)

这里写图片描述

猜你喜欢

转载自blog.csdn.net/u010429424/article/details/75322182