[OPENCV] FaceDectection - (四) 模型訓練

前面幾篇關於FaceDetection的文章已經介紹如何應用openCV內建的 library
以及分析臉部辨識所需要的xml檔案

這一篇文章將會介紹如何產生出 xml 檔案提供給 openCV使用

獲取人臉資料庫

因為要重新訓練出模型, 因此需要有大量的人臉圖案
喵使用了FERET的資料庫

要獲取這人臉資料不用什麼太繁複的手續
但也沒有簡單地直接下載就可以了
按照網站內容的描述
只需要寫封 e-mail過去, 自動審核過就會提供下載點

簡易的方式如下
To: colorferet@nist.gov(link sends e-mail) 
標題: Color FERET download request 
內容:Please create an account that will allow me to download the Color FERET database. I will abide by the Release Agreement version 1.

寄完信件之後, 就去散個步, 喝個下午茶
很快就會收到回信, 接著由回信當中的下載點將資料庫下載回來
另外, 在訓練模型的時候, 也需要提供 negative的圖檔
這圖檔可以在這邊取得negatives

萃取人臉資料

下載完的資料庫, 可以在當中找到資料夾名稱為 ground_truth
FERET/colorferet/dvd1/data/ground_truths
FERET/colorferet/dvd2/data/ground_truths

這ground_truths裡頭就有相關的人臉資料
喵隨意取一個當作範本
FERET/colorferet/dvd1/data/ground_truths/xml/00001/00001_930831_fb_a.xml

範本
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Recordings SYSTEM "http://hbase.humanid.org/hbase/dtd/recording.dtd">
<Recordings>
<Recording id="cfrR00003">
  <URL root="Disc1" relative="data/images/00001/00001_930831_fb_a.ppm.bz2"/>
  <CaptureDate>08/31/1993</CaptureDate>
  <CaptureTime>00:00:00</CaptureTime>
  <Format value="ppm" scanning="Progressive" compression="bzip2"/>
  <Subject id="cfrS00001">
   <Application>
    <Face>
     <Pose name="fb" yaw="0" pitch="0" roll="0"/>
     <Wearing glasses="Yes"/>
     <Hair beard="No" mustache="No" source="Retrospectively"/>
     <Expression name="fb"/>
     <LeftEye x="328" y="324"/>
     <RightEye x="204" y="328"/>
     <Nose x="270" y="392"/>
     <Mouth x="272" y="460"/>
    </Face>
   </Application>
   <Stage id="cfrT00001"/>
  </Subject>
  <Collection id="cfrC00001"/>
  <Environment id="cfrE00001"/>
  <Sensor id="cfrN00002"/>
  <Illuminant id="cfrI00001"/>
  <Illuminant id="cfrI00002"/>
  <Illuminant id="cfrI00003"/>
  <Weather condition="Inside"/>
 </Recording>
</Recordings>

由這xml檔案萃取出左眼, 右眼, 鼻子 以及嘴巴的位子, 當中也有檔案所在位置
注意, 位置大小是以左上角為(0,0)
接著就是寫一個python script將這些資料轉化成 openCV 訓練模型所用的檔案
在詳細寫python script之前, 先了解一下openCV已經提供了哪些執行檔可以幫助我們訓練模型, 這樣再回頭寫 script才知道要產生出什麼

OpenCV 模型訓練相關程式

非常好心的openCV不只提供給了face detection的library, 其實也內含了幾個幫助訓練模型的程式, 喵一個個介紹


opencv_annotation 

使用在手動勾選出人臉的位置來產生出 annotation file, 這檔案將可以成為 opencv_createsamples所用
相關的參數
--annotations : file path to output annotations
--images: image path, it could be file path or folder path

EXx
  opencv_annotation --annotations=annotations_manual.txt --images="out/00002_940928_fa.png"

可以先拿一個檔案玩一下, 類似上述的範例, 在圖形介面上可以使用
c : confirm
n : next image
畫面會是這樣


產生出來的 file則是

$cat annotations_manual.txt
out/00002_940928_fa.png 1 122 259 287 333

排列方式是
file_path number_of_object x y width height
這邊要注意一下的是圖的左上方是為 (x,y) = (0, 0)的點


opencv_createsamples

需有 annotations檔案以及原始的圖檔, 加上 negatives的圖庫之後, 可以產生出 vec檔案, 這檔案將會被 opencv_traincascade 使用
相關參數
-vec : output vector file
-info : annotations file
-w : vector image width
-h : vector image height
-num : positive file's number, 可以看作是從annotations.xml當中所拿取出來使用的檔案數
-bg : negative file

Ex:
  opencv_createsamples -vec positive_trainingfaces_24-24.vec -info annotations.txt -w 24 -h 24 -num 2500 -bg negative.txt

對了, negative.txt當中只需要記載image file path, 範例如下

negative.txt
---
negatives/neg-4850.jpg
negatives/neg-4851.jpg
negatives/neg-4852.jpg
negatives/neg-4854.jpg
negatives/neg-4855.jpg
negatives/neg-4856.jpg
negatives/neg-4857.jpg

opencv_traincascade

開始訓練模型, 需要輸入 vector file, negative file
相關參數
-data : 輸出 xml file的資料夾
-vec : vector file
-bg : negative file
-numStages : 所要訓練的階層數目
-w : vector image width (需與vector file當時所創立的 width一樣)
-h : vector image height (需與vector file當時所創立的 height一樣)
-numPos : postive file number (必須要小於 vector 所含的數量)
-featureType : 預設為 Haar, 可以使用 LBP (可以不用這參數)

Ex:
  opencv_traincascade -data cascade_out -vec positive_trainingfaces_24-24.vec -bg negative.txt -numStages 22 -w 24 -h 24 -numPos 2200 -numNeg 1000 -featureType LBP

實際操作

按照上述的幾個功能, 可以歸納出步調如下


第一步就是要得到 annotations.txt, 因此也是回到了一開始我們資料萃取的那邊, 有了人臉的幾個特徵之後, 就可以使用python之類的語言將 database當中的資料, 轉化整理成為
1. annotations.txt
2. image collection (png format)

接著第二步跟第三步驟都接著上述的範例做就可以了
最後將生成出來的 cascade.xml 拿來取代https://codacat.blogspot.tw/2017/11/opencv-facedetection.html 當中所寫的 xml file

但是呢
事情有時候就是沒這麼順利....

問題與對策

按照這些步驟跟步調, 喵花了點時間寫了一個python可以從FERET下載的資料庫中截取出幾千個annotations file以及 image file.
但是不管怎樣都無法正確的辨識, 花了喵幾個星期 (喵都是星期一放著跑training, 隔幾天看看狀況)
所以問題來了

Q1. 為什麼按照步驟做了, 卻連一丁點都無法辨識出人臉? 喵的勒

對策: 回頭好好思考過之後, 發現vector的大小是正方形, 可是我們萃取出來的annotations.txt當中所記載的 width, height不是正方形, 所以...  仔細地思考一下之後驚覺~
annotations.txt當中所記載的 width and height 必須是正方形才能夠提升辨識度

Q2. 按照步驟Q1的對策做了之後, 確實可以提升了辨識度, 但是還是不夠準確, 該怎樣提升?

對策: 辨識度要提升到很高, 有幾個方向可以努力

  1. 增加 postive 以及 negative 量
  2. 使用當地的資料庫, 避免膚色, 輪廓等的差異 (就像是我們使用外國人的資料庫訓練出來模型之後, 拿他來辨識在地人, 辨識度一定不夠好)
  3. 將 vector size提高
  4. 改變 stage numbers
Q3. 每次訓練起來都很久, 有什麼辦法可以加快速度

對策: 當然最快的方式就是升級硬體, 但是喵還是推薦免錢的方法, 那就是先使用 LBP的方式訓練一次之後, 如果方向對了, 在把 featureType這選項去掉, 放著給他跑

Q4. 我怎麼知道annotations.txt當中所載明的區域就是人臉所在? 尤其使用程式萃取往往會有出乎意外的結果...

對策: 自己寫一個讀取 annotations.txt, 然後標示出方框的位置, 喵寫的就在這邊給大家參考

#include <opencv2/opencv.hpp>
#include <stdio.h>
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
using namespace cv;

String window_name = "Face Window";

/**
 * Detects faces and draws an ellipse around them
 */
int main(int argc, char** argv) {

    string annotationFile;
    string directory;
    if (argc < 3) { // Check the value of argc. If not enough parameters have been passed, inform user and exit.
        std::cout << "Usage is -f <annotation file>\n"; // Inform the user of how to use the program
        std::cin.get();
        exit(0);
    } else { // if we got enough parameters...
        char *myPath;
        char *myOutPath;
        std::cout << argv[0];
        for (int i = 1; i < argc; i++) { 
            if (i + 1 != argc) {
                if (strcmp(argv[i] , "-f") == 0) {
                    // We know the next argument *should* be the filename:
                    annotationFile = string(argv[i + 1]);

                    const size_t last_slash_idx = annotationFile.rfind('/');
                    if (std::string::npos != last_slash_idx)
                    {
                        directory = annotationFile.substr(0, last_slash_idx+1);
                    }
                } else {
                    std::cout << "Not enough or invalid arguments, please try again.\n";
                    exit(0);
                }
            }
        }
        std::cout << annotationFile ;
        printf("annotatilFile is %s\n", annotationFile.c_str());
        printf("directory is %s\n", directory.c_str());

    }
    printf("Jay test 1\n");
    // open file
    std::ifstream infile(annotationFile.c_str());
    std::string line;
#if 1
    while (std::getline(infile, line)) {
        std::istringstream iss(line);
        string imgFilePath;
        int objectCount = 0;
        int x = 0;
        int y = 0;
        int width = 0;
        int height = 0;
        if (!(iss >> imgFilePath >> objectCount >> x >> y >> width >> height)) {
            break;
        }; //error
        string absolutePath = directory + (imgFilePath);
        printf("%s, x %d, y %d, width %d, height %d\n", absolutePath.c_str(), x, y, width, height);
//        printf("%s, x %d, y %d, width %d, height %d\n", absolutePath.c_str(), x, y, width, height);
        printf("%s\n", line.c_str());
        {
            Mat inputImage = imread(absolutePath);
            Point origin(x, y);
            Point diagonal(x+width, y+height);
            rectangle(inputImage, origin, diagonal, Scalar( 255, 0, 255 ));
            imshow(window_name, inputImage);  // Display frame
            int k = waitKey(0);
            if(k == 27){ //ESC is 27
                printf("%s, x %d, y %d, width %d, height %d\n", absolutePath.c_str(), x, y, width, height);
                break;
            }
        }
    }
#endif
    return 0;
}
這小程式需要參數 -f 指名 annotations file的位置
 ./Debug/P06-ValidateImg -f /home/jay/face_detection/annotations.txt
其中會去 parsing annotations 所記載相對位置的 image, 所以這邊也要請各位注意~

這篇的文章有點長, 但也是因為幾個星期積累下來的東西比較多
大家看完應該感覺對 face dectection更了解一些了吧~
喵~~

沒有留言:

張貼留言

OpenGL 閱讀筆記 (二) OpenGL基本操作

這邊虎喵跳過glfw/glew的初始化, 先來提一下OpenGL的基本操作方式 前面也提到過, OpenGL是一個類C的語言, 因此使用C/C++的攻城獅們應該會感到很熟悉. OpenGL的基本動作循環如下: 每一行code的解釋如下: // 本地變數,...