ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

基于深度卷积神经网络的车牌识别

2021-07-24 18:00:46  阅读:188  来源: 互联网

标签:Mat img 卷积 float 神经网络 图像 tf 车牌 size


 ANPR算法(车牌自动识别系统)

        1.车牌检测

                在整个视频帧中检测到车牌的位置。

        2.车牌识别

                 当在图像中检测到车牌时,使用OCR(光学字符识别)算法来识别车牌上的字母和数字。

        

1.1车牌检测

        这一步要检测当前帧中的所有车牌,我们将其分为两个主要步骤,分割和分类。

        在第一步(分割)中,将应用不同的滤波器,形态学算子,轮廓算法来验证图像中可能包含的车牌的部分。

        在第二步(分类)中,将对每个图像块(特征)应用SVM分类器进行分类。先训练两个不同的类:车牌和非车牌。

1.1.1分割

        车牌分割的一个重要特征是:假设图像是正面拍的,车牌没有旋转,则车牌会有大量的垂直边缘,首次分割的时候,可以利用这个特征来深处没有任何垂直边缘的区域。

        在找到垂直边缘之前,需要将彩色图像转换为灰度图像,并消除可能由相机或其他因素产生的噪点,利用5x5高斯模糊去噪。

//转化为灰度图像
Mat img_gray;
cvtColor(input,img_gray,CV_BGR2GRAY);
blur(img_gray,img_gray,Size(5,5));

        为了找到垂直边缘,采用Sobel滤波器对水平方向(x)求一阶导数,这个导数是一个数学导数,可以在图像上找到垂直边缘,函数定义如下:

CV_EXPORTS_W void Sobel( InputArray src, OutputArray dst, int ddepth,
                         int dx, int dy, int ksize = 3,
                         double scale = 1, double delta = 0,
                         int borderType = BORDER_DEFAULT );

         这里ddepth是目标图像深度,xorder是对x求导的阶数,yorder是对y求导的阶数,ksize表示kernel的大小,取值为1,3,5,7(默认3),scale是计算导数值的可选因子,delta是加入结果的可选值(默认0),borderType是橡树内插方法。

        这里采用xorder=1,yorder=0,ksize=3。

//寻找垂直边缘
Mat img_sobel;
Sobel(img_gray,img_sobel,CV_8U,1,0,3,1,0);

        Sobel滤波器后,采用阈值滤波器来获得二值图像,阈值通过Otsu算法得到(Otsu算法通过输入一个8位图像,自动获取图像的最优阈值)

//阈值图像
Mat img_threshold;
threshold(img_sobel,img_threshold,0,255,CV_THRESH_OTSU+CV_THRESH_BINARY;

        通过使用一个闭形态学算子,可以去除每条垂直边缘线之间的空白区,并将边缘数目较多的区域连接在一起,在此步骤中,可能包含车牌区域。

        先定义形态学算子中使用的结构元素,这里定义具有17x3大小的结构矩形元素,其他图像可能元素尺寸不一样 

        

Mat element = getStructuringElement(MORPH_RECT,Size(17,3));

        然后,在闭形态学算子中通过morphologyEx函数使用这个结构元素。

morphologyEx(img_threshold,img_threshold,CV_MOP_CLOSE_element);

        上述操作后,得到了包含车牌的区域,但是大多数区域并不包含插排,可以使用findContours函数来拆分这些区域,下面这个函数用来获取二进制图像的轮廓。

    // Find contours of possibles plates
    vector<vector<Point>> contours;
    findContours(img_threshold,
        contours, // a vector of contours
        cv::RETR_EXTERNAL, // retrieve the external contours
        cv::CHAIN_APPROX_NONE); // all pixels of each contours

        可用minAreaRect函数对检测到的每一个轮廓,提取最小面积的边界矩形。

    // Remove patch that are no inside limits of aspect ratio and area.
    while (itc != contours.end()) {
        // Create bounding rect of object
        RotatedRect mr = minAreaRect(Mat(*itc));
        if (!verifySizes(mr)) {
            itc = contours.erase(itc);
        } else {
            ++itc;
            rects.push_back(mr);
        }
    }

        根据区域面积和纵横比,对检测到的区域进行基本验证,若纵横比约为520/110=4.727272,那么认为该区域是个车牌,误差范围位40%,车牌区域高度误差在15~125个像素,这些参数因为图像的大小,相机的位置而不同。

bool DetectRegions::verifySizes(RotatedRect mr)
{
    float error = 0.4;
    // Spain car plate size: 52x11 aspect 4,7272
    float aspect = 4.7272;
    // Set a min and max area. All other patchs are discarded
    int min = 15 * aspect * 15; // minimum area
    int max = 125 * aspect * 125; // maximum area
    // Get only patchs that match to a respect ratio.
    float rmin = aspect - aspect * error;
    float rmax = aspect + aspect * error;

    int area = mr.size.height * mr.size.width;
    float r = (float)mr.size.width / (float)mr.size.height;
    if (r < 1)
        r = (float)mr.size.height / (float)mr.size.width;

    if ((area < min || area > max) || (r < rmin || r > rmax)) {
        return false;
    } else {
        return true;
    }
}

        所有的车牌都有白色的背景,为了得到精确的裁剪,可使用漫水填充算法来获取旋转的矩形。

                

    for (int i = 0; i < rects.size(); i++) {
        // For better rect cropping for each posible box
        // Make floodfill algorithm because the plate has white background
        // And then we can retrieve more clearly the contour box
        circle(result, rects[i].center, 3, Scalar(0, 255, 0), -1);
        // get the min size between width and height
        float minSize = (rects[i].size.width < rects[i].size.height) ? rects[i].size.width
                                                                     : rects[i].size.height;
        minSize = minSize - minSize * 0.5;
        // initialize rand and get 5 points around center for floodfill algorithm
        srand(time(NULL));
        // Initialize floodfill parameters and variables
        Mat mask;
        mask.create(input.rows + 2, input.cols + 2, CV_8UC1);
        mask = Scalar::all(0);
        const int loDiff = 30;
        const int upDiff = 30;
        const int connectivity = 4;
        const int newMaskVal = 255;
        const int NumSeeds = 10;
        Rect ccomp;
        const int flags = connectivity
            + (newMaskVal << 8)
            + cv::FLOODFILL_FIXED_RANGE
            + cv::FLOODFILL_MASK_ONLY;
        for (int j = 0; j < NumSeeds; j++) {
            Point seed;
            seed.x = rects[i].center.x + rand() % (int)minSize - (minSize / 2);
            seed.y = rects[i].center.y + rand() % (int)minSize - (minSize / 2);
            circle(result, seed, 1, Scalar(0, 255, 255), -1);
            int area = floodFill(input, mask, seed, Scalar(255, 0, 0), &ccomp,
                Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff), flags);
        }

        一旦有了裁剪掩码,可用利用图像掩码来得到一个最小面积的矩形,并再次检查他的有效大小。

// Check new floodfill mask match for a correct patch.
        // Get all points detected for get Minimal rotated Rect
        vector<Point> pointsInterest;
        Mat_<uchar>::iterator itMask = mask.begin<uchar>();
        Mat_<uchar>::iterator end = mask.end<uchar>();
        for (; itMask != end; ++itMask)
            if (*itMask == 255)
                pointsInterest.push_back(itMask.pos());

        RotatedRect minRect = minAreaRect(pointsInterest);

        if (verifySizes(minRect)) {
            // rotated rectangle drawing
            Point2f rect_points[4];
            minRect.points(rect_points);
            for (int j = 0; j < 4; j++)
                line(result, rect_points[j], rect_points[(j + 1) % 4], Scalar(0, 0, 255), 1, 8);

            // Get rotation matrix
            float r = (float)minRect.size.width / (float)minRect.size.height;
            float angle = minRect.angle;
            if (r < 1)
                angle = 90 + angle;
            Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);

            // Create and rotate image
            Mat img_rotated;
            warpAffine(input, img_rotated, rotmat, input.size(), cv::INTER_CUBIC);

            // Crop image
            Size rect_size = minRect.size;
            if (r < 1)
                swap(rect_size.width, rect_size.height);
            Mat img_crop;
            getRectSubPix(img_rotated, rect_size, minRect.center, img_crop);

        裁剪到的图像不适合用于训练与分类,因为他们的大小不同,此外他们的光照条件也不同,为此,可将所有的图像都缩放至相同的尺寸,并用光照直方图来调整所有的图像。

            Mat resultResized;
            resultResized.create(33, 144, CV_8UC3);
            resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC);
            // Equalize croped image
            Mat grayResult;
            cvtColor(resultResized, grayResult, cv::COLOR_BGR2GRAY);
            blur(grayResult, grayResult, Size(3, 3));
            grayResult = histeq(grayResult);
            if (saveRegions) {
                stringstream ss(stringstream::in | stringstream::out);
                ss << "tmp/" << filename << "_" << i << ".jpg";
                imwrite(ss.str(), grayResult);
            }
         

        将裁剪后的检测图像以及位置存储到一个向量中。

   output.push_back(Plate(grayResult, minRect.boundingRect()));

1.2分类

        分类之前的首要任务是训练分类器,这里我们采用75张车牌图像和35张不是车牌的图像但同样是144x33像素的图像来训练系统。

        我们使用图像像素特征来训练分类器算法,需要用DetectRegions类来创建用于训练的图像系统,并将SavingRegions变量设置为true保存图像,用脚本文件segmentAllFiles.sh对文件夹中的所有文件图像重复处理。

        我们将准备的所有图像训练数据存储为XML文件,以便直接与SVM函数一起使用,trainSVM.cpp通过指定的文件夹和图像文件编号来创建XML文件。

         OpenCV通过FileStorage类管理XML和YAML格式的数据文件,使用该函数,可用读取训练数据矩阵和类标签,并将信息保存在SVM_TrainingData和SVM_Classes中。

FileStorage fs;
fs.open("SVM.xml",FileStorage::READ);
Mat SVM_TrainingData;
Mat SVM_Classes;
fs["TrainingData"]>>SVM_TrainingData;
fs["Classes"]>>SVM_Classes;

         现在我们在SVM_TrainingData变量中存储了训练数据,在SVM_Classes中存储了标签,接着,只需要创建训练数据对象,链接数据和标签就可以在机器学习算法中使用了。

        

    Ptr<SVM> svmClassifier = cv::ml::SVM::create();
    svmClassifier->setType(cv::ml::SVM::C_SVC);
    svmClassifier->setKernel(cv::ml::SVM::LINEAR);
    svmClassifier->setDegree(0.0);
    svmClassifier->setGamma(1.0);
    svmClassifier->setCoef0(0);
    svmClassifier->setC(1);
    svmClassifier->setNu(0.0);
    svmClassifier->setP(0);
    svmClassifier->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 1000, 0.01));

    Ptr<TrainData> tdata = TrainData::create(SVM_TrainingData, ROW_SAMPLE, SVM_Classes);

    svmClassifier->train(tdata);

    // For each possible plate, classify with svm if it's a plate or no
    vector<Plate> plates;
    for (int i = 0; i < posible_regions.size(); i++) {
        Mat img = posible_regions[i].plateImg;
        Mat p = img.reshape(1, 1);
        p.convertTo(p, CV_32FC1);

        int response = (int)svmClassifier->predict(p);
        if (response == 1)
            plates.push_back(posible_regions[i]);
    }

2车牌识别 

2.1OCR分割

        首先,对获取的车牌图像用直方图均衡进行处理,将其作为OCR函数的输入,然后,应用阈值滤波器对图像进行处理,并将处理后的图像作为查找轮廓算法的输入。

    // Threshold input image
    Mat img_threshold;
    threshold(input, img_threshold, 60, 255, cv::THRESH_BINARY_INV);
    if (DEBUG)
        imshow("Threshold plate", img_threshold);
    Mat img_contours;
    img_threshold.copyTo(img_contours);
    // Find contours of possibles characters
    vector<vector<Point>> contours;
    findContours(img_contours,
        contours, // a vector of contours
        cv::RETR_EXTERNAL, // retrieve the external contours
        cv::CHAIN_APPROX_NONE); // all pixels of each contours

bool OCR::verifySizes(Mat r)
{
    // Char sizes 45x77
    float aspect = 45.0f / 77.0f;
    float charAspect = (float)r.cols / (float)r.rows;
    float error = 0.35;
    float minHeight = 15;
    float maxHeight = 28;
    // We have a different aspect ratio for number 1, and it can be ~0.2
    float minAspect = 0.2;
    float maxAspect = aspect + aspect * error;
    // area of pixels
    float area = countNonZero(r);
    // bb area
    float bbArea = r.cols * r.rows;
    //% of pixel in area
    float percPixels = area / bbArea;

    if (DEBUG)
        cout << "Aspect: " << aspect << " [" << minAspect << "," << maxAspect << "] "
             << "Area " << percPixels << " Char aspect " << charAspect << " Height char " << r.rows
             << "\n";
    if (percPixels < 0.8 && charAspect > minAspect && charAspect < maxAspect && r.rows >= minHeight
        && r.rows < maxHeight)
        return true;
    else
        return false;
}

2.2基于卷积神经网络的字符分类

        这里训练一个新的TenserFlow模型,先检查图像数据集并生成用于训练模型的资源。

 

         深度学习需要大量的样本,很多时候,需要对原始数据进行数据集增强(通过旋转、翻转图像、透视变换、添加噪声),来创建新的样本的方法。

        这里我们使用Augmentor工具,他是一个python库,允许我们通过想要的变换来增加需要的样本数量。

        查看你的pip版本,如果你没有pip,那么你就去装一个吧。

        通过pip安装Augmentor

        创建一个py脚本,这里的number_samples控制生成的样本数量

import Augmentor
number_samples=2000
p = Augmentor.Pipeline("/home/damiles/Projects/Damiles/Mastering-OpenCV-4-Third-Edition/Chapter_05/data/chars_seg/chars/")

p.random_distortion(probability=0.4, grid_width=4, grid_height=4, magnitude=1)
p.shear(probability=0.5, max_shear_left=5, max_shear_right=5)
p.skew_tilt(probability=0.8, magnitude=0.1)
p.rotate(probability=0.7, max_left_rotation=5, max_right_rotation=5)

p.sample(number_samples)

         该脚本生成一个输出文件夹,存储所有产生的图像,并保持在原路径下,生成两个数据集,一个用来训练,一个用来测试算法,这里修改number_samples=20000生成2w个训练的图像和2000个用来测试的图像。

        有了图像,要将他们输入到TensorFlow算法中,最好使用TFRecordDataset文件格式输入。

        首先用     pip install tensorflow   安装TensorFlow

        然后使用提供的脚本创建数据集文件来训练我们的模型,生成test.tfrecords和train.tfrecords文件。

        创建一个 CNN层结构的卷积网络:

卷积层1

32个5x5滤波器,具有ReLU激活函数
池化层2步长为2的2x2滤波器的最大池化层
卷积层364个5x5滤波器,具有ReLU激活函数
池化层4步长为2的2x2滤波器的最大池化层
密集层51024个神经元
Dropout层6比例为0.4的Dropout正则化处理
密集层730个神经元,每个数字和字符对应一个神经元        
Softmax层8具有梯度下降优化器的Softmax损失函数,学习率0.0001,20000个训练步骤

        得到的TensorFlow代码如下:

import tensorflow as tf
import argparse
import os

BASE_PATH="./chars_seg/DNN_data/"
project_name="ANPR_v2"
train_csv_file=BASE_PATH+"train.tfrecords"
test_csv_file=BASE_PATH+"test.tfrecords"
image_resize=[20,20]

def model_fn(features, labels, mode, params):

    convolutional_2d_1537261701724 = tf.layers.conv2d(
            name="convolutional_2d_1537261701724",
            inputs=features,
            filters=32,
            kernel_size=[5,5],
            strides=(1,1),
            padding="same",
            data_format="channels_last",
            dilation_rate=(1,1),
            activation=tf.nn.relu,
            use_bias=True)

    max_pool_2d_1537261722515 = tf.layers.max_pooling2d(
        name='max_pool_2d_1537261722515',
        inputs=convolutional_2d_1537261701724,
        pool_size=[2,2],
        strides=[2,2],
        padding='same',
        data_format='channels_last')

    convolutional_2d_1537261728442 = tf.layers.conv2d(
            name="convolutional_2d_1537261728442",
            inputs=max_pool_2d_1537261722515,
            filters=64,
            kernel_size=[5,5],
            strides=(1,1),
            padding="same",
            data_format="channels_last",
            dilation_rate=(1,1),
            activation=tf.nn.relu,
            use_bias=True)

    max_pool_2d_1537261754562 = tf.layers.max_pooling2d(
        name='max_pool_2d_1537261754562',
        inputs=convolutional_2d_1537261728442,
        pool_size=[2,2],
        strides=[2,2],
        padding='same',
        data_format='channels_last')

    flatten_1537261781778 = tf.reshape(max_pool_2d_1537261754562, [-1, 1600])

    dense_1537261790190 = tf.layers.dense(inputs=flatten_1537261781778, units=1024, activation=tf.nn.relu)

    dropout_1537261796854= tf.layers.dropout(inputs=dense_1537261790190, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)

    dense_1537261807397 = tf.layers.dense(inputs=dropout_1537261796854, units=30, activation=tf.nn.relu)

    logits=dense_1537261807397

    predictions = {
        "classes": tf.argmax(input=logits, axis=1),
        "probabilities": tf.nn.softmax(logits, name="softmax_tensor")
    }
    #Prediction and training
    if mode == tf.estimator.ModeKeys.PREDICT:
        return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

    # Calculate Loss (for both TRAIN and EVAL modes)
    onehot_labels = tf.one_hot(indices=tf.cast(labels, tf.int32), depth=30)
    loss = tf.losses.softmax_cross_entropy(
        onehot_labels=onehot_labels, logits=logits)
    
    # Compute evaluation metrics.
    accuracy = tf.metrics.accuracy(labels=labels,
                                   predictions=predictions["classes"],
                                   name='acc_op')
    metrics = {'accuracy': accuracy}
    tf.summary.scalar('accuracy', accuracy[1])

    # Configure the Training Op (for TRAIN mode)
    if mode == tf.estimator.ModeKeys.TRAIN:
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
        train_op = optimizer.minimize(
            loss=loss,
            global_step=tf.train.get_global_step())
        return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)

    # Add evaluation metrics (for EVAL mode)
    eval_metric_ops = {
        "accuracy": tf.metrics.accuracy(
            labels=labels, predictions=predictions["classes"])}
    return tf.estimator.EstimatorSpec(
        mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)


def _parser_function(example_proto):
    features = {"label": tf.FixedLenFeature((), tf.int64, default_value=0),
                "data": tf.FixedLenFeature((), tf.string, default_value="")
                }
    parsed_features = tf.parse_single_example(example_proto, features)
    image = tf.decode_raw(parsed_features['data'], tf.uint8)
    image = tf.cast(image, tf.float16)
    height = 20
    width = 20

    image_shape = tf.stack([height, width, 1])
    image = tf.reshape(image, image_shape)

    return image, parsed_features["label"]

def data_train_estimator():
    tfrecord_filenames = [train_csv_file]
    dataset = tf.data.TFRecordDataset(tfrecord_filenames)
    dataset = dataset.repeat()
    dataset = dataset.map(_parser_function, num_parallel_calls=100)
    dataset = dataset.batch(100)
    dataset = dataset.shuffle(100)
    iterator = dataset.make_one_shot_iterator()  # create one shot iterator
    feature, label = iterator.get_next()
    return feature, label

def data_test_estimator():
    tfrecord_filenames = [test_csv_file]
    dataset = tf.data.TFRecordDataset(tfrecord_filenames)
    dataset = dataset.map(_parser_function, num_parallel_calls=100)
    dataset = dataset.batch(100)
    iterator = dataset.make_one_shot_iterator()  # create one shot iterator
    feature, label = iterator.get_next()
    return feature, label
        

def build_estimator(model_dir):
    # Create the Estimator
    return tf.estimator.Estimator(
        model_fn=model_fn,
        model_dir=model_dir,
        params={
            # PARAMS
        }
    )

def run_experiment(args):
    """Run the training and evaluate using the high level API"""

    estimator = build_estimator(args.job_dir)

    train_spec = tf.estimator.TrainSpec(input_fn=data_train_estimator, max_steps=20000)
    eval_spec = tf.estimator.EvalSpec(input_fn=data_test_estimator)

    tf.estimator.train_and_evaluate(estimator, train_spec, eval_spec)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    # Input Arguments
    parser.add_argument(
        '--job-dir',
        help='GCS location to write checkpoints and export models',
        required=True
    )
  
    # Argument to turn on all logging
    parser.add_argument(
        '--verbosity',
        choices=[
            'DEBUG',
            'ERROR',
            'FATAL',
            'INFO',
            'WARN'
        ],
        default='INFO',
    )
  
    args = parser.parse_args()
  
    # Set python level verbosity
    tf.logging.set_verbosity(args.verbosity)
    # Set C++ Graph Execution level verbosity
    os.environ['TF_CPP_MIN_LOG_LEVEL'] = str(
        tf.logging.__dict__[args.verbosity] / 10)
  
    # Run the training job
    run_experiment(args)

        现在,我们可以使用TensorFlow来开始训练算法,使用下面的命令行。

        python code.py --job-dir=./model_output

        这里的--job-dir参数定义了存储训练输出模型的输出文件夹,在终端中,我们可以看到每次迭代的输出,以及损失值和精度值。

        这里一运行就报错了,各种版本不兼容,各种动态链接库找不到,心态崩了,下次再更新了!

        吐了。。。。。。

        好吧,下了个CUDA,找到你了。

       还是不行,再装了个CUDNN。。dll找不到的问题解决了,但是还是各种版本兼容问题

       真不更了。。。。

标签:Mat,img,卷积,float,神经网络,图像,tf,车牌,size
来源: https://blog.csdn.net/weixin_50260062/article/details/119056797

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有