1. 关于OpenCV进阶之路

前段时间写过一些关于OpenCV基础知识方面的系列文章,主要内容是面向OpenCV初学者,介绍OpenCV中一些常用的函数的接口和调用方法,相关的内容在OpenCV的手册里都有更详细的解释,当时自己也是边学边写,权当为一种笔记的形式,所以难免有浅尝辄止的感觉,现在回头看来,很多地方描述上都存在不足,以后有时间,我会重新考虑每一篇文章,让成长系列对基础操作的介绍更加详细一些。

OpenCV进阶之路相比于成长系列,不会有太多的基础函数的介绍,相对来说会更偏向于工程实践,通过解决实际问题来说明某些较高级函数的用法和注意事项,主要内容会集中在特征提取、机器学习和目标跟踪几个方向。所以这个系列文章知识点没有先后顺序之分,根据个人平时工作学习中遇到的问题而定。

这篇文章主要介绍OpenCV中神经网络的用法,并通过车牌字符的识别来说明一些参数设置,函数调用顺序等,而关于神经网络的原理在博客机器学习分类里已经详细的讲解与实现了,所以本文中就不多加说明。

2. 车牌字符识别

车牌识别是计算机视觉在实际工程中一个非常成功的应用,虽然现在技术相对来说已经成熟,但是围绕着车牌定位、车牌二值化、车牌字符识别等方向,还是不时的有新的算法出现。通过学习车牌识别来提升自己在图像识别方面的工程经验是非常好的,因为它非常好的说明了计算机视觉的一般过程:

图像$\to$预处理$\to$图像分析$\to$目标提取$\to$目标识别

而整个车牌识别过程实际上相当于包含了两个上述过程:1,是车牌的识别;2,车牌字符的识别。

这篇文章其实主要是想介绍OpenCV中神经网络的用法,而不是介绍车牌识别技术。所以我们主要讨论的内容集中在车牌字符的识别上,关于定位、分割等不多加叙述叙述。

3. 字符特征提取

在深度学习(将特征提取作为训练的一部分)这个概念引入之前,一般在准备分类器进行识别之前都需要进行特征提取。因为一幅图像包含的内容太多,有些信息能区分差异性,而有些信息却代表了共性。所以我们要进行适当的特征提取把它们之间的差异性特征提取出来。

这里面我们计算二种简单的字符特征:梯度分布特征、灰度统计特征。这两个特征只是配合本篇文章来说明神经网络的普遍用法,实际中进行字符识别需要考虑的字符特征远远要比这复杂,还包括相似字特征的选取等,也由于工作上的原因,这一部分并不深入的介绍。

1,首先是梯度分布特征,该特征计算图像水平方向和竖直方向的梯度图像,然后通过给梯度图像分划不同的区域,进行梯度图像每个区域亮度值的统计,以下是算法步骤:

<1>将字符由RGB转化为灰度,然后将图像归一化到16*8。
<2>定义soble水平检测算子:$x\_mask = [-1,0,1;-2,0,2; –1,0,1]$和竖直方向梯度检测算子$y\_mask=x\_mask^T$。
<3>对图像分别用$mask\_x$和$mask\_y$进行图像滤波得到$SobelX$和$SobelY$,下图分别代表原图像、$SobelX$和$SobelY$。
<4>对滤波后的图像,计算图像总的像素和,然后划分4*2的网络,计算每个网格内的像素值的总和。
<5>将每个网络内总灰度值占整个图像的百分比统计在一起写入一个向量,将两个方向各自得到的向量并在一起,组成特征向量。

         

 void calcGradientFeat(const Mat& imgSrc, vector<float>& feat)
{
float sumMatValue(const Mat& image); // 计算图像中像素灰度值总和 Mat image;
cvtColor(imgSrc,image,CV_BGR2GRAY);
resize(image,image,Size(,)); // 计算x方向和y方向上的滤波
float mask[][] = { { , , }, { , , }, { -, -, - } }; Mat y_mask = Mat(, , CV_32F, mask) / ;
Mat x_mask = y_mask.t(); // 转置
Mat sobelX, sobelY; filter2D(image, sobelX, CV_32F, x_mask);
filter2D(image, sobelY, CV_32F, y_mask); sobelX = abs(sobelX);
sobelY = abs(sobelY); float totleValueX = sumMatValue(sobelX);
float totleValueY = sumMatValue(sobelY); // 将图像划分为4*2共8个格子,计算每个格子里灰度值总和的百分比
for (int i = ; i < image.rows; i = i + )
{
for (int j = ; j < image.cols; j = j + )
{
Mat subImageX = sobelX(Rect(j, i, , ));
feat.push_back(sumMatValue(subImageX) / totleValueX);
Mat subImageY= sobelY(Rect(j, i, , ));
feat.push_back(sumMatValue(subImageY) / totleValueY);
}
}
}
float sumMatValue(const Mat& image)
{
float sumValue = ;
int r = image.rows;
int c = image.cols;
if (image.isContinuous())
{
c = r*c;
r = ;
}
for (int i = ; i < r; i++)
{
const uchar* linePtr = image.ptr<uchar>(i);
for (int j = ; j < c; j++)
{
sumValue += linePtr[j];
}
}
return sumValue;
}

calcGradientFeat

2,第二个特征非常简单,只需要将图像归一化到特定的大小,然后将图像每个点的灰度值作为特征即可。

<1>将图像由RGB图像转换为灰度图像;

<2>将图像归一化大小为$8×4$,并将图像展开为一行,组成特征向量。

4. OpenCV中的神经网络

关于神经网络的原理我的博客里已经写了两篇文章,并且给出了C++的实现,所以这里我就不提了,下面主要说明在OpenCV中怎么使用它提供的库函数。

CvANN_MLP是OpenCV中提供的一个神经网络的类,正如它的名字一样(multi-layer perceptrons),它是一个多层感知网络,它有一个输入层,一个输出层以及1或多个隐藏层。

4.1. 首先我们来创建一个网络,我们可以利用CvANN_MLP的构造函数或者create函数。

 CvANN_MLP::CvANN_MLP(const Mat& layerSizes, int activateFunc=CvANN_MLP::SIGMOID_SYM, double fparam1=, double fparam2= );
void CvANN_MLP::create(const Mat& layerSizes, int activateFunc=CvANN_MLP::SIGMOID_SYM, double fparam1=, double fparam2= );

上面是分别是构造函数和cteate成员函数的接口,我们来分析各个形参的意思。

layerSizes:一个整型的数组,这里面用Mat存储。它是一个1*N的Mat,N代表神经网络的层数,第$i$列的值表示第$i$层的结点数。这里需要注意的是,在创建这个Mat时,一定要是整型的,uchar和float型都会报错。

比如我们要创建一个3层的神经网络,其中第一层结点数为$x_1$,第二层结点数为$x_2$,第三层结点数为$x_3$,则layerSizes可以采用如下定义:

 Mat layerSizes=(Mat_<int>(,)<<x1,x2,x3);

或者用一个数组来初始化:

 int ar[]={x1,x2,x3};
Mat layerSizes(,,CV_32S,ar);

activateFunc:这个参数用于指定激活函数,不熟悉的可以去看我博客里的这篇文章《神经网络:感知器与梯度下降》,一般情况下我们用SIGMOID函数就可以了,当然你也可以选择正切函数或高斯函数作为激活函数。OpenCV里提供了三种激活函数,线性函数(CvANN_MLP::IDENTITY)、sigmoid函数(CvANN_MLP::SIGMOID_SYM)和高斯激活函数(CvANN_MLP::GAUSSIAN)。

后面两个参数则是SIGMOID激活函数中的两个参数$\alpha$和$\beta$,默认情况下会都被设置为1。

$$f(x)=\beta \frac{1-e^{-\alpha x}}{1+e^{-\alpha x}}$$

4.2. 设置神经网络训练参数

神经网络训练参数的类型存放在CvANN_MLP_TrainParams这个类里,它提供了一个默认的构造函数,我们可以直接调用,也可以一项一项去设。

 CvANN_MLP_TrainParams::CvANN_MLP_TrainParams()
{
term_crit = cvTermCriteria( CV_TERMCRIT_ITER + CV_TERMCRIT_EPS, , 0.01 );
train_method = RPROP;
bp_dw_scale = bp_moment_scale = 0.1;
rp_dw0 = 0.1; rp_dw_plus = 1.2; rp_dw_minus = 0.5;
rp_dw_min = FLT_EPSILON; rp_dw_max = .;
}

它的参数大概包括以下几项。

term_crit:终止条件,它包括了两项,迭代次数(CV_TERMCRIT_ITER)和误差最小值(CV_TERMCRIT_EPS),一旦有一个达到条件就终止训练。

train_method:训练方法,OpenCV里提供了两个方法一个是很经典的反向传播算法BACKPROP,另一个是弹性反馈算法RPROP,对第二种训练方法,没有仔细去研究过,这里我们运用第一种方法。

剩下就是关于每种训练方法的相关参数,针对于反向传播法,主要是两个参数,一个是权值更新率bp_dw_scale和权值更新冲量bp_moment_scale。这两个量一般情况设置为0.1就行了;太小了网络收敛速度会很慢,太大了可能会让网络越过最小值点。

我们一般先运用它的默认构造函数,然后根据需要再修改相应的参数就可以了。如下面代码所示,我们将迭代次数改为了5000次。

 CvANN_MLP_TRainParams param;
param.term_crit=cvTermCriteria(CV_TerMCrIT_ITER+CV_TERMCRIT_EPS,,0.01);

4.3. 神经网络的训练

我们先看训练函数的接口,然后按接口去准备数据。

 int CvANN_MLP::train(const Mat& inputs, const Mat& outputs, const Mat& sampleWeights, const Mat& sampleIdx=Mat(), CvANN_MLP_TrainParams params=CvANN_MLP_TrainParams(), int flags= );

inputs:输入矩阵。它存储了所有训练样本的特征。假设所有样本总数为nSamples,而我们提取的特征维数为ndims,则inputs是一个$nSamples*ndims$的矩阵,我们可以这样创建它。

 Mat inputs(nSamples,ndims,CV_32FC1); //CV_32FC1说明它储存的数据是float型的。

我们需要将我们的训练集,经过特征提取把得到的特征向量存储在inputs中,每个样本的特征占一行。

outputs:输出矩阵。我们实际在训练中,我们知道每个样本所属的种类,假设一共有nClass类。那么我们将outputs设置为一个nSample行nClass列的矩阵,每一行表示一个样本的预期输出结果,该样本所属的那类对应的列设置为1,其他都为0。比如我们需要识别0-9这10个数字,则总的类数为10类,那么样本数字“3”的预期输出为[0,0,1,0,0,0,0,0,0,0];

sampleWeights:一个在使用RPROP方法训练时才需要的数据,所以这里我们不设置,直接设置为Mat()即可。

sampleIdx:相当于一个遮罩,它指定哪些行的数据参与训练。如果设置为Mat(),则所有行都参与。

params:这个在刚才已经说过了,是训练相关的参数。

flag:它提供了3个可选项参数,用来指定数据处理的方式,我们可以用逻辑符号去组合它们。UPDATE_WEIGHTS指定用一定的算法去初始化权值矩阵而不是用随机的方法。NO_INPUT_SCALE和NO_OUTPUT_SCALE分别用于禁止输入与输出矩阵的归一化。

一切都准备好后,直接开始训练吧!

4.4. 识别

识别是通过Cv_ANN_MLP类提供的predict来实现的,知道原理的会明白,它实际上就是做了一次向前传播。

 float CvANN_MLP::predict(const Mat& inputs, Mat& outputs) const

在进行识别的时候,我们对图像进行特征提取,把它保存在inputs里,通过调用predict函数,我们得到一个输出向量,它是一个1*nClass的行向量,其中每一列说明它与该类的相似程度(0-1之间),也可以说是置信度。我们只用对output求一个最大值,就可得到结果。这个函数的返回值是一个无用的float值,可以忽略。

5. 车牌字符识别测试

1,我们需要读取所有的训练样本,将它们的路径在保存在vector<string>中。

这里面我的车牌字符,因为1和I、0和O是一样的,所以数字加字母一共34类,其中每类有200个样本图像,共34*200个训练样本。

2,计算特征。我们按顺序读入图像,调用特征计算函数,把得到的结合保存在input对应的行中,同时把图像对应的预期输出保存在output中。

3,创建神经网络,这里我们计算得到的特征维数为48维,所以我们简单的设计一个3层的神经网络,输入层有48个结点,隐藏层也为48个结点,输出层为34个结点。然后神经网络的训练方法选用BACKPROP,迭代次数设置为5000次。

4,调用训练函数进行训练,并保存训练得到的权值矩阵,直接调用save成员函数即可。

nnetwork.save(“mlp.xml”);

5,识别测试,我们可以用单张图像进行测试,也可以选定一个测试集去进行测试,比如可以用一半的图像作为训练集,一半的图像作为测试集。这里我们可以加载已经训练好的权值矩阵,而不用重新训练,只要开始有保存了xml文件。但是记得你还是要创建一个网络后,才能加载进来。

 int NNClassifier::classifier(const Mat& image)
{
Mat nearest(, nclass, CV_32FC1, Scalar());
Mat charFeature;
calcFeature(image, charFeature); neuralNetwork.predict(charFeature, nearest);
Point maxLoc;
minMaxLoc(nearest, NULL, NULL, NULL, &maxLoc);
int result = maxLoc.x;
return result;
}

这里我简单的做了一下测试,在这两个特征下,网络设置为3层[48,48,34],一半图像为测试集,得到的识别率为98%,我相信通过尝试调整网络的层数以及选用更好的特征,一定会得到更满意的识别率。PS(工作中用的是SVM识别器,正常采集到的车牌,字符识别率在99.8%以上)。但是神经网络识别器有个很大的优点就是,一旦网络训练好,识别需要的数据文件非常小,而且速度很快。

6. 字符样本的下载

看到文章下的评论多是需求字符样本的,希望拿到字符样本的同学不要将其用于商业用途或者创建分享下载的链接。博文里用的样本是每类200张图像的测试样本,下面给出一份每类50个图像的样本子集,用来做测试已经够了。

链接:http://pan.baidu.com/s/1pLPeZkZ 密码:26eb

OpenCV进阶之路:神经网络识别车牌字符的更多相关文章

  1. OpenCV进阶之路:一个简化的视频摘要程序

    一.前言 视频摘要又称视频浓缩,是对视频内容的一个简单概括,先通过运动目标分析,提取运动目标,然后对各个目标的运动轨迹进行分析,将不同的目标拼接到一个共同的背景场景中,并将它们以某种方式进行组合.视频 ...

  2. opencv 车牌字符分割 ANN网络识别字符

    最近在复习OPENCV的知识,学习caffe的深度神经网络,正好想起以前做过的车牌识别项目,可以拿出来研究下 以前的环境是VS2013和OpenCV2.4.9,感觉OpenCV2.4.9是个经典版本啊 ...

  3. OpenCV+Python识别车牌和字符分割的实现

    本篇文章主要基于python语言和OpenCV库(cv2)进行车牌区域识别和字符分割,开篇之前针对在python中安装opencv的环境这里不做介绍,可以自行安装配置! 车牌号检测需要大致分为四个部分 ...

  4. [转]Theano下用CNN(卷积神经网络)做车牌中文字符OCR

    Theano下用CNN(卷积神经网络)做车牌中文字符OCR 原文地址:http://m.blog.csdn.net/article/details?id=50989742 之前时间一直在看 Micha ...

  5. NO.3:自学tensorflow之路------MNIST识别,神经网络拓展

    引言 最近自学GRU神经网络,感觉真的不简单.为了能够快速跑完程序,给我的渣渣笔记本(GT650M)也安装了一个GPU版的tensorflow.顺便也更新了版本到了tensorflow-gpu 1.7 ...

  6. 交警也觉得妙——Python 识别车牌

    车牌识别在高速公路中有着广泛的应用,比如我们常见的电子收费(ETC)系统和交通违章车辆的检测,除此之外像小区或地下 车库门禁也会用到,基本上凡是需要对车辆进行身份检测的地方都会用到. 一些背景: 车牌 ...

  7. 使用TensorFlow的卷积神经网络识别自己的单个手写数字,填坑总结

    折腾了几天,爬了大大小小若干的坑,特记录如下.代码在最后面. 环境: Python3.6.4 + TensorFlow 1.5.1 + Win7 64位 + I5 3570 CPU 方法: 先用MNI ...

  8. opencv学习之路(25)、轮廓查找与绘制(四)——正外接矩形

    一.简介 二.外接矩形的查找绘制 #include "opencv2/opencv.hpp" using namespace cv; void main() { //外接矩形的查找 ...

  9. GO语言的进阶之路-网络编程之socket

    GO语言的进阶之路-网络编程之socket 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.什么是socket; 在说socket之前,我们要对两个概念要有所了解,就是IP和端口 ...

随机推荐

  1. Oracle开发常用函数

    max 最大数 自动加 1 create or replace function fun_getmaxlot( vend in varchar2 , domain IN VARCHAR2, tag i ...

  2. 写Action的三种方法

    Action类似于servlet,在用户对浏览器输入url访问的时候充当控制器的角色.它在访问时产生,执行execute()之后就销毁了. 写Action是代理事务,它实现的三种方式是: (1)POJ ...

  3. PHP----遇到的Session问题

    使用SESSION,当跨页面使用时,会提示错误Cannot modify header information - headers already sent by..., 背景:使用session_s ...

  4. Qt5+VS2012编程

    安装配置 http://www.bogotobogo.com/Qt/Qt5_Visual_Studio_Add_in.php Qt5+GL http://qt-project.org/doc/qt-5 ...

  5. js生成一个以零开头的八位数并且依次递增

    条件:1八位数:2.以领开头:3.末尾要依次递增 <script type="text/javascript">function Account(num,a){ // ...

  6. yourphp读取不到hits

    源代码 <YP:list name="Article" order="id desc" catid="37" limit=" ...

  7. linux下解压命令大全

    .tar 解包:tar xvf FileName.tar打包:tar cvf FileName.tar DirName(注:tar是打包,不是压缩!)———————————————.gz解压1:gun ...

  8. GoLang之协程

    GoLang之协程 目前,WebServer几种主流的并发模型: 多线程,每个线程一次处理一个请求,在当前请求处理完成之前不会接收其它请求:但在高并发环境下,多线程的开销比较大: 基于回调的异步IO, ...

  9. [webgrid] – header - (How to Add custom html to Header in WebGrid)

    How to Add custom html to Header in WebGrid MyEvernote Link Posted on March 30, 2013by mtryambake Ho ...

  10. Mac添加bash alias

    1. vim ~/.bash_profile (无则新建) 添加 if [ -f ~/.bashrc ]; then source ~/.bashrcfi 2.vim ~/.bashrc (无则新建) ...