easyPR源码解析之chars_identify.h

     在上一篇文章的介绍中,我们已经通过相应的字符分割方法,将车牌区域进行分割,得到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方法时才会创建类的实例

猜你喜欢

转载自blog.csdn.net/qq_30815237/article/details/89320071