CvANN_MLP(OpenCV 的神经网络-多层感知器) 进行路牌判别

原载于 用 CvANN_MLP 进行路牌判别


这原是智能计算课程大作业。

(有时候我真的不知道怎么措辞,应该用“分类识别”,还是“判别”,还是“断别”?不在意这些细节。我说的是区分一张图片是不是路牌,就这么简单。)

使用 ANN-MLP(神经网络--多层感知器)方法。利用 Qt4、OpenCV2 程序库,进行路牌的抠取、分类和识别。开源在 GitHub:district10/SignProcessing: 路牌提取、分类,包括源码、文档、测试数据和可执行文件。其中的qt4cv3vs2015分支将 OpenCV 更新到了 OpenCV3,重新梳理、整合了各个模块。

快速入门

文档贴在这里,原载 Issues · district10/SignProcessing

下载二进制

DLL 依赖

Qt 和 OpenCV 的 dll,以及 VS2015 的 runtime,点此下载 (11.2 MB),和下面的二进制执行档放到一起即可。

二进制执行档

如何用已训练模型来测试图片是否是路牌

SignProcessorDemo.exe

这个 exe 有三个功能,主要是用已经训练好的模型来预测(predict)。

功能 1,判别单张图片

加载一个图片,如果判定是一个路牌,用淡蓝色显示“Sign (pos)”:

如果不是一个路牌,用红色显示“Not Sign (neg)”:

功能 2,判别文件夹下所有图片

批量判断。选择一个文件夹(这里是 dir_of_images)。

判断结果输出在文本框内,包括图片数目、每张图片的路径以及判别结果:

To Predict 38 images.


    processing D:/tzx/git/SignProcessing/data/input/dir_of_images/0002-team7 (109)-shift-sx-36-sy-16.bmp... done. assigned to [pos]
    ...
    processing D:/tzx/git/SignProcessing/data/input/dir_of_images/dir2/dir22/0001-team7 (108)-rand-cx0623-cy0398-r135.bmp... done. assigned to [neg]

所选文件夹同一目录下还会出现两个目录 posneg,分别把判别后的图片拷贝到里里面:

功能 3,判别选择的多张图片

判别为路牌的,标记“Pos”,不是路牌的,标记“Neg”。

默认用了
data/output/4.xml
来预测,你也可以点击【Load Another XML】加载别的已训练模型。


1)双击运行 SignClassifierDemo.exe

2)加载模型

点击【Load Trained】选择 XML 文件(在 data/output 文件夹)。
我这里选择了 4.xml

3)选择想要测试的图片

点击【Unknown】,选择一些图片(在 data/input/tests 文件夹)。

如下图,

还没有预测的图片下面有“???”标记,它表示还未测试。

然后点击【Predict】。

有的标记变成了“Pos”(是路牌),有得标记变成了“Neg”(不是路牌),
如下图:

如何从数据训练模型

1)双击运行 SignClassifierDemo.exe

2)添加正负样本

点击【Pos】,选择一些正样本。(可用 <kbd>Control+A</kbd> 全选)

同理,点击【Neg】,选择一些负样本。(加载可能有点慢)

3)训练

点击【Train】,训练开始。训练完成后,会提示已经训练的量。

记得点击【Save Trained】保存训练参数,下次就可以直接用【Load Trained】加载它了。

试试加载一些 Unknown,测试一下它的判断是否正确。


就是这样。

从源码编译

具体见 代码编译 · Issue #3 · district10/SignProcessing

更进一步

可以看到,看到程序能基本准确地分辨哪些是路牌,哪些不是。现在我从数据采集、处理,程序编写来把整个流程说一遍。

源数据的处理

源数据是我们在武汉街头拍摄的路牌图片。大概像这样:

因为源图片里面有车牌之类的没有打码(哪有这空啊……),就不分享了。

从源图片中提取正负样本,用的是我们组写的 Sign Cutter(路牌切割)程序(源码),

Sign Cutter

利用它可以很快速地导入源影像,并进行切片处理。【保存切片】操作会把框选区域保存成一个切片图,作为我们人工选取的正样本。同时,正样本需要转化到 24 × 24 的 RGB 图像,为保证图片不失真,还要将之存储为 BMP 格式。

![正样本为人工选取的路牌][cutbmp]

因为正样本的数量有限,需要通过平移、旋转、镜像(镜像后再平移旋转)等方式从一张正样本产生多张类似正样本。在 SignCutter 中通过点击【生成正样本】实现。

![posnegclick]

负样本则是从图中非正样本区域,随机选取中心点和旋转角度,选取而来。正负样本选取比例我们设置为 1:7。通过点击【生成负样本】来快速地生成一系列负样本。结果就是,一张源图片,一个人工框选区域,生成了 87 张正样本,602 张负样本。最终我们生成了 8 组共 25,230 张正样本,69,894 张负样本,用于训练和检测。

你可以下载 图片索引,每一行代表一个样本,后面的 1 表示“是路牌”,0 表示“不是路牌”,大概长这样:

1/pos/0001-team7 (1)-___.bmp, 1
1/pos/0001-team7 (1)-___-r000.bmp, 1
1/pos/0001-team7 (1)-___-r030.bmp, 1
...
8/neg/0025-team7 (292)-rand-cx0469-cy2820-r108.bmp, 0
8/neg/0025-team7 (292)-rand-cx0471-cy2508-r232.bmp, 0
8/neg/0025-team7 (292)-rand-cx0473-cy2701-r358.bmp, 0

图片数据在这里下载:

![正样本 87 张 & 负样本 602 张][posneg]

为了保证得到的正样本足够好,我们不是在切片上进行这些操作,而是在源影像上,这就避免了旋转平移后图片中存在空白。这通过一个 SignLogger 模块实现(源码),它记录了切片的源图片、归一化了的中心点、归一化了的宽度和高度。

![通过点击【查看切片记录】查看切片信息][cuttedoutinfo]

![这就是刚才那张路牌切片的 log 信息][posinfo]

整个正样本和负样本如图,红框内为切片区域,绿色矩形为正样本框(较密集),蓝色为负样本框(分散在源影像中非正样本区域)。

![signcutdemo]

从文件名也能看出每个切片的信息,这里是正负样本文件名的 Sample^[完整版可以在 http://gnat.qiniudn.com/sc/info-all.txt 下载。]

原图片 team7 (148).jpg 产生的正负样本

.
├── pos                            (正样本 87 张)
│   ├── 0041-team7 (148)-___.bmp                 原切片
│   ├── 0041-team7 (148)-___-r030.bmp            原切片旋转 30 度
│   ├──..............................
│   ├── 0041-team7 (148)-___-r330.bmp
│   ├── 0041-team7 (148)-L-R.bmp                 左右镜像
│   ├── 0041-team7 (148)-L-R-r030.bmp            左右镜像后,旋转 30 度
│   ├── 0041-team7 (148)-L-R-r060.bmp
│   ├── 0041-team7 (148)-L-R-r090.bmp
│   ├── .............................
│   ├── 0041-team7 (148)-L-R-r330.bmp
│   ├── 0041-team7 (148)-shift-sx000-sy003.bmp   平移左右 0%,上下 3%
│   ├── .............................
│   ├── 0041-team7 (148)-shift-sx-10-sy-10.bmp
│   ├── 0041-team7 (148)-U-D.bmp                 上下镜像
│   ├── .............................
│   └── 0041-team7 (148)-U-D-r330.bmp
├── neg                            (负样本 602 张)
│   ├── 0041-team7 (148)-rand-cx0057-cy1450-r305.bmp 随机中心点、旋转角度
│   ├── ............................................
│   └── 0041-team7 (148)-rand-cx2393-cy2979-r299.bmp
└── team7 (148)-demo.jpg           (展示了正负样本切片位置)

2 directories, 696 files

有了正负样本,就看用 OpenCV 提供的 ANN_MLP 进行基于神经网络方法的学习。这部分知识附在本文末尾。

从编码的角度,利用神经网络多层感知器分类的实际比图片预处理、正负样本的生成要简单,代码量也少很多。所以我从代码大概说一下使用方法。

首先,引入 OpenCV3 相关头文件:

#include <opencv2/core.hpp>
#include <opencv2/ml.hpp>

我封装了一个 MLP 类,提供简单的训练接口(源码)。这里是精简了的类声明:

class MLP
{
public:
    MLP();                                          // 构造函数,mlp 的初始化
    ~MLP() { }

    void loadXML( const QString &xml  );            // 加载已经训练好的模型
    void saveXML( const QString &xml  );            // 保存训练好的模型
    void loadCSV( const QString &csv );             // 加载正负样本用于训练
    void train();                                   // 训练

    // 最后,对图片进行判别:是否为路牌
    //  输入为一串图片路径
    //  输出为图片路径和一个标志,true 说明图片为路牌,false 说明不是
    QList<QPair<QString, bool> > predictImages( const QStringList &images );

private:
    cv::Ptr<cv::ml::ANN_MLP> mlp;                   // 存储了参数
};

其中在构造函数中,要实例化 mlp,还要对它进行配置:

mlp = cv::ml::ANN_MLP::create();
// 层数和每层 neuron 个数是“精选”出来的
mlp->setLayerSizes( (cv::Mat)(cv::Mat_<int>(1,5)
                                << FEATURENUM, FEATURENUM / 2, FEATURENUM / 6, FEATURENUM / 24, 1) );

// 激活函数设置为 sigmoid
mlp->setActivationFunction( cv::ml::ANN_MLP::SIGMOID_SYM, 1.0, 1.0 );

// 训练方法为反向传播
mlp->setTrainMethod( cv::ml::ANN_MLP::BACKPROP, 0.1, 0.1 );
mlp->setTermCriteria( cv::TermCriteria(
                            cv::TermCriteria::COUNT + cv::TermCriteria::EPS
                            , 5000
                            , 0.01 ) );

激活函数和反向传播的说明见本文末尾附录。反向传播一个比较好的文档见我的笔记:Principles of training multi-layer neural network using backpropagation

Layer 的层数和每层的 neuron 数目对训练结果有较大影响,这是我之前测试后画的 Excel 表格:^[顺便学习了 Excel 的 minimap 的用法哈哈。]

CvANN::MLP 中神经网络层数对正确率的影响
CvANN::MLP 中 layerSizes(即每层中 neuron 的个数)对正确率的影响

设置好了,就可以用已经准备好的正负样本训练它。我们先不考虑的是如何从 24 × 24 的 RGB 图片,生成 feature 向量(也就是这里的 Utils::img2feature 函数的实现细节)。

// pos 和 neg 是图片路径列表,分别是正负样本集合
int np = pos.length();
int nn = neg.length();

// 生成数据集,这里的 features 和 flags 就是机器学习里
// 常说的 data 和 label
float *features = new float[FEATURENUM*(np + nn)];
float *flags    = new float[np + nn];

// 用图片初始化 features,并设置好 labels
for ( int i = 0; i < np; ++i ) {
    Utils::img2feature(qPrintable(pos.at(i)), features + FEATURENUM*i);
    *(flags + i)        =       1.0f;       // 正样本为 1
}
for ( int i = 0; i < nn; ++i ) {
    Utils::img2feature(qPrintable(neg.at(i)), features + FEATURENUM*np+FEATURENUM*i);
    *(flags + np + i)   =       -1.0f;      // 负样本为 -1
}

// 存到 OpenCV 的矩阵结构 Mat 里,并生成数据集
cv::Mat featureMat( np+nn, FEATURENUM,  CV_32F, features );
cv::Mat flagMat(    np+nn, 1,           CV_32F, flags );
cv::Ptr<cv::ml::TrainData> trainSet // mat 的每一行代表一条数据
    = cv::ml::TrainData::create( featureMat, cv::ml::ROW_SAMPLE, flagMat );

// 然后就可以训练了
mlp->train( trainSet );

// 不要忘了释放内存
delete[] features;
delete[] flags;

现在就能测试,

float feature[FEATURENUM];
Utils::img2feature( "path/to/image", feature );

cv::Mat featureMat  ( 1, FEATURENUM,    CV_32F, feature );
cv::Mat result      ( 1, 1,             CV_32FC1 );

mlp->predict( featureMat, result );
float *p = result.ptr<float>(0);

这个 *p 大于 0,则图片被判定为路牌,否则不是路牌。

我们还可以将训练好的模型保存起来,方便下载加载:

// 保存
mlp->save( "path/to/save/model(xml file)" );

// 加载
mlp = cv::Algorithm::load<cv::ml::ANN_MLP>( "path/to/saved-model.xml" );

我训练了几组数据,得到的 xml 文件见 data/output 文件夹。大概长这样。

最后我们看看那个 Utils::img2feature 函数的实现(这里加上了额外的注释,做了额外的对齐):

// feature 已经分配好内存了,内存大小是 sizeof(float)*FEATURENUM
// FEATURENUM 是一个宏,定义为
//      #define FEATURENUM  ( 24*(3+3)+ 3 + 4*3 )
// 看了后面的数据处理你大概能懂这些数字每个代表什么

bool Utils::img2feature( const char *filePath, float *feature )
{
    Mat img = imread( filePath, cv::IMREAD_COLOR );
    if ( img.rows != IMGSIZE || img.cols != IMGSIZE ) {
        qDebug() << __FUNCDNAME__ << "failed, because of wrong dim," << "\tfilepath:" << filePath;
        return false;
    }

    int allCountR          =   0  , allCountG          =   0  , allCountB          =   0  ;
    int rowCountR[IMGSIZE] = { 0 }, rowCountG[IMGSIZE] = { 0 }, rowCountB[IMGSIZE] = { 0 };
    int colCountR[IMGSIZE] = { 0 }, colCountG[IMGSIZE] = { 0 }, colCountB[IMGSIZE] = { 0 };
    int tempR[4]           = { 0 }, tempG[4]           = { 0 }, tempB[4]           = { 0 };

    for( int i = 0; i < IMGSIZE; ++i ) {
        for( int j = 0; j < IMGSIZE; ++j ) {

            int b = img.at<cv::Vec3b>(i,j)[0];
            int g = img.at<cv::Vec3b>(i,j)[1];
            int r = img.at<cv::Vec3b>(i,j)[2];

            allCountR += r;     rowCountR[i] += r;     colCountR[j] += r;
            allCountG += g;     rowCountG[i] += g;     colCountG[j] += g;
            allCountB += b;     rowCountB[i] += b;     colCountB[j] += b;

            /*
             * 分成四个部分,统计各自区域内的 rgb 比例,四个区域低编号为:
             *
             *           0 | 1
             *           --+--
             *           2 | 3
            */
            int index = 2 * (i < IMGSIZE / 2 ? 0 : 1) + (j < IMGSIZE / 2 ? 0 : 1);
            tempR[index] += r;
            tempG[index] += g;
            tempB[index] += b;
        }
    }

    for ( int i=0; i < IMGSIZE; ++i ) {
        feature[i*6+0] = (float)rowCountR[i]/allCountR; // 第 i 行的红色占全图红色的比例
        feature[i*6+1] = (float)rowCountG[i]/allCountG; // 绿色
        feature[i*6+2] = (float)rowCountB[i]/allCountB; // 蓝色
        feature[i*6+3] = (float)colCountR[i]/allCountR; // 第 i 列
        feature[i*6+4] = (float)colCountG[i]/allCountG;
        feature[i*6+5] = (float)colCountB[i]/allCountB;
    }
    // rgb 三种颜色的比例
    feature[IMGSIZE*6+0] = (float)allCountR / (allCountR + allCountG +allCountB);
    feature[IMGSIZE*6+1] = (float)allCountG / (allCountR + allCountG +allCountB);
    feature[IMGSIZE*6+2] = (float)allCountB / (allCountR + allCountG +allCountB);

    for ( int i = 0; i < 4; ++i ) {
        // 区域内的 rgb 比例
        int total = tempR[i] + tempG[i] + tempB[i];
        feature[IMGSIZE*6+3 + 3*i+0] = (float)tempR[i] / total;
        feature[IMGSIZE*6+3 + 3*i+1] = (float)tempG[i] / total;
        feature[IMGSIZE*6+3 + 3*i+2] = (float)tempB[i] / total;
    }
    return true;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • 推荐一篇非常详细的对BP的解答:http://www.2cto.com/kf/201610/553336.html...
    碧影江白阅读 3,554评论 0 6
  • 在GAN的相关研究如火如荼甚至可以说是泛滥的今天,一篇新鲜出炉的arXiv论文《Wasserstein GAN》却...
    MiracleJQ阅读 2,218评论 0 8
  • HOG+ADABOOST训练方式网上资料很多,这篇文章是在小编训练过程中遇到的一些问题加以总结。 首先,在准备文件...
    Vivian_yolo阅读 3,219评论 2 5
  • 考试说明 注重基础知识和概念的理解,因此解题中的计算过程不会很复杂,但是会有推公式的过程。本课程的重点知识包括:贝...
    艺术叔阅读 2,820评论 0 3
  • 注:题中所指的『机器学习』不包括『深度学习』。本篇文章以理论推导为主,不涉及代码实现。 前些日子定下了未来三年左右...
    我偏笑_NSNirvana阅读 39,911评论 12 145