OpenCV人臉識別

2017-8-13

前言

實習了一個月,搞了一個月的人臉識別,終于研究出結果,就和大家分享一下,雖然感覺不是真正意義上的人臉識別,但還是有很高識別度的,代碼我就只貼出了比較重要的代碼和邏輯,源碼已經在Github上了。完整的項目分為客戶端和服務器端,圖片的對比和存儲以及一些注冊信息就存在服務器端,不讓客戶端處理,但是客戶端還是存在人臉對比的代碼的(Compare類)。比較基礎一點的搭建opencv for android 和 opencv for java 的環境就不說了,說一點干貨???(黑人問號.jpg) 講一講遇到的問題,分析一下流程和原理。

閑話不多說,我們開始吧。

https://github.com/Hyyzt/FaceRecognition

客戶端

1.邏輯

ControlActivity:
用來控制整個程序的流程,進行注冊和登錄

FaceLoginActivity:
進行人臉注冊,根據服務器返回的數據判斷是否可以進形注冊

InfoActivity:
根據服務器返回的數據進行注冊信息的數據,并將數據提交給服務器保存

FaceReconginzedAcitvity:
進行人臉識別,上傳數據至服務器,返回人臉識別是否成功的信息

SuccessActivity:
人臉識別成功后從服務器返回注冊時的信息并展示

2.重要代碼

  • 初始化opencv類庫

若我們需要使用opencv類庫,則必須進行初始化,盡量在Application中的oncreate()中初始化,每次啟動的時候只加載一次類庫。

System.loadLibrary("opencv_java");
  • 剔除opencv manager的關聯

使用人臉識別類庫的時候,官方規定你必須安裝opencv manager才可以使用這些類庫,但我們可以通過一些操作來剔除依賴。

首先將opencv目錄下的sdk/native/libs下的文件全部拷貝出來,并在自己的程序目錄下建立一個與main同級并命名為jniLibs的文件夾,將之前的拷貝文件全被拷貝進去,并在onCreate中加入以下代碼

    System.loadLibrary("detection_based_tracker");
    try {
        InputStream is = getResources().openRawResource(R.raw.lbpcascade_frontalface);
        File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
        mCascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");
        FileOutputStream os = new FileOutputStream(mCascadeFile);

        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        }
        is.close();
        os.close();

        mJavaDetector = new CascadeClassifier(mCascadeFile.getAbsolutePath());
        if (mJavaDetector.empty()) {
            Log.e(TAG, "Failed to ControlActivity cascade classifier");
            mJavaDetector = null;
        } else
            Log.i(TAG, "Loaded cascade classifier from " + mCascadeFile.getAbsolutePath());

        mNativeDetector = new DetectionBasedTracker(mCascadeFile.getAbsolutePath(), 0);

        cascadeDir.delete();

    } catch (IOException e) {
        e.printStackTrace();
    }
    mOpenCvCameraView.enableView();

完成之后你就可以在沒有opencv manager的情況下進行使用了

  • 開始人臉識別

首先,你需要在布局中加入一個opencv自己定義的控件,這個控件就是我們進行人臉檢測和識別的控件,這個控件是一個視頻流控件,它初始化是后置攝像頭,你需要將它前置,但是前置過后每一幀會出現鏡像的結果,我們在回調中處理這個問題。

你需要在activity中引入一個接口CvCameraViewListener2,并實現它的方法

    <org.opencv.android.JavaCameraView
    android:id="@+id/fd_activity_surface_view"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" />
    //前置攝像頭
    mOpenCvCameraView.setCameraIndex(CameraBridgeViewBase.CAMERA_ID_FRONT);

實現接口

//視頻流開始
//mGray和mRgba分別是每一幀圖像的灰度化圖像和彩色圖像
public void onCameraViewStarted(int width, int height) {
    Log.e("TAG", "onCameraViewStarted");
    mGray = new Mat();
    mRgba = new Mat();
}

//視頻流結束
public void onCameraViewStopped() {
    Log.e("TAG", "onCameraViewStopped");
    mGray.release();
    mRgba.release();
}

//使用視頻流時每一幀的回調
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {

    mRgba = inputFrame.rgba();
    mGray = inputFrame.gray();
    //處理前置后鏡像的攝像頭
    //倒轉鏡像的攝像頭
    Core.flip(mRgba, mRgba, 1);
    Core.flip(mGray, mGray, 1);

    //將視頻流控制住,只在一定區域內可以檢測人臉
    Point point = new Point(mGray.width() / 2 - 375, mGray.height() / 2 - 375);
    Rect rect = new Rect(point, new Size(750, 750));
    mGray = new Mat(mGray, rect);


    if (mAbsoluteFaceSize == 0) {
        int height = mGray.rows();
        if (Math.round(height * mRelativeFaceSize) > 0) {
            mAbsoluteFaceSize = Math.round(height * mRelativeFaceSize);
        }
        mNativeDetector.setMinFaceSize(mAbsoluteFaceSize);
    }
    MatOfRect faces = new MatOfRect();
    if (mDetectorType == JAVA_DETECTOR) {
        if (mJavaDetector != null)
            mJavaDetector.detectMultiScale(mGray, faces, 1.1, 2, 2, // TODO: objdetect.CV_HAAR_SCALE_IMAGE
                    new Size(mAbsoluteFaceSize, mAbsoluteFaceSize), new Size());
    } else if (mDetectorType == NATIVE_DETECTOR) {
        if (mNativeDetector != null)
            mNativeDetector.detect(mGray, faces);
    } else {
        Log.e(TAG, "Detection method is not selected!");
    }
    //這個facesArray數組是每一幀我們提取到的人臉個數,我們需要將它篩選,剔除掉錯誤的識別情況
    Rect[] facesArray = faces.toArray();
    if (facesArray.length > 0) {
        for (int i = 0; i < facesArray.length; i++){
            Point point1 = new Point(facesArray[i].x + point.x, facesArray[i].y + point.y);
            facesArray[i] = new Rect(point1, facesArray[i].size());
            //遍歷數組時,此處剔除,并將正確的臉部在屏幕上顯示出來,且返回這個臉部頭像bitmap
            if(facesArray[i].width > 350) {
                Core.rectangle(mRgba, facesArray[i].tl(), facesArray[i].br(), FACE_RECT_COLOR, 3);
                //根據矩陣和臉部大小裁剪成圖片
                bitmap = FaceUtils.cutDownFaceROI(mRgba, facesArray[i]);
            }
        }
    }
    return mRgba;
}

在請求服務器時,我們盡量不要在onCameraFrame中請求,在開始識別后,延時消息發送bitmap至服務器,也不要在onCameraFrame中進行復雜的邏輯判斷,否則會出現視頻流卡死的情況。

在銷毀和恢復activity時,要對JavaCameraView進行銷毀和恢復

服務器端

1.開發環境

編譯工具:Eclipse

服務器:Tomcat

數據庫:MySQL

2.相關類庫

OpenCV相關Jar包:opencv-2411.jar, opencv-windows-x86_64.jar等

JavaCV相關Jar包:javacv.jar, javacpp.jar等

其他Jar包:gson-2.3.1.jar

3.實現邏輯

客戶端請求時,根據不同請求執行不同邏輯并返回結果:

1.客戶端注冊時對發送的人臉數據進行臨時存儲,并對比數據庫匹配,若匹配結果達到設定閾值(75%),則返回已注冊過;反之,則返回未注冊過。

2.客戶端成功注冊后對其發送的用戶基本信息和人臉數據進行處理和入庫存儲,其中人臉數據以圖片格式(.png)存儲本地。

3.客戶端人臉驗證登陸時發送的人臉數據進行臨時存儲,并對比數據庫匹配,若匹配結果達到設定閾值(80%)則取出庫中對應的用戶數據和人臉圖片url返回;反之,則返回登陸失敗。

4.數據庫設計

數據庫face-detect-database,表user-info,

主要字段id: int類型, 主鍵;

name: text類型, 用戶名;

age: int類型, 年齡,

sex: int類型, 性別,

birthday: text類型, 出生日期;

face_path: text類型, 人臉圖片本地存儲路徑。

5.接口設計

1.GetData: 注冊時判斷用戶是否注冊過

public class GetData extends HttpServlet{
    ...
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 存儲臨時圖片
        ServletInputStream is = request.getInputStream();
        String tempPath = ImageUtils.saveImageToLocal(is, "temp");
        // 遍歷數據庫,比較相似度
        MySQLDatabaseHelper helper = new MySQLDatabaseHelper();
        List<User> userList = helper.query();
        helper.close();
        PrintWriter writer = response.getWriter();

        // FaceRecognizer匹配
        User user = MyFaceRecognizer.matchByTrainAndPredict(userList, tempPath);
        if (user != null) {
            writer.write("Login");
        } else {
            writer.write("NoLogin");
        }
        // 灰度匹配
        // double similarity = 0;
        // for (int i = 0; i < userList.size(); i++) {
        // similarity = FaceMatchUtils.
        // histogramMatch(userList.get(i).getFace_pic(), tempPath);
        // if (similarity > 0.75) {
        // writer.write("Login");
        // return;
        // }
        // }
        // writer.write("NoLogin");
    }
    ...
}

2.GetJson: 處理和存儲用戶基本信息和人臉圖片

public class GetJson extends HttpServlet {
    ...
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        // 獲取圖片流和Json
        ServletInputStream is = request.getInputStream(); // 圖片流
        // String jsonData = request.getParameter("Info");
        // 轉碼
        String str = request.getParameter("Info");
        String jsonData = new String(str.getBytes("ISO-8859-1"), "utf-8");
        System.out.println(jsonData);
        Gson gson = new Gson();
        UserInfo userInfo = gson.fromJson(jsonData, UserInfo.class);
        // 獲取數據后返回Success
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.write("Success");
        // 存儲圖片和更新數據庫
        MySQLDatabaseHelper helper = new MySQLDatabaseHelper();
        int userNum = helper.query().size();
        String facePath = ImageUtils.saveImageToLocal(is, "face" + (userNum));
        User user = new User();
        user.setName(userInfo.name);
        user.setAge(Integer.parseInt(userInfo.age));
        user.setSex(userInfo.sex);
        user.setBirthday(userInfo.birthday);
        user.setFace_pic(facePath);
        System.out.println(user.toString());
        helper.insert(user);
        helper.close();
    }
    ...
}

3.VerifyLogin: 人臉驗證登陸

public class VerifyLogin extends HttpServlet {
    ...
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 接收圖片并存儲
        ServletInputStream is = request.getInputStream(); // 圖片流
        String tempPath = ImageUtils.saveImageToLocal(is, "verify");
        // 遍歷數據庫,驗證登陸
        MySQLDatabaseHelper helper = new MySQLDatabaseHelper();
        List<User> userList = helper.query();
        helper.close();
        PrintWriter writer = response.getWriter();
        
        // FaceRecognizer匹配
        User user = MyFaceRecognizer.matchByTrainAndPredict(userList, tempPath);
        if(user != null) {
            Gson gson = new Gson();
            String json = gson.toJson(new UserInfo(user.getName(), 
                    user.getSex(), user.getAge(), user.getBirthday(),
                    url + user.getFace_pic().
                    substring(user.getFace_pic().lastIndexOf("/") + 1)));
            System.out.println(json);
            response.setContentType("text/html");
            writer.write(json);
        } else {
            writer.write("Fail");
        }
    }
    ...
}

6.關鍵代碼

1.基于圖像灰度直方圖比較的人臉匹配算法

public class FaceMatchUtils {
    // 利用灰度直方圖計算圖像相似度,輸要求入人臉圖像的均為正方形
    public static double histogramMatch(String face, String testFace) {
        Mat faceMat = Highgui.imread(face);
        Mat testFaceMat = Highgui.imread(testFace);
        // 圖像灰度化
        System.out.println("histogramMatch: 圖像灰度化");
        Imgproc.cvtColor(faceMat, faceMat, Imgproc.COLOR_RGB2GRAY);
        Imgproc.cvtColor(testFaceMat, testFaceMat, Imgproc.COLOR_RGB2GRAY);
        // 直方圖均衡化,暫時注釋
//      System.out.println("histogramMatch: 直方圖均衡化");
//      Imgproc.equalizeHist(faceMat, faceMat);
//      Imgproc.equalizeHist(testFaceMat, testFaceMat);
        // 把Mat矩陣的type轉換為Cv_32F,因為在c++代碼中會判斷他的類型
        faceMat.convertTo(faceMat, CvType.CV_32F);
        testFaceMat.convertTo(testFaceMat, CvType.CV_32F);
        // 直方圖匹配
        System.out.println("histogramMatch: 直方圖匹配");
        double similarity = Imgproc.compareHist(faceMat, testFaceMat, Imgproc.CV_COMP_CORREL);
        System.out.println("灰度直方圖相似性結果: " + face + " : "+ similarity);
        return similarity;
    }
}

2.基于FaceRecognizer人臉訓練和預測的人臉匹配算法

public static User matchByTrainAndPredict(List<User> userList, String path) {
    List<String> pathList = new ArrayList<String>();
    for (User user : userList) {
        pathList.add(user.getFace_pic());
    }
    MatVector images = new MatVector(pathList.size());
    Mat labels = new Mat(pathList.size(), 1, CV_32SC1);
    IntBuffer labelsBuf = labels.createBuffer();
    for (int i = 0; i < pathList.size(); i++) {
        String p = pathList.get(i);
        Mat img = imread(p, CV_LOAD_IMAGE_GRAYSCALE);
        images.put(i, img);
        labelsBuf.put(i, i);
    }
    // FaceRecognizer faceRecognizer = createFisherFaceRecognizer();
    // FaceRecognizer faceRecognizer = createEigenFaceRecognizer();
    FaceRecognizer faceRecognizer = createLBPHFaceRecognizer();

    faceRecognizer.train(images, labels);

    Mat testImage = imread(path, CV_LOAD_IMAGE_GRAYSCALE);
    IntPointer label = new IntPointer(1);
    DoublePointer confidence = new DoublePointer(1);
    faceRecognizer.predict(testImage, label, confidence);
    int predictedLabel = label.get(0);
    System.out.println("Predicted label: " + predictedLabel);
    System.out.println("Confidence: " + confidence.get(0));
    if (confidence.get(0) > 10000) {
        System.out.println(userList.get(predictedLabel).toString());
        return userList.get(predictedLabel);
    } else {
        System.out.println("沒有匹配");
        return null;
    }
}

問題

  • 在進行圖像相似度對比時,要注意對比的圖像大小要一致,否則會出現傳入非法參數的異常
  • 由于對比方法使用的是灰度直方圖在歸一化之后對比,使得圖像對環境的光照強烈十分的敏感,而且對于拍攝條件有一點的限制,注冊時的背景和識別時的背景不能相差過大,拍攝距離要控制好,不能太遠也不能太近,要保證拍攝的質量,而且拍攝后的照片在經過壓縮處理后,會損失一部分的精度,使相似度下降了10個百分點。在上述條件都確保的情況下,對比的結果還是非常精確的,高達80%左右
  • 由于opencv類庫本身是一個圖像處理的庫,而不能提取人臉特征,只能通過對比識別人臉后的人臉圖像來查看差異,而灰度直方圖是在各種方法中最準確的方法
  • Opencv高版本提供了一個FaceRecognizer類,對人臉進行特征對比和匹配,但是它沒有對應的JAVA API,而JavaCV對這個類進行了封裝,提供了對應接口,卻沒有給出人臉特征點數據,而是通過對比直接返回了匹配結果,且由于源碼沒開放,無法控制相似度閾值

結束語

下一階段開始研究Arcgis for android 的三維地圖,等研究出東西再和大家分享吧.

到這里就差不多結束了,希望能幫到你們,多多支持哦!!!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容