在上一篇文章的介绍中,我们已经通过相应的字符分割方法,将车牌区域进行分割,得到7个分割字符图块,接下来要做的就是将字符图块放入训练好的神经网络模型,通过模型来预测每个图块所表示的具体字符。本节主要介绍字符特征的提取,和如何通过训练好的神经网络模型来进行字符的识别。
字符识别主要是通过类CharsIdentify 来进行,对于中文字符和非中文字符,分别采取了不同的策略,训练得到的ANN模型也不一样,中文字符的识别主要使用 identifyChinese 来处理,非中文字符的识别主要采用 identify 来处理。另外,类CharsIdentify采用了单例模式。参见文末5
chars_identify.h头文件如下:
#include "easypr/util/kv.h"
#include "easypr/core/character.hpp"
#include "easypr/core/feature.h"
namespace easypr {
class CharsIdentify {
public:
static CharsIdentify* instance();
//ann_模型由LoadModel函数加载
//参见文末1。通过神经网络模型ann_对车牌字符进行识别,maxVal表示ann输出的每个字符的可能性大小
int classify(cv::Mat f, float& maxVal, bool isChinses = false, bool isAlphabet = false);
//输入车牌字符的特征向量,送入ann_
void classify(cv::Mat featureRows, std::vector<int>& out_maxIndexs,std::vector<float>& out_maxVals, std::vector<bool> isChineseVec);
//输入车牌类CCharacter的对象,调用charFeatures函数提取字符特征,送入ann_预测,文末2
void classify(std::vector<CCharacter>& charVec);
//annChinese_ annGray_分别由LoadChineseModel和LoadGrayChANN加载,见文末3
//使用模型annChinese_进行字符识别
void classifyChinese(std::vector<CCharacter>& charVec);
//使用模型annGray_进行字符识别
void classifyChineseGray(std::vector<CCharacter>& charVec);
//文末4 ,调用classify函数进行分类,得到字符在KChar数组中的索引
std::pair<std::string, std::string> identify(cv::Mat input, bool isChinese = false, bool isAlphabet = false);
int identify(std::vector<cv::Mat> inputs, std::vector<std::pair<std::string, std::string>>& outputs,
std::vector<bool> isChineseVec);
std::pair<std::string, std::string> identifyChinese(cv::Mat input, float& result, bool& isChinese);//调用annChinese_分类
std::pair<std::string, std::string> identifyChineseGray(cv::Mat input, float& result, bool& isChinese);//调用annGray_分类
//使用classify函数进行分类,经过ann得到每个字符的分数,如果最大值maxVal不满如条件(maxVal >= 0.9 || (isChinese && maxVal >= chineseMaxThresh)),则说明,这个不是字符.
bool isCharacter(cv::Mat input, std::string& label, float& maxVal, bool isChinese = false);
void LoadModel(std::string path);//加载非中文字符模型ann_
void LoadChineseModel(std::string path);//加载中文字符模型annChinese_
void LoadGrayChANN(std::string path);//annGray_
void LoadChineseMapping(std::string path);//加载模型kv_,不知道是啥??
private:
CharsIdentify();
annCallback extractFeature;
static CharsIdentify* instance_;
// binary character classifer
cv::Ptr<cv::ml::ANN_MLP> ann_;
// binary character classifer, only for chinese
cv::Ptr<cv::ml::ANN_MLP> annChinese_;
// gray classifer, only for chinese
cv::Ptr<cv::ml::ANN_MLP> annGray_;
// used for chinese mapping
std::shared_ptr<Kv> kv_;
};
}
#endif
文末:
1、通过上述函数获取字符特征之后,可以通过神经网络模型对车牌字符进行识别,具体的识别函数:
int CharsIdentify::classify(cv::Mat f, float& maxVal, bool isChinses, bool isAlphabet){
int result = 0;
cv::Mat output(1, kCharsTotalNumber, CV_32FC1);
ann_->predict(f, output);//调用其 predict() 函数,即可得到输出矩阵 output,输出矩阵中最大的值即为识别的车牌字符.
maxVal = -2.f;
if (!isChinses) {//如果不是中文字符
if (!isAlphabet) {//不是字母
result = 0;
for (int j = 0; j < kCharactersNumber; j++) {//kCharactersNumber=34,10个数字,24个字母
float val = output.at<float>(j);
// std::cout << "j:" << j << "val:" << val << std::endl;
if (val > maxVal) {
maxVal = val;
result = j;//招待output中最大值,和其对应的位置
}
}
}
else {//是字母字符
result = 0;
// begin with 11th char, which is 'A'
for (int j = 10; j < kCharactersNumber; j++) {
float val = output.at<float>(j);
// std::cout << "j:" << j << "val:" << val << std::endl;
if (val > maxVal) {
maxVal = val;
result = j;
}
}
}
}
else {//是中文字符,从34开始,kCharsTotalNumber为65,
result = kCharactersNumber;
for (int j = kCharactersNumber; j < kCharsTotalNumber; j++) {
float val = output.at<float>(j);
//std::cout << "j:" << j << "val:" << val << std::endl;
if (val > maxVal) {
maxVal = val;
result = j;
}
}
}
//std::cout << "maxVal:" << maxVal << std::endl;
return result;
}
注意classify函数返回字符在kchars数组中的索引号
ann_为之前加载得到的神经网路模型,直接调用其 predict() 函数,即可得到输出矩阵 output,输出矩阵中最大的值即为识别的车牌字符,其中,数值分别为0-64的65个数字,对应的值如下所示:
static const char *kChars[] = {
"0", "1", "2",
"3", "4", "5",
"6", "7", "8",
"9",
/* 10 */
"A", "B", "C",
"D", "E", "F",
"G", "H", /* {"I", "I"} */
"J", "K", "L",
"M", "N", /* {"O", "O"} */
"P", "Q", "R",
"S", "T", "U",
"V", "W", "X",
"Y", "Z",
/* 24 */
"zh_cuan" , "zh_e" , "zh_gan" ,
"zh_gan1" , "zh_gui" , "zh_gui1" ,
"zh_hei" , "zh_hu" , "zh_ji" ,
"zh_jin" , "zh_jing" , "zh_jl" ,
"zh_liao" , "zh_lu" , "zh_meng" ,
"zh_min" , "zh_ning" , "zh_qing" ,
"zh_qiong", "zh_shan" , "zh_su" ,
"zh_sx" , "zh_wan" , "zh_xiang",
"zh_xin" , "zh_yu" , "zh_yu1" ,
"zh_yue" , "zh_yun" , "zh_zang" ,
"zh_zhe"
/* 31 */
};
2、字符特征的获取,主要通过 charFeatures 函数来实现,返回字符特征向量。
非中文字符features个数为 10+10+10*10=120,10个水平投影,10个垂直投影,100个像素行累加和特征。
Mat charFeatures(Mat in, int sizeData) {
const int VERTICAL = 0;
const int HORIZONTAL = 1;
// cut the cetner, will afect 5% perices.
Rect _rect = GetCenterRect(in);
Mat tmpIn = CutTheRect(in, _rect);
// Low data feature
Mat lowData;
//非中文字符和中文字符获得的字符特征个数是不同的,非中文字符features个数为 10+10+10*10=120,
//中文字符features个数为 20+20+20*20=440
resize(tmpIn, lowData, Size(sizeData, sizeData));//英文字符尺寸size:10*10
// Histogram features
Mat vhist = ProjectedHistogram(lowData, VERTICAL);//垂直投影
Mat hhist = ProjectedHistogram(lowData, HORIZONTAL);/水平投影
int numCols = vhist.cols + hhist.cols + lowData.cols * lowData.cols;
Mat out = Mat::zeros(1, numCols, CV_32F);
int j = 0;
//将这些特征填入out,用于ANN
for (int i = 0; i < vhist.cols; i++) {
out.at<float>(j) = vhist.at<float>(i);
j++;
}
for (int i = 0; i < hhist.cols; i++) {
out.at<float>(j) = hhist.at<float>(i);
j++;
}
for (int x = 0; x < lowData.cols; x++) {
for (int y = 0; y < lowData.rows; y++) {
out.at<float>(j) += (float)lowData.at <unsigned char>(x, y);
j++;
}
}
return out;//返回字符特征向量
}
对于中文字符和英文字符,默认的图块大小是不一样的,中文字符默认是 20*20,非中文默认是10*10。
- GetCenterRect 函数主要用于获取字符的边框,分别查找从四个角落查找字符的位置;
- CutTheRect 函数裁剪原图,即将字符移动到图像的中间位置,通过这一步的操作,可将字符识别的准确率提高5%左右;
- ProjectedHistogram 函数用于获取归一化序列,归一化到0-1区间范围内;
3、
void CharsIdentify::LoadModel(std::string path) {
if (path != std::string(kDefaultAnnPath)) {
if (!ann_->empty())
ann_->clear();
LOAD_ANN_MODEL(ann_, path);
}
}
void CharsIdentify::LoadChineseModel(std::string path) {
if (path != std::string(kChineseAnnPath)) {
if (!annChinese_->empty())
annChinese_->clear();
LOAD_ANN_MODEL(annChinese_, path);
}
}
void CharsIdentify::LoadGrayChANN(std::string path) {
if (path != std::string(kGrayAnnPath)) {
if (!annGray_->empty())
annGray_->clear();
LOAD_ANN_MODEL(annGray_, path);
}
}
4、
std::pair<std::string, std::string> CharsIdentify::identify(cv::Mat input, bool isChinese, bool isAlphabet) {
cv::Mat feature = charFeatures(input, kPredictSize);
float maxVal = -2;
auto index = static_cast<int>(classify(feature, maxVal, isChinese, isAlphabet));
if (index < kCharactersNumber) {
return std::make_pair(kChars[index], kChars[index]);
}
else {
const char* key = kChars[index];
std::string s = key;
std::string province = kv_->get(s);
return std::make_pair(s, province);
}
}
5、单例模式
单例模式(Singleton Pattern)涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
5.1、单例模式的懒汉模式
所谓懒汉模式,就是在需要用到创建实例了程序再去创建实例,不需要创建实例程序就“懒得”去创建实例,这是一种时间换空间的做法,这体现了“懒汉的本性”。如果有多个线程,同时调用了获取实例,那么可能就会产生多个实例,那么这就不是我们的单例模式了。所以我们需要做一些事情才能保证我们是一个单例类。
对于饿汉模式来说,它在单例类内部定义了一个类对象的指针,在初始化这个指针的时候,直接调用new在堆上申请了空间。于是达到了这种效果,饿汉就是类一旦加载,就把单例初始化完成,在进入main函数之前,这个单例类的实例已经创建好了参考:单例模式——饿汉模式。
而对于懒汉模式而言,我们可以不让这个类对象指针在初始化的时候就new,而是给它赋一个NULL,那这样,在进入main函数之前,这个类对象指针只是一个空指针,并没有产生实际的对象。而当我们在程序中调用instance()函数时,就需要进行一个判断,如果该类对象指针为空,那么我们就需要调用new创建一个对象,而如果该类对象指针不为空,那么我们就不用创建对象直接返回该指针就好了。根据这个思路,我们实现了下面的代码(因为我们的静态成员变量采用的是类对象的指针而不是类对象,因此我们需要写一个垃圾回收机制,这里我们采用的是上一篇文章的方法二,即实现一个内部的垃圾回收类)
头文件声明:
class CharsIdentify {
public:
static CharsIdentify* instance();
private:
CharsIdentify(); //通过创建私有构造函数这样是可以保证单例。
}
源文件定义:
CharsIdentify* CharsIdentify::instance_ = nullptr;//定义一个空的类对象指针
CharsIdentify* CharsIdentify::instance() {
if (!instance_) {
instance_ = new CharsIdentify;
}
return instance_;
}
类加载时类的初始化和创建实例时的初始化顺序
1、虚拟机在首次加载类时,会对静态初始化块、静态成员变量、静态方法进行一次初始化
2、只有在调用new方法时才会创建类的实例