人臉識別趟坑歷程

1.人臉識別概述

人臉識別,是基於人的臉部特徵信息進行身份識別的一種生物識別技術。用攝像機或攝像頭採集含有人臉的圖像或視頻流,並自動在圖像中檢測和跟蹤人臉,進而對檢測到的人臉進行臉部的一系列相關技術。其中技術包括圖像採集、特徵定位、身份的確認和查找等等。簡單來說,就是從照片中提取人臉中的特徵,比如眉毛高度、嘴角等等,再通過特徵的對比輸出結果。

聽着這麼高大上,很高科技,然而目前很多人臉識別的落地應用還處在一個非常初級的階段,在我國應用最多還是1:1等級,也就是人臉識別中最初級的“證明你是你”。 目前國內已有一些人臉識別開發平臺,開發者可以直接調用他們的開發的API ,進行一些簡單的人臉識別應用的開發。

筆者直接採用市場上人臉識別開發平臺的API,進行人臉識別快速開發,不涉及原理、算法、模型等複雜東西,當然如果對人臉識別原理比較感興趣,可以參考這篇博客:人臉識別(face recognition)

筆者在從事掃碼支付工作時,目前正在進行智能設備上人臉識別研究,本文就記錄筆者在人臉識別這個項目的趟坑歷程,希望對人臉識別感興趣的各位作一些參考。

2. 人臉識別平臺選擇

目前人臉識別在國內發展迅速,各種新興公司如雨後春筍,目前中國人臉識別的獨角獸:face++ 的曠視科技,senseTime的商湯科技,還有云從科技,依圖科技,他們依舊在繼續發力,搶奪市場。

而國內的 BAT 對人臉識別這個方向也蠻重視,阿里巴巴控股曠視科技、依圖科技,並且開發自己的人臉識別接口,已全面將人臉識別技術應用到自己支付寶、淘寶等平臺,並聯合系統旗下其他業務板塊,培養人臉識別的應用場景;騰訊旗下有自己內部的優圖團隊,爲 QQ 空間、騰訊地圖、騰訊遊戲等 50 多款產品提供圖像技術支持;百度人臉識別也依靠龐大的數據資源進步神速,已推出百度識圖、臉優app等產品。

目前雲從科技,依圖,商湯等知名的人臉識別公司主要做企業用戶,目前市面上能提供開放平臺給開發者,免費進行調試的就只有BAT三家旗下的產品

下面是筆者調研百度、騰訊、face++這三家人臉識別開放平臺對比。

公司 免費額度 收費 技術能力 知名應用
百度 <=1000次/天 公測階段,未開始收費 99.77%(LFW) 百變魔圖 百度網盤
騰訊優圖 2萬張/月
 =0.0005元/張,階梯價位
99.80%(LFW) 83.29%(MegaFace) 微衆銀行 手機QQ
face++ 有限用量,共享的 QPS 配額,<=1000FaceSet,<=1000000faces
 =0.001次
技術先進 支付寶 滴滴

上面的三家企業均可以免費接入,然後對比下來,各位選擇誰家呢?face++平臺開放早,業內名氣大,應用多,背靠阿里,坐擁龐大的用戶羣體;騰訊優圖平臺後發先至,有龐大的社交平臺作依託,技術實力強悍;百度技術先進,免費額度高。

筆者產品離大規模商業應用還是有段距離,現階段還是技術體驗階段,所以筆者就選擇免費額度比較大的百度。現階段百度還是公測階段,正式收費後,可以重點關注騰訊優圖與百度,目前人臉識別開發平臺中這兩家技術處於領先地位。

3. 人臉識別流程

從2015年3月馬雲展示支付寶的刷臉支付,目前很多app中也加入了人臉識別功能,然而現在人臉識別應用還在很初級階段,目前我國應用最多還是1:1等級,也就是人臉識別中最初級的“證明你是你”,一般應用於身份覈對方面。

下圖是目前人臉識別最常見的流程圖: 
 
用戶拍攝自己身份信息並上傳系統,系統經過公民身份信息查詢獲取用戶信息,建立用戶檔案,關聯用戶人臉;下次掃描頭像,經活體檢測、人臉質量檢測、人臉圖像等處理後進行人臉對比,覈對結果,完成“你是你”的證明。

另外目前中國公民身份證上的圖片一般拍攝的比較早,與本人現在頭像差距較大,如果直接用身份證上面的圖片會加大識別難度,識別誤差大。目前通常做法是完成身份證信息提交後,還需添加人臉,即進行活體檢測、人臉質量檢測、處理人臉圖像,添加用戶的現在頭像並關聯用戶,下次人臉識別時與此頭像進行對比。

目前市場上大部分帶有人臉識別的app,都是C端產品,用戶掃自己,實現在線用戶身份驗證。當人臉覈對時,app需將用戶信息及拍攝的人臉圖像上傳,根據用戶信息獲取該用戶之前人臉數據,其實最終的是現在拍攝的人臉與該用戶所關聯之前的人臉圖像進行對比,判斷是否爲「真人」且爲「本人」,快捷的完成身份覈實工作。

筆者的小項目就沒有這麼複雜的,而且信息少,是B端產品,商戶使用。商戶掃描用戶,識別用戶的人臉,查詢人臉數據庫,找到最相似的,當相似度超過80%,就可以判斷該用戶就是我們所需要的客戶,完成身份的確認。我們項目更像是一個會員的管理系統。詳細的流程圖如下:

拍攝會員頭像,上傳頭像並關聯會員的id,系統存儲他們關係,完成用戶註冊;當下次用戶過來,掃描用戶進行人臉檢測、人臉處理、上傳頭像、人臉識別,查詢到該頭像所對應的會員id, 從而得到該用戶的所有信息,從而對該用戶進行後續服務。

然而落實到具體項目中,我們發現如果我們進行人臉識別過程、完成人臉捕獲、質量檢測、頭像處理、人臉檢測、活體檢測、人臉識別等這麼多流程,會導致我們耗時嚴重,體檢較差,我們必須精簡流程,提高人臉識別速度。另外市場上app 要求用戶進行眨眼、說口令等操作,通過我們的一些微表情、微動作判斷攝象頭面前的人是活生生的真人,完成活體檢測,驗證是否真人,而非是虛假的頭像。但是目前筆者還未找到免費開放的活體檢測API,而百度人臉識別API中含有活體檢測參數配置,可以進行靜態的活體檢測,筆者就採用此方案。

人臉識別方面的流程精簡優化爲:

當設備掃描人臉,預覽頭像自動完成人臉定位與檢測。開啓子線程處理掃描得到的圖片,進行人臉檢測,當人臉檢測失敗時繼續獲取圖片開始新一輪的檢測人臉,直至人臉檢測成功。當人臉檢測返回成功,停止掃描人臉檢測,開始處理頭像、上傳頭像、進行人臉識別。另外如果設備對準了人臉頭像,確認頭像無誤,也可以手動拍攝頭像、上傳頭像、進行人臉識別。

3. 人臉識別趟坑細節

3.1 圖片壓縮

目前市面的人臉識別開發平臺的 API 對上傳的圖片都是有一定的限制的,好一點的是10M,很多API限制爲2M, 例如筆者在用的百度人臉識別API中對人臉檢測的上傳圖片限制是小於2M, 人臉識別是小於10M;騰訊優圖對整個請求包體限制爲2M, 那上傳的圖片限制更小了;

在這裏需要注意一個問題,現在很多家人臉識別開放平臺對圖片限制的大小是指base64編碼後大小。我們知道Base64編碼要求把3個8位字節轉化爲4個6位的字節,之後在6位的前面補兩個0,形成8位一個字節的形式。如果剩下的字符不足3個字節,則用0填充,輸出字符使用’=’,因此編碼後輸出的文本末尾可能會出現1或2個’=’。由此可見圖片經base64編碼後數據量會變大,簡單計算大約是原來數據量的4/3倍。 
(具體可以參見http://www.ruanyifeng.com/blog/2008/06/base64.html

所以最終上傳圖片大小限制更小了,筆者使用百度人臉識別接口,限制爲10M,實際上圖片限制大小爲大約爲10*3/4= 7.5M。

目前的手機攝像頭拍照出現的照片像素都很高,動不動就1200W像素,1600W像素,甚至是2000W也常見,拍出來的圖片數據量都很大,一張圖片幾十M現在都很常見。然後這麼大的圖片直接上傳給人臉識別API 肯定是不行的。我們必須要想方設法減少圖片的大小。

另外筆者發現當我們上傳的圖片數據量比較大時,API響應比較慢,筆者曾經測試過當上傳圖片爲10M,API 返回結果時間需要15s左右,當減到5M爲10s左右,當圖片大小減到2M返回結果需要7s左右(統計數據不是很準確,僅供參考)。 
這個原因不難理解,當我們上傳圖片數據量大,我們圖片上傳及服務器處理肯定耗時加長,爲提高API 調用速度,減少響應時間,也必須減少圖片大小,進行圖片壓縮。

減少圖片數據量的原理無非就是兩條:一是縮小圖片的長寬,二是降低圖片的質量。應用中一般我們採用方法如下: 
1. 採用預覽框,讓拍攝時讓頭像落在預覽框中,減少多餘圖像。 
2. 進行人臉檢測,裁剪頭像外多餘圖片 
3. 合理設置壓縮比,縮小圖片 
4. 降低圖片質量

另外圖片壓縮算法Luban,號稱可能是最接近微信朋友圈的圖片壓縮算法,各位可以借鑑一下。

3.2 相機調整

目前的人臉識別都需要相機預覽頭像,並拍攝頭像照片,因此很多工作與相機相關,我們需要設置好相機參數。

當我們對準人臉時候發現,相機離人臉比較遠時,預覽的人臉比較小;當相機靠近時,預覽的人臉比較大。當我們的人臉頭像過小時,頭像質量比較差,人臉檢測及人臉識別容易出差錯。另外測試發現每個手機的放大倍數不是都是相同的,這可能與各個手機的型號相關。如果直接設置爲一個固定值,這可能會在某些手機上過度放大,某些手機上放大的倍數不夠。索性相機的參數設定裏給我們提供了最大的放大倍數值,通過取放大倍數值的N分之一作爲當前的放大倍數,就完美地解決了手機的適配問題。

    private static final int TEN_DESIRED_ZOOM = 27;

      private void setZoom(Camera.Parameters parameters) {
        String zoomSupportedString = parameters.get("zoom-supported");
        if (zoomSupportedString != null && !Boolean.parseBoolean(zoomSupportedString)) {
            return;
        }

        int tenDesiredZoom = TEN_DESIRED_ZOOM;
        String maxZoomString = parameters.get("max-zoom");
        if (maxZoomString != null) {
            try {
                int tenMaxZoom = (int) (10.0 * Double.parseDouble(maxZoomString));
                if (tenDesiredZoom > tenMaxZoom) {
                    tenDesiredZoom = tenMaxZoom;
                }
            } catch (NumberFormatException nfe) {
                Log.e(TAG, "Bad max-zoom: " + maxZoomString);
            }
        }

        String takingPictureZoomMaxString = parameters.get("taking-picture-zoom-max");
        if (takingPictureZoomMaxString != null) {
            try {
                int tenMaxZoom = Integer.parseInt(takingPictureZoomMaxString);
                if (tenDesiredZoom > tenMaxZoom) {
                    tenDesiredZoom = tenMaxZoom;
                }
            } catch (NumberFormatException nfe) {
                Log.e(TAG, "Bad taking-picture-zoom-max: " + takingPictureZoomMaxString);
            }
        }

        String motZoomValuesString = parameters.get("mot-zoom-values");
        if (motZoomValuesString != null) {
            tenDesiredZoom = findBestMotZoomValue(motZoomValuesString, tenDesiredZoom);
        }

        String motZoomStepString = parameters.get("mot-zoom-step");
        if (motZoomStepString != null) {
            try {
                double motZoomStep = Double.parseDouble(motZoomStepString.trim());
                int tenZoomStep = (int) (10.0 * motZoomStep);
                if (tenZoomStep > 1) {
                    tenDesiredZoom -= tenDesiredZoom % tenZoomStep;
                }
            } catch (NumberFormatException nfe) {
                // continue
            }
        }

        if (parameters.isZoomSupported()) {
            Log.e(TAG, "max-zoom:" + parameters.getMaxZoom());
            if (SystemUtils.isMadGlass() && parameters.getMaxZoom() >= 8) {
                parameters.setZoom(8);
            } else {
                parameters.setZoom(parameters.getMaxZoom() / tenDesiredZoom);
            }
        } else {
            Log.e(TAG, "Unsupported zoom.");
        }

        // Most devices, like the Hero, appear to expose this zoom parameter.
        // It takes on values like "27" which appears to mean 2.7x zoom
        // if (takingPictureZoomMaxString != null) {
        // parameters.set("taking-picture-zoom", tenDesiredZoom);
        // }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

另外拍攝頭像時聚焦、預覽尺寸/圖片尺寸選擇、掃描區域等等也需要進行考慮。筆者前段時間做了Zxing優化,涉及相機方面的優化設置,可供參考。

3.3 預覽圖片轉換

目前筆者在拍攝預覽時不停地進行人臉的檢測,檢測出人臉後纔開始處理頭像 
、進行人臉識別。目前進行拍攝預覽,獲取預覽頭像數據也很簡單。只需給 camera 對象設置一個 Camera.PreviewCallback,在這個回調中實現一個方法 onPreviewFrame(byte[] data, Camera camera), 
data 就是我們預覽得到的圖片數據,我們只要將其轉換成bitmap即可,接下來進行人臉檢測等後續工作。然而的預覽數據格式問題,得到數據不是Bitmap格式,不能簡單通過BitmapFactory.decodeByteArray轉換成Bitmap。

Android相機預覽的時候支持幾種不同的格式,從圖像的角度(ImageFormat)來說有NV16、NV21、YUY2、YV12、RGB_565和JPEG,從像素的角度(PixelFormat)來說,對應的是YUV422SP、YUV420SP、YUV422I、YUV420P、RGB565和JPEG。 
目前大部分android手機攝像頭設置的默認格式是yuv420sp,即圖像格式NV21,編碼成YUV的所有像素格式裏,yuv420sp佔用的空間是最小的。預覽得到數據可以經過YuvImage轉換成Bitmap格式。

  private void decode(final byte[] data, int width, int height) {
        YuvImage yuv = new YuvImage(data, ImageFormat.NV21, width, height, null);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        yuv.compressToJpeg(new Rect(0, 0, width, height), 100, out);

        byte[] bytes = out.toByteArray();
        final Bitmap picture = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
      ......
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.4 Android官方人臉識別API

Android 4.1開始提供人臉識別開發API,我們可以根據其API進行簡單的人臉檢測,判斷是否包含人臉,及確認人臉大小,眼距等參數。

     private static final int MAX_FACE_NUM = 4;//最大可以檢測出的人臉數量

    public void faceDetect(Bitmap srcImg) {
        Bitmap bm = srcImg.copy(Bitmap.Config.RGB_565, true);
        //最關鍵的就是下面三句代碼
        FaceDetector faceDetector = new FaceDetector(bm.getWidth(), bm.getHeight(), MAX_FACE_NUM);
        FaceDetector.Face[] faces = new FaceDetector.Face[MAX_FACE_NUM];
        int realFaceNum = faceDetector.findFaces(bm, faces);
        Log.i(tag, "檢測到人臉:n = " + realFaceNum);
        for (int i = 0; i < realFaceNum; i++) {
            FaceDetector.Face f = faces[i];
            PointF midPoint = new PointF();
            float dis = f.eyesDistance();//獲取人臉兩眼的間距
            f.getMidPoint(midPoint); //獲取人臉中心點
            int dd = (int) (dis);
            Point eyeLeft = new Point((int) (midPoint.x - dis / 2), (int) midPoint.y);
            Point eyeRight = new Point((int) (midPoint.x + dis / 2), (int) midPoint.y);
            Rect faceRect = new Rect((int) (midPoint.x - dd), (int) (midPoint.y - dd), (int) (midPoint.x + dd), (int) (midPoint.y + dd));
            Log.i(tag, "左眼座標 x = " + eyeLeft.x + "y = " + eyeLeft.y);
            Canvas canvas = new Canvas(bm);
            Paint p = new Paint();
            p.setAntiAlias(true);
            p.setStrokeWidth(8);
            p.setStyle(Paint.Style.STROKE);
            p.setColor(Color.GREEN);
            canvas.drawCircle(eyeLeft.x, eyeLeft.y, 20, p);
            canvas.drawCircle(eyeRight.x, eyeRight.y, 20, p);
            canvas.drawRect(faceRect, p);
        }
        ImageUtil.saveJpeg(bm);
        Log.i(tag, "保存完畢");
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

而人臉識別特性不是在所有的Android 4.1以上的設備上都支持,有網友測試了谷歌系列的設備,有的設備也不支持使用官方API方式實現人臉識別功能,具體問題還是得追蹤底層是否開放這個功能!我們可以調用getMaxNumDetectedFaces()來檢測設備是否支持。各位在使用官方的人臉檢測 API 時需要注意這一點。

4. 總結

目前筆者按照自己的想法做個了一個人臉識別的demo, 研究下來發現人臉識別技術沒有網上宣傳的那麼酷炫,那麼高大上。

其實目前人臉識別的應用還停留在基礎上,也就是在較好環境中實現1:1人臉識別,而拍照美顏更僅僅應用到了人臉特徵定點提取,連識別預處理都算不上。對應複雜環境、拍攝條件差情況下,人臉識別檢測困難。而對於1:N級和N:N級的人臉識別,也就是單一特徵對比多種特徵和多種特徵對比多種特徵。而這兩種等級的人臉識別在應用上也常常無法提供較好的環境,比如1:N級人臉識別可以應用於失蹤人口搜索中,在特殊情況下拍的照片存在角度、光線的複雜性,加大了特徵提取、對比的難度。可見人臉識別大規模商業應用還是有很長一條路要走,但目前技術進步神速,相信我們未來完全依靠刷臉的時代會來臨。

5. 參考

  1. 百度人臉識別技術文檔
  2. 人臉識別(face recognition
  3. 關於Android 使用官方API 實現人臉檢測功能
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章