OpenCV — 人臉識別

南丶煙發表於2017-01-13

前段時間弄過一下人臉識別相關的東西,記錄一下

撰寫不易,轉載需註明出處:http://blog.csdn.net/jscese/article/details/54409627本文來自 【jscese】的部落格!

概念

FaceDetect
人臉檢測 在一張影象中判斷是否存在人臉並找出人臉所在的位置

FaceRecognize
人臉識別 在人臉檢測的基礎上收集人臉資料集合進行處理儲存資訊,將輸入人臉與儲存的資訊進行比對校驗,得到是否為其中某個人臉

特徵值
以某種特定規則對輸入源進行處理得到具有唯一性質量化的值,在人臉識別中特徵值的提取有
HOG-方向梯度直方圖 , HAAR-like特徵 , LBP-區域性二進位制模式 三種方法
前面轉載的部落格有介紹:影象特徵提取三大法寶:HOG特徵,LBP特徵,Haar特徵

分類器
根據特徵值界定輸入事物是否屬於已知某種類別的過濾條件組合,未知類別的算聚類器,弱分類器:分類器的正確率高於隨機分類(50%),強分類器:能滿足預期分類並且正確率很高的分類器

Adaboost
迭代演算法,同一個訓練集合下訓練多個弱分類器,把弱分類器迭代組合成一個強分類器
演算法具體原理實現:
這裡寫連結內容
這裡寫連結內容

級聯分類器
將多個同型別的分類器聯合起來進行推算整合以得到符合目標的最終分類器的方法
最終的分類處理流程如下圖:
這裡寫圖片描述

adaboost訓練出來的強分類器一般具有較小的誤識率,但檢測率並不很高,一般情況下,高檢測率會導致高誤識率,這是強分類閾值的劃分導致的,要提高強分類器的檢測率既要降低閾值,要降低強分類器的誤識率就要提高閾值

級聯強分類器的策略是,將若干個強分類器由簡單到複雜排列,希望經過訓練使每個強分類器都有較高檢測率,而誤識率可以放低,比如幾乎99%的人臉可以通過,但50%的非人臉也可以通過,這樣如果有20個強分類器級聯,那麼他們的總識別率為0.99*20 98%,錯誤接受率也僅為0.5*20 0.0001%。這樣的效果就可以滿足現實的需要了

設K是一個級聯檢測器的層數,D是該級聯分類器的檢測率,F是該級聯分類器的誤識率,di是第i層強分類器的檢測率,fi是第i層強分類器的誤識率。如果要訓練一個級聯分類器達到給定的F值和D值,只需要訓練出每層的d值和f值,這樣:
D*K = D,f*K = F

級聯分類器的要點就是如何訓練每層強分類器的d值和f值達到指定要求
演算法虛擬碼如下:

1)設定每層最小要達到的檢測率d,最大誤識率f,最終級聯分類器的誤識率Ft;
2)P=人臉訓練樣本,N=非人臉訓練樣本,D0=1.0,F0=1.03i=04for : Fi>Ft
   ++i;
     ni=0;Fi=Fi-1;
   for : Fi>f*Fi-1
     ++ni;
    利用AdaBoost演算法在P和N上訓練具有ni個弱分類器的強分類器;
    衡量當前級聯分類器的檢測率Di和誤識率Fi;
    for : di<d*Di-1;
       降低第i層的強分類器閾值;
       衡量當前級聯分類器的檢測率Di和誤識率Fi;
    N = Φ;
    利用當前的級聯分類器檢測非人臉影象,將誤識的影象放入N;

分類器生成及使用

一個高準確率的級聯分類器的主要生成步驟如下:
1.大量樣本集合,特徵值的提取
2.通過adaboost 訓練多個弱分類器並迭代為強分類器
3.多層級聯強分類器,得到最終的級聯分類器
這些訓練流程完成之後結果以xml的方式儲存起來,就是分類器檔案,opencv中包含了以上的實現,並且已經存放了許多已經訓練好的不同型別的不同特徵值提取生成的的級聯分類器
Opencv中可以直接載入這些分類器檔案,並且給出了便捷的API


OpenCV 中的人臉識別

人臉檢測:

Opencv原始碼中的分類器存放路徑:opencv-2.4.13\data\
其中有hog haar lbp 分類目錄,還有針對gpu使用的分類器檔案,當使用gpu 或者 ocl時候需要載入此類分類器檔案

Cpu進行的人臉檢測實現類為 CascadeClassifier ,在 objdetect 模組中,主要api實現檔案:
opencv-2.4.13\modules\objdetect\src\cascadedetect.cpp

呼叫opencl處理的類 OclCascadeClassifier, 在ocl模組中實現:
opencv-2.4.13\modules\ocl\src\haar.cpp

還包含了gpu-cuda處理的檢測,屬於windows的範疇 opencv-2.4.13\modules\gpu\src\cascadeclassifier.cpp
檢測包含的主要api:

CV_WRAP bool load( const string& filename );

用於載入分類器檔案

CV_WRAP virtual void detectMultiScale( const Mat& image,
                                   CV_OUT vector<Rect>& objects,
                                   double scaleFactor=1.1,
                                   int minNeighbors=3, int flags=0,
                                   Size minSize=Size(),
                                   Size maxSize=Size() );

用於檢測影象中的人臉

引數1:image–待檢測圖片,一般為灰度影象加快檢測速度;
引數2:objects–被檢測物體的矩形框向量組;
引數3:scaleFactor–表示在前後兩次相繼的掃描中,影象被縮放的比例,1.1即每次縮放10% 用於檢測
引數4:minNeighbors–表示構成檢測目標的相鄰矩形的最小個數(預設為3個)。
如果組成檢測目標的小矩形的個數和小於 min_neighbors - 1 都會被排除。
如果min_neighbors 為 0, 則函式不做任何操作就返回所有的被檢候選矩形框,
這種設定值一般用在使用者自定義對檢測結果的組合程式上;(沒搞懂.使用預設值)

引數5:flags–要麼使用預設值,要麼使用CV_HAAR_DO_CANNY_PRUNING,如果設定為
CV_HAAR_DO_CANNY_PRUNING,那麼函式將會使用Canny邊緣檢測來排除邊緣過多或過少的區域,
因此這些區域通常不會是人臉所在區域;
引數6、7:minSize和maxSize用來限制得到的目標區域的範圍。

測試用例程式碼和效果可參考後面的 Facedetect測試用例。


人臉識別

Opencv為人臉識別封裝FaceRecognizer類,往下三種型別的識別器,分別為:

EigenFaceRecognizer 
FisherFaceRecognizer 
LBPHFaceRecognizer

是以根據不同的特徵值來進行識別,主要的實現在contrib模組中:
\opencv-2.4.13\modules\contrib\src\facerec.cpp
通過

createEigenFaceRecognizer(…)
createFisherFaceRecognizer(…)
createLBPHFaceRecognizer(…)

不同的識別器 構造時需要傳入相關的引數,比如Eigen 和 Fisher的需要可以選擇性傳入num_components 和threshold 分別代表識別器通過PCA降維處理時保留的影象維度,預設為全保留,以及在識別時,傳入影象與最相近距離閥值的設定,關乎識別的精度,預設為很大的一個值,可以通過介面讀取以及設定該值,如下:
getDouble(“threshold”));
set(“threshold”, 1600.0f);
如果傳入識別的影象與訓練好的資訊集合中最相近的一幀影象距離為1500,此值小於1600的閥值,那我們就認定識別到了這個人,反之找不到此人
主要的api:

CV_WRAP virtual void train(InputArrayOfArrays src, InputArray labels) = 0;

將樣本進行PCA降維提取儲存到一個投影空間中,src與label一一對應

// Gets a prediction from a FaceRecognizer.
virtual int predict(InputArray src) const = 0;
// Predicts the label and confidence for a given sample.
CV_WRAP virtual void predict(InputArray src, CV_OUT int &label, CV_OUT double &confidence) const = 0;

進行識別,傳入需要識別的物件 src,label即為結果,confidence就是置信度,就是上面說到的 距離可能為1500. 為0 就是100%完全一樣的兩幀畫面。

// Serializes this object to a given filename.
CV_WRAP virtual void save(const string& filename) const;

// Deserializes this object from a given filename.
CV_WRAP virtual void load(const string& filename);

經過初始化好的識別器經過train之後,可以驚醒predict 也可以將此時train好的資料資訊 save起來
下次初始化一個識別器直接load 就可以免去每次的train。

測試用例程式碼可參考最後的 Facerecognize測試用例。


Opencv 編譯移植問題

編譯配置ocl-neon-stltype

Opencv的編譯配置問題,選的linux版本opencv原始碼,android的是現成的庫,直接到opencv-2.4.13\platforms\scripts目錄下新建一個配置指令碼cmake_android_arm.sh,內容如下:

#!/bin/sh
cd `dirname $0`/..

mkdir -p build_android_arm
cd build_android_arm

export ANDROID_NDK=/home/local/ACTIONS/jiangbin/tool/android-ndk-r10
export ANDROID_NATIVE_API_LEVEL=19
export ANDROID_TOOLCHAIN_NAME=arm-linux-androideabi-4.8
export ANDROID_SDK=/home/local/ACTIONS/jiangbin/tool/android-sdk-linux
export PATH=$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools:$PATH
cmake \
    -DBUILD_SHARED_LIBS=ON \
    -DBUILD_opencv_apps=ON \
    -DBUILD_ANDROID_EXAMPLES=ON \
    -DBUILD_DOCS=ON \
    -DBUILD_EXAMPLES=ON \
    -DBUILD_PERF_TESTS=ON \
    -DBUILD_TESTS=ON \
    -DENABLE_VFPV3=ON \
    -DENABLE_NEON=ON \
    -DWITH_OPENCL=ON \
    -DINSTALL_C_EXAMPLES=ON \
    -DINSTALL_PYTHON_EXAMPLES=ON \
    -DINSTALL_ANDROID_EXAMPLES=ON \
    -DINSTALL_TESTS=ON \
    -DDOXYGEN_EXECUTABLE=/usr/bin/doxygen \
    -DANDROID_STL=stlport_static \
    -DANDROID_ABI=armeabi-v7a with NEON \
    -DCMAKE_TOOLCHAIN_FILE=../android/android.toolchain.cmake $@ ../..
make clean
make -j8
make install

配置好NDK路徑以及編譯工具鏈,往下就是各個功能項的開關
DBUILD_SHARED_LIBS 最終連結生成動態庫
DENABLE_NEON 開啟cpu的neon
DWITH_OPENCL 編譯ocl模組
DANDROID_STL=stlport_static 這個是配置C++執行需要的模板庫的型別,預設為gnustl_static
這些配置巨集的定義以及預設初始值可以在\opencv-2.4.13\platforms\android\android.toolchain.cmake中檢視


存在的問題

Stl
並且還發現一個問題:就是按照上面的編譯指令碼DANDROID_STL=stlport_static 的方式來編譯整個opencv,在我們的android平板上跑opencv自帶的ocl用例,會出現stl庫相容性的問題,一些filestream類讀取等會存在異常問題,換成預設的gnustl_static 就不會存在問題,但是stlport_static 這種模式是配置檔案中列出來支援的,暫沒搞清楚是不是NDK環境或者其它配置項存在問題導致
當DANDROID_STL= gnustl_static 預設配置編譯的時候,如果想直接使用opencv編譯出來的動態庫,在Android.mk中就不能引入stlport支援以及標頭檔案,否則string會傳遞異常,但是發現android本身的裁剪的stdc++ 滿足不了opencv的編譯需求,存在stl相容性的問題

Neon
開關編譯配置生效,程式碼中的配置巨集也沒有問題,搜查到只有video以及3rdparty部分才用到,objdetect以及contrib等模組中並沒有使用,應該是不支援


OpencvManager

Opencv為android準備了一套SDK,類似:OpenCV-2.4.13.1-android-sdk
然後提供了一套java的api,提供了一套從java到底層opencv實現庫的中介軟體,以apk service形式存在:opencvmanager, sdk種的原理圖如下:
這裡寫圖片描述

這裡寫圖片描述

具體的實現細節可參考OpenCV-2.4.13.1-android-sdk doc目錄中的opencv2manager.pdf
這樣只要安裝了這個opencvmanager apk,apk中的libopencv_java.so 就是封好的底層libraries,就可以使用opencv封好的java api開發apk了

也可以不用sdk java api往下調,自己封一層jni ,然後以gunstl_static形式連結opencv生成的靜態庫集合,需要用NDK來編譯,詳細可參考
OpenCV-2.4.13.1-android-sdk\OpenCV-android-sdk\samples\face-detection\jni
需要定義Application.mk內容

還可以過掉opencvmanager這個中間環節,不使用sdk java提供的api往下,全部都走jni,編譯我們自己的jni的時候把需要的opencv庫以靜態或者動態的方式打包到我們的apk中。除錯發現sdk java部分的api功能也不全,有的功能介面不支援,比如FaceRecognizer的使用還是得走jni往下
去掉opencvmanager的依賴可參考:
這裡寫連結內容

有使用apk的方式驗證了人臉識別的收集和檢測,java sdk中提供了對camera 每一幀資料的處理回撥
單獨的人臉檢測可以直接使用java sdk + opencvmanager-apk 就可以實現,可以參考opencv自帶的facedetect apk這個例子。
如果還要在檢測的時候加上識別功能就需要自己定義jni往下初始化FaceRecognizer,呼叫關係跟下面的C++實現基本雷同,程式碼就不貼了,使用方法可參考往下的測試用例


Facedetect測試用例

簡述:

需要opencv的標頭檔案以及庫檔案,在android原始碼中編譯為二進位制測試,寫好android.mk即可,這裡直接使用cpu檢測
CascadeClassifier類,呼叫了load以及detectMultiScale介面,檢測完成得到的檢測到的人臉向量座標在原圖上標記成方框,儲存

#include "include/opencv2/objdetect/objdetect.hpp"
#include "include/opencv2/highgui/highgui.hpp"
#include "include/opencv2/imgproc/imgproc.hpp"
#include <utils/Log.h>
#define LOG_TAG "opencv"
using namespace std;
using namespace cv;

int main()
{
    ALOGE("begain = \n");
    CascadeClassifier cpufaceCascade;
    const string path="haarcascade_frontalface_alt.xml";
    const string gpu_path="haarcascade_frontalface_alt-GPU.xml";
    if(!cpufaceCascade.load(path))
    {
        ALOGE("cpufaceCascade.load error \n");
        return -1;
    }
    Mat img = imread("face_detect.jpg", CV_LOAD_IMAGE_GRAYSCALE);
    if(img.empty())
        {
          ALOGE("img.empty\n");
          return 1;
        }
    Mat imgGray;
    vector<Rect> faces;
    if(img.channels() ==3)
    {
       cvtColor(img, imgGray, CV_RGB2GRAY);
       ALOGE("img cvtColor\n");
    }
    else
    {
       imgGray = img;
    }
    ALOGE("cpu begain detectMultiScale\n");
    cpufaceCascade.detectMultiScale(imgGray, faces, 1.1, 3, 0);
    ALOGE("cpu detect = faces.size= %d\n",faces.size());
    if(faces.size()>0)
    {
       for(int i =0; i<faces.size(); i++)
       {
           rectangle(img, Point(faces[i].x, faces[i].y), Point(faces[i].x + faces[i].width, faces[i].y + faces[i].height),
                           Scalar(0, 255, 0), 1, 8);
       }
    }
    IplImage src = IplImage(img);
    IplImage *input = cvCloneImage(&src);
    cvSaveImage("/data/face_detect_save1.jpg", (CvArr*) input);
    ALOGE("cvSaveImage from cpudetect  compelete \n");
    return 0;
}

檢測到人臉並用方框畫出儲存,影象如下:
這裡寫圖片描述


Facerecognize測試用例:

簡述:

與檢測的環境相同,這裡採用了1-10張標號的人臉影象,其中1-5 號是第一個人的不同表情頭像,6-10號為第二個人不同表情頭像,
最後的03標號為第三個人頭像。
訓練資料取1-4 ,6-9 總共8張頭像,刻意不把第5張和第10張包載入到識別器中,以用來識別
同時設定了enginefacerecognize的閥值引數,也就是前面說到過的1500,1600 的關係,在這裡執行之後就能看出來
通過兩種不同的predict的呼叫,可以得出識別的置信度,低於識別器設定的閥值就認定識別成功,否則返回-1,代表沒有識別到
最後save了這次的訓練結果,再次建立識別器直接load 測試

#include "include/opencv2/objdetect/objdetect.hpp"
#include "include/opencv2/highgui/highgui.hpp"
#include "include/opencv2/imgproc/imgproc.hpp"
#include "include/opencv2/contrib/contrib.hpp"
#include <utils/Log.h>
#define LOG_TAG "opencv"

using namespace std;
using namespace cv;
int main()
{
    ALOGE("face recongnition begin\n");
    vector <Mat> faces;
    vector <int> labels;
    char ss[128];
    int predicted_label = -1;
    double predicted_confidence = 0.0;
    for (size_t i = 1; i <= 5; i++) {
        sprintf(ss,"face/%d.bmp",i);
        ALOGE("face imread name  =%s\n",ss);
        faces.push_back(imread(ss,CV_LOAD_IMAGE_GRAYSCALE));
    labels.push_back(1);
    }
    Mat testpeople1=faces[faces.size()-1];
    int testlabel1=labels[labels.size()-1];
    faces.pop_back();
    labels.pop_back();

    ALOGE("face recongnition 1 faces-vec size =%d to train except for  5.bmp \n",faces.size());
//==========================================================
    for (size_t i = 6; i <= 10; i++) {
        sprintf(ss,"face/%d.bmp",i);
        faces.push_back(imread(ss, CV_LOAD_IMAGE_GRAYSCALE));
    labels.push_back(2);
    }
    Mat testpeople2=faces[faces.size()-1];
    int testlabel2=labels[labels.size()-1];
    faces.pop_back();
    labels.pop_back();
    ALOGE("face recongnition 1+2 faces-vec size =%d to train  except for  5.bmp and 10.bmp \n",faces.size());
//==========================================================
    // --------------------------------------------------------------------------
    // createEigenFaceRecognizer,createFisherFaceRecognizer,createLBPHFaceRecognizer
    Ptr<FaceRecognizer> face_recog = createEigenFaceRecognizer();
    ALOGE("face recongnition train -the threshold=%8f\n",face_recog->getDouble("threshold"));
//
    face_recog->set("threshold", 1600.0f);//threshold
    ALOGE("face recongnition the threshold=%8f\n",face_recog->getDouble("threshold"));
    face_recog->train(faces, labels);
    ALOGE("face recongnition train over\n");
    /*opencv-2.4.13\modules\contrib\src\facerec.cpp  line:478
     *
     *    minDist = DBL_MAX;
     *    minClass = -1;
     *
     *    if((dist < minDist) && (dist < _threshold)) {
            minDist = dist;
            minClass = _labels.at<int>((int)sampleIdx);
        }
     *
     *
     *
     *
     * */
//  predict method 1 test
//  int face_id0 = face_recog->predict(imread("face/1.bmp", CV_LOAD_IMAGE_GRAYSCALE)/*testSample*/);
//  int face_id1 = face_recog->predict(imread("face/8.bmp", CV_LOAD_IMAGE_GRAYSCALE));
//  int face_id2 = face_recog->predict(imread("face/3.bmp", CV_LOAD_IMAGE_GRAYSCALE));
//  ALOGE("face recongnition face_id0 =%d  face_id1 =%d face_id2 =%d\n",face_id0,face_id1,face_id2);
//  int face_testid1 = face_recog->predict(testpeople1);
//  int face_testid2 = face_recog->predict(testpeople2);
//  ALOGE("face recongnition  5 .face_testid1 =%d  10. face_testid2 =%d\n",face_testid1,face_testid2);
//  end
    //  predict method 2
    face_recog->predict(imread("face/1.bmp", CV_LOAD_IMAGE_GRAYSCALE),predicted_label,predicted_confidence);
     ALOGE("face recongnition face/1.bmp  predicted_label =%d predicted_confidence =%8f\n",predicted_label,predicted_confidence);
    face_recog->predict(imread("face/8.bmp", CV_LOAD_IMAGE_GRAYSCALE),predicted_label,predicted_confidence);
     ALOGE("face recongnition face/8.bmp  predicted_label =%d predicted_confidence =%8f\n",predicted_label,predicted_confidence);
    face_recog->predict(imread("face/3.bmp", CV_LOAD_IMAGE_GRAYSCALE),predicted_label,predicted_confidence);
     ALOGE("face recongnition face/3.bmp  predicted_label =%d predicted_confidence =%8f\n",predicted_label,predicted_confidence);


    face_recog->predict(testpeople1,predicted_label,predicted_confidence);
     ALOGE("face recongnition face/5.bmp  predicted_label =%d predicted_confidence =%8f\n",predicted_label,predicted_confidence);
    face_recog->predict(testpeople2,predicted_label,predicted_confidence);
     ALOGE("face recongnition face/10.bmp  predicted_label =%d predicted_confidence =%8f\n",predicted_label,predicted_confidence);
    //  end
    string savepath = "face_rec_model.xml";
    face_recog->save(savepath);
//=============================reload xml test=====================================
    Ptr<FaceRecognizer> face_load = createEigenFaceRecognizer();
    ALOGE("face_load recongnition train -the threshold 0 =%8f\n",face_load->getDouble("threshold"));
    face_load->set("threshold", 1500.0f);
    ALOGE("face_load recongnition train -the threshold 1 =%8f\n",face_load->getDouble("threshold"));
    face_load->load(savepath);
    ALOGE("face_load recongnition train -the threshold 2 =%8f\n",face_load->getDouble("threshold"));
    ALOGE("face recongnition face_reload test 4.bmp id =%d   7.bmp id =%d\n",face_load->predict(imread("face/4.bmp", CV_LOAD_IMAGE_GRAYSCALE)),face_load->predict(imread("face/7.bmp", CV_LOAD_IMAGE_GRAYSCALE)));
    ALOGE("face recongnition face_reload test 10.bmp id =%d  5.bmp id =%d\n",face_load->predict(imread("face/10.bmp", CV_LOAD_IMAGE_GRAYSCALE)),face_load->predict(imread("face/5.bmp", CV_LOAD_IMAGE_GRAYSCALE)));
//  ALOGE("face recongnition face_reload test otherpeople id=%d\n",face_load->predict(imread("face/03.bmp", CV_LOAD_IMAGE_GRAYSCALE)));
    face_load->predict(imread("face/03.bmp", CV_LOAD_IMAGE_GRAYSCALE),predicted_label,predicted_confidence);
         ALOGE("face recongnition face/03.bmp -  predicted_label =%d predicted_confidence =%8f\n",predicted_label,predicted_confidence);
    return 0;
}

相關文章