虹軟人臉識別3.0 - 圖像數據結構介紹(Android)

從虹軟開放了2.0版本SDK以來,因爲具備免費、離線使用的特色,咱們公司在人臉識別門禁應用中使用了虹軟SDK,識別效果還不錯,所以比較關注虹軟SDK的官方動態。近期上線了ArcFace 3.0 SDK版本,確實作了比較大的更新。首先本篇介紹一下關於Android平臺算法的更新內容,下一篇將針對Windows平臺的算法更新展開介紹。html

  • 特徵比對支持比對模型選擇,有生活照比對模型人證比對模型
  • 識別率、防攻擊效果顯著提高
  • 特徵值更新,升級後人臉庫需從新註冊
  • Android平臺新增64位的SDK
  • 圖像處理工具類
  • 人臉檢測同時支持全角度及單一角度
  • 新增了一種圖像數據傳入方式

在實際開發過程當中使用新的圖像數據結構具備必定的難度,本文將從如下幾點對該圖像數據結構及使用方式進行詳細介紹java

  1. SDK接口變更android

  2. ArcSoftImageInfo類解析算法

  3. SDK相關代碼解析數組

  4. 步長的做用數據結構

  5. 將Camera2回傳的Image轉換爲ArcSoftImageInfoide

1、SDK接口變更

在接入3.0版SDK時,發現FaceEngine類中的detectFacesprocessextractFaceFeature等傳入圖像數據的函數都有重載函數,重載函數的接口均使用ArcSoftImageInfo對象做爲入參的圖像數據,以人臉檢測爲例,具體接口以下:函數

原始接口:工具

public int detectFaces(byte[] data, int width, int height, int format, List<FaceInfo> faceInfoList)

新增接口:ui

public int detectFaces(ArcSoftImageInfo arcSoftImageInfo, List<FaceInfo> faceInfoList)

能夠看到,重載函數傳入ArcSoftImageInfo對象做爲圖像數據進行檢測,arcSoftImageInfo替代了原來的data, width, height, format

2、ArcSoftImageInfo類解析

在我實際使用後發現,ArcSoftImageInfo不僅是簡單封裝一下,它還將一維數組data修改成二維數組planes,還新增了一個與planes對應的步長數組strides

步長概念介紹: 步長能夠理解爲一行像素的字節數。

類結構以下:

public class ArcSoftImageInfo {
    private int width;
    private int height;
    private int imageFormat;
    private byte[][] planes;
    private int[] strides;
    ...
}

官方文檔中對該類的介紹:

  • 成員描述
類型 變量名 描述
int width 圖像寬度
int height 圖像高度
int imageFormat 圖像格式
byte[][] planes 圖像通道
int[] strides 每一個圖像通道的步長
  • 組成方式介紹
// arcSoftImageInfo組成方式舉例:

// NV21格式數據,有兩個通道,
// Y通道步長通常爲圖像寬度,若圖像通過8字節對齊、16字節對齊等操做,需填入對齊後的圖像步長
// VU通道步長通常爲圖像寬度,若圖像通過8字節對齊、16字節對齊等操做,需填入對齊後的圖像步長
ArcSoftImageInfo arcSoftImageInfo = new ArcSoftImageInfo(width, height, FaceEngine.CP_PAF_NV21, new byte[][]{planeY, planeVU}, new int[]{yStride, vuStride});

// GRAY,只有一個通道,
// 步長通常爲圖像寬度,若圖像通過8字節對齊、16字節對齊等操做,需填入對齊後的圖像步長
arcSoftImageInfo = new ArcSoftImageInfo(width, height, FaceEngine.CP_PAF_GRAY, new byte[][]{gray}, new int[]{grayStride});

// BGR24,只有一個通道,
// 步長通常爲圖像寬度的三倍,若圖像通過8字節對齊、16字節對齊等操做,需填入對齊後的圖像步長
arcSoftImageInfo = new ArcSoftImageInfo(width, height, FaceEngine.CP_PAF_BGR24, new byte[][]{bgr24}, new int[]{bgr24Stride});

// DEPTH_U16,只有一個通道,
// 步長通常爲圖像寬度的兩倍,若圖像通過8字節對齊、16字節對齊等操做,需填入對齊後的圖像步長
arcSoftImageInfo = new ArcSoftImageInfo(width, height, FaceEngine.CP_PAF_DEPTH_U16, new byte[][]{depthU16}, new int[]{depthU16Stride});

能夠看到,ArcSoftImageInfo用於存儲分離的圖像數據,以NV21數據爲例,NV21數據有兩個通道,那二維數組planes存儲的就是兩個數組:y數組和vu數組。如下是NV21數據的排列方式:

NV21圖像格式屬於 YUV顏色空間中的YUV420SP格式,每四個Y份量共用一組U份量和V份量,Y連續存儲,U與V交叉存儲。

排列方式以下(以8x4的圖像爲例):

Y Y   Y Y   Y Y   Y Y
Y Y   Y Y   Y Y   Y Y
Y Y   Y Y   Y Y   Y Y
Y Y   Y Y   Y Y   Y Y
V U   V U   V U  V U
V U   V U   V U  V U

以上數據分爲兩個通道,首先是連續的Y數據,而後是交叉存儲的VU數據。若是咱們使用的是Camera API,那基本用不到ArcSoftImageInfo類,由於Camera API回傳的NV21數據是連續的,直接使用舊版接口便可;而當咱們使用的是其餘API時,拿到的數據多是不連續的,例如使用Camera2 APIMediaCodec拿到的android.media.Image類對象,其圖像數據也是分通道的,咱們能夠根據其通道內容,獲取Y通道數據和VU通道數據,組成NV21格式的ArcSoftImageInfo對象用於處理。

3、SDK相關代碼解析

咱們來看下SDK中判斷圖像數據是否合法的校驗代碼:

注:原始代碼因爲被編譯器修改過,閱讀體驗不佳,如下代碼是我修改過的,將常量值替換回常量名,更便於閱讀。

  • 校驗分離的圖像信息數據

    private static boolean isImageDataValid(byte[] data, int width, int height, int format) {
            return 
            (format == CP_PAF_NV21 && (height & 1) == 0 && data.length == width * height * 3 / 2)|| 
            (format == CP_PAF_BGR24 && data.length == width * height * 3)|| 
            (format == CP_PAF_GRAY && data.length == width * height) ||
            (format == CP_PAF_DEPTH_U16 && data.length == width * height * 2);
        }

    解讀: 各個圖像數據的要求以下:

    1. NV21格式圖像數據的高度是偶數,數據大小是:寬x高x3/2
    2. BGR24格式圖像數據的大小是:寬x高x3
    3. GRAY格式圖像數據的大小是:寬x高
    4. DEPTH_U16格式圖像數據的大小是:寬x高x2
  • 校驗ArcSoftImageInfo對象

    private static boolean isImageDataValid(ArcSoftImageInfo arcSoftImageInfo) {
            byte[][] planes = arcSoftImageInfo.getPlanes();
            int[] strides = arcSoftImageInfo.getStrides();
            if (planes != null && strides != null) {
                if (planes.length != strides.length) {
                    return false;
                } else {
                    byte[][] var3 = planes;
                    int var4 = planes.length;
    
                    for(int var5 = 0; var5 < var4; ++var5) {
                        byte[] plane = var3[var5];
                        if (plane == null || plane.length == 0) {
                            return false;
                        }
                    }
    
                    switch(arcSoftImageInfo.getImageFormat()) {
                    case CP_PAF_BGR24:
                    case CP_PAF_GRAY:
                    case CP_PAF_DEPTH_U16:
                        return planes.length == 1 && planes[0].length == arcSoftImageInfo.getStrides()[0] * arcSoftImageInfo.getHeight();
                    case CP_PAF_NV21:
                        return (arcSoftImageInfo.getHeight() & 1) == 0 && planes.length == 2 && planes[0].length == planes[1].length * 2 && planes[0].length == arcSoftImageInfo.getStrides()[0] * arcSoftImageInfo.getHeight() && planes[1].length == arcSoftImageInfo.getStrides()[1] * arcSoftImageInfo.getHeight() / 2;
                    default:
                        return false;
                    }
                }
            } else {
                return false;
            }
        }

    解讀:

    1. 每一個通道數據的大小是:高度x每一個通道的步長
    2. BGR24GRAYDEPTH_U16格式圖像數據都只有一個通道,但上述示例組成方式說明中提到它們的步長不一樣,關係以下:
      • BGR24格式圖像數據步長通常爲3 x width
      • GRAY格式圖像數據步長通常爲width
      • DEPTH_U16格式圖像數據步長通常爲2 x width
    3. NV21格式圖像數據的高度是偶數,有兩個通道,且第0個通道的數據大小是第1個通道數據大小的2倍。

4、步長的做用

  • 具體踩坑舉例

    以下圖,這是在某臺手機上使用Camera2 API時,指定了以1520x760分辨率進行預覽時獲取的數據。雖然指定的分辨率是1520x760,可是預覽數據的實際大小倒是1536x760,解析存下的圖像數據,發現右邊填充的16像素內容均爲0,此時若咱們以1520x760的分辨率去將這組YUV數據取出並轉換爲NV21,並在進行人臉檢測時傳入的寬度是1520,SDK將沒法檢測到人臉;若咱們以1536x760的分辨率去解析,生成的NV21傳給SDK,而且傳入的寬度是1536時,SDK可以檢測到人臉。

    高字節對齊

  • 步長的重要性 只是差了這幾個像素,爲何就致使人臉檢測不到了呢?以前說到過,步長能夠理解爲一行像素的字節數。若是第一行像素的讀取有誤差,那後續像素的讀取也會受到影響。<br>

如下是對一張大小爲1000x554NV21圖像數據,以不一樣步長進行解析的結果:

以正確的步長解析 以錯誤的步長解析
以1000爲步長解析 以996爲步長解析

能夠看到,對於一張圖像,若是使用了錯誤的步長去解析,咱們可能就沒法看到正確的圖像內容。

結論:經過引入圖像步長可以有效的避免高字節對齊的問題。

5、將Camera2回傳的Image轉換爲ArcSoftImageInfo

  • Camera2 API回傳數據處理
    對於以上場景,咱們可提取android.media.Image對象的YUV通道數據,組成NV21格式的ArcSoftImageInfo對象,傳入SDK處理。示例代碼以下:

    • 取出Camera2 API回傳數據的YUV通道數據
      private class OnImageAvailableListenerImpl implements ImageReader.OnImageAvailableListener{
                     private byte[] y;
                     private byte[] u;
                     private byte[] v;
      
                     @Override
                     public void onImageAvailable(ImageReader reader) {
                         Image image = reader.acquireNextImage();
                         // 實際結果通常是 Y:U:V == 4:2:2
                         if (camera2Listener != null && image.getFormat() == ImageFormat.YUV_420_888) {
                             Image.Plane[] planes = image.getPlanes();
                             // 重複使用同一批byte數組,減小gc頻率
                             if (y == null) {
                                 y = new byte[planes[0].getBuffer().limit() - planes[0].getBuffer().position()];
                                 u = new byte[planes[1].getBuffer().limit() - planes[1].getBuffer().position()];
                                 v = new byte[planes[2].getBuffer().limit() - planes[2].getBuffer().position()];
                             }
                             if (image.getPlanes()[0].getBuffer().remaining() == y.length) {
                                 planes[0].getBuffer().get(y);
                                 planes[1].getBuffer().get(u);
                                 planes[2].getBuffer().get(v);
                                 camera2Listener.onPreview(y, u, v, mPreviewSize, planes[0].getRowStride());
                             }
                         }
                         image.close();
                     }
                 }
    • 轉換爲ArcSoftImageInfo對象

    注意: 拿到的YUV數據多是YUV422,也多是YUV420,須要分別實現二者轉換爲NV21格式的ArcSoftImageInfo對象的函數。

    @Override
           public void onPreview(final byte[] y, final byte[] u, final byte[] v, final Size previewSize, final int stride) {
               if (arcSoftImageInfo == null) {
                   arcSoftImageInfo = new ArcSoftImageInfo(previewSize.getWidth(), previewSize.getHeight(), FaceEngine.CP_PAF_NV21);
               }
               // 回傳數據是YUV422
               if (y.length / u.length == 2) {
                   ImageUtil.yuv422ToNv21ImageInfo(y, u, v, arcSoftImageInfo, stride, previewSize.getHeight());
               }
               // 回傳數據是YUV420
               else if (y.length / u.length == 4) {
                   ImageUtil.yuv420ToNv21ImageInfo(y, u, v, arcSoftImageInfo, stride, previewSize.getHeight());
               }
               // 此時的arcSoftImageInfo數據便可傳給SDK使用
               if (faceEngine != null) {
                   List<FaceInfo> faceInfoList = new ArrayList<>();
                   int code = faceEngine.detectFaces(arcSoftImageInfo, faceInfoList);
                   if (code == ErrorInfo.MOK) {
                       Log.i(TAG, "onPreview: " + code + "  " + faceInfoList.size());
                   } else {
                       Log.i(TAG, "onPreview: no face detected , code is : " + code);
                   }
               } else {
                   Log.e(TAG, "onPreview: faceEngine is null");
                   return;
               }
               ...
           }

以上代碼中即是Camera2 API回傳的數據轉換爲ArcSoftImageInfo對象並檢測的具體實現。如下是將YUV數據組成ArcSoftImageInfo對象的具體實現。

  • YUV數據組成ArcSoftImageInfo對象

對於Y通道,直接拷貝便可,對於U通道和V通道,須要考慮這組YUV數據的格式是YUV420仍是YUV422,再獲取其中的UV數據

/**
          * YUV420數據轉換爲NV21格式的ArcSoftImageInfo
          *
          * @param y                YUV420數據的y份量
          * @param u                YUV420數據的u份量
          * @param v                YUV420數據的v份量
          * @param arcSoftImageInfo NV21格式的ArcSoftImageInfo
          * @param stride           y份量的步長,通常狀況下,因爲YUV數據的對應關係,Y份量步長肯定了,U和V也隨之肯定
          * @param height           圖像高度
          */
         public static void yuv420ToNv21ImageInfo(byte[] y, byte[] u, byte[] v, ArcSoftImageInfo arcSoftImageInfo, int stride, int height) {
             if (arcSoftImageInfo.getPlanes() == null) {
                 arcSoftImageInfo.setPlanes(new byte[][]{new byte[stride * height], new byte[stride * height / 2]});
                 arcSoftImageInfo.setStrides(new int[]{stride, stride});
             }
             System.arraycopy(y, 0, arcSoftImageInfo.getPlanes()[0], 0, y.length);
             // 注意,vuLength 不能直接經過步長和高度計算,實測發現Camera2 API回傳的數據有數據丟失,須要使用真實數據長度
             byte[] vu = arcSoftImageInfo.getPlanes()[1];
             int vuLength = u.length / 2 + v.length / 2;
             int uIndex = 0, vIndex = 0;
             for (int i = 0; i < vuLength; i++) {
                 vu[i] = v[vIndex++];
                 vu[i + 1] = u[uIndex++];
             }
         }
         /**
          * YUV422數據轉換爲NV21格式的ArcSoftImageInfo
          *
          * @param y                YUV422數據的y份量
          * @param u                YUV422數據的u份量
          * @param v                YUV422數據的v份量
          * @param arcSoftImageInfo NV21格式的ArcSoftImageInfo
          * @param stride           y份量的步長,通常狀況下,因爲YUV數據的對應關係,Y份量步長肯定了,U和V也隨之肯定
          * @param height           圖像高度
          */
         public static void yuv422ToNv21ImageInfo(byte[] y, byte[] u, byte[] v, ArcSoftImageInfo arcSoftImageInfo, int stride, int height) {
             if (arcSoftImageInfo.getPlanes() == null) {
                 arcSoftImageInfo.setPlanes(new byte[][]{new byte[stride * height], new byte[stride * height / 2]});
                 arcSoftImageInfo.setStrides(new int[]{stride, stride});
             }
             System.arraycopy(y, 0, arcSoftImageInfo.getPlanes()[0], 0, y.length);
             byte[] vu = arcSoftImageInfo.getPlanes()[1];
             // 注意,vuLength 不能直接經過步長和高度計算,實測發現Camera2 API回傳的數據有數據丟失,須要使用真實數據長度
             int vuLength = u.length / 2 + v.length / 2;
             int uIndex = 0, vIndex = 0;
             for (int i = 0; i < vuLength; i += 2) {
                 vu[i] = v[vIndex];
                 vu[i + 1] = u[uIndex];
                 vIndex += 2;
                 uIndex += 2;
             }
         }

6、ArcSoftImageInfo優勢總結

  1. 在獲取的圖像數據源是分通道的數據時,使用ArcSoftImageInfo對象傳入分離的圖像數據可避免數據拼接所需的額外內存消耗。
  2. 引入了步長的概念,在使用時傳入了各個通道的步長,使開發者在使用SDK時對圖像數據的瞭解更清晰。

Android Demo可在虹軟人臉識別開放平臺下載

相關文章
相關標籤/搜索