用 CvANN_MLP(OpenCV 的神經網絡-多層感知器) 進行路牌判別

原載於 用 CvANN_MLP 進行路牌判別html


這原是智能計算課程大做業。git

(有時候我真的不知道怎麼措辭,應該用「分類識別」,仍是「判別」,仍是「斷別」?不在乎這些細節。我說的是區分一張圖片是否是路牌,就這麼簡單。)github

使用 ANN-MLP(神經網絡--多層感知器)方法。利用 Qt四、OpenCV2 程序庫,進行路牌的摳取、分類和識別。開源在 GitHub:district10/SignProcessing: 路牌提取、分類,包括源碼、文檔、測試數據和可執行文件。其中的qt4cv3vs2015分支將 OpenCV 更新到了 OpenCV3,從新梳理、整合了各個模塊。網絡

快速入門

文檔貼在這裏,原載 Issues · district10/SignProcessing機器學習

下載二進制

DLL 依賴函數

Qt 和 OpenCV 的 dll,以及 VS2015 的 runtime,點此下載 (11.2 MB),和下面的二進制執行檔放到一塊兒便可。post

二進制執行檔學習

如何用已訓練模型來測試圖片是不是路牌

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】,選擇一些正樣本。(可用 Control+A 全選)

同理,點擊【Neg】,選擇一些負樣本。(加載可能有點慢)

3)訓練

點擊【Train】,訓練開始。訓練完成後,會提示已經訓練的量。

記得點擊【Save Trained】保存訓練參數,下次就能夠直接用【Load Trained】加載它了。

試試加載一些 Unknown,測試一下它的判斷是否正確。


就是這樣。

從源碼編譯

具體見 代碼編譯 · Issue #3 · district10/SignProcessing

更進一步

能夠看到,看到程序能基本準確地分辨哪些是路牌,哪些不是。如今我從數據採集、處理,程序編寫來把整個流程說一遍。

源數據的處理

源數據是咱們在武漢街頭拍攝的路牌圖片。大概像這樣:

由於源圖片裏面有車牌之類的沒有打碼(哪有這空啊……),就不分享了。

從源圖片中提取正負樣本,用的是咱們組寫的 Sign Cutter(路牌切割)程序(源碼),

Sign Cutter

利用它能夠很快速地導入源影像,並進行切片處理。【保存切片】操做會把框選區域保存成一個切片圖,做爲咱們人工選取的正樣本。同時,正樣本須要轉化到 24 × 24 的 RGB 圖像,爲保證圖片不失真,還要將之存儲爲 BMP 格式。

正樣本爲人工選取的路牌

由於正樣本的數量有限,須要經過平移、旋轉、鏡像(鏡像後再平移旋轉)等方式從一張正樣本產生多張相似正樣本。在 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 張

爲了保證獲得的正樣本足夠好,咱們不是在切片上進行這些操做,而是在源影像上,這就避免了旋轉平移後圖片中存在空白。這經過一個 SignLogger 模塊實現(源碼),它記錄了切片的源圖片、歸一化了的中心點、歸一化了的寬度和高度。

經過點擊【查看切片記錄】查看切片信息

這就是剛纔那張路牌切片的 log 信息

整個正樣本和負樣本如圖,紅框內爲切片區域,綠色矩形爲正樣本框(較密集),藍色爲負樣本框(分散在源影像中非正樣本區域)。

![signcutdemo]

從文件名也能看出每一個切片的信息,這裏是正負樣本文件名的 Sample^[完整版能夠在 http://gnat.qiniudn.com/sc/in... 下載。]

原圖片 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;
}
相關文章
相關標籤/搜索