從虹軟開放了2.0版本SDK以來,因爲具備免費、離線使用的特色,咱們公司在人臉識別門禁應用中使用了虹軟SDK,識別效果還不錯,所以比較關注虹軟SDK的官方動態。近期上線了ArcFace 3.0 SDK版本,確實作了比較大的更新。首先本篇介紹一下關於Android平臺算法的更新內容,下一篇將針對Windows平臺的算法更新展開介紹。html
在實際開發過程當中使用新的圖像數據結構具備必定的難度,本文將從如下幾點對該圖像數據結構及使用方式進行詳細介紹java
SDK接口變更android
ArcSoftImageInfo類解析算法
SDK相關代碼解析數組
步長的做用數據結構
將Camera2回傳的Image轉換爲ArcSoftImageInfoide
在接入3.0版SDK時,發現FaceEngine
類中的detectFaces
、process
、extractFaceFeature
等傳入圖像數據的函數都有重載函數,重載函數的接口均使用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
。
在我實際使用後發現,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
數據,而後是交叉存儲的V
和U
數據。若是咱們使用的是Camera API
,那基本用不到ArcSoftImageInfo
類,由於Camera API
回傳的NV21
數據是連續的,直接使用舊版接口便可;而當咱們使用的是其餘API時,拿到的數據多是不連續的,例如使用Camera2 API
、MediaCodec
拿到的android.media.Image
類對象,其圖像數據也是分通道的,咱們能夠根據其通道內容,獲取Y
通道數據和VU
通道數據,組成NV21
格式的ArcSoftImageInfo
對象用於處理。
咱們來看下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); }
解讀: 各個圖像數據的要求以下:
NV21
格式圖像數據的高度是偶數,數據大小是:寬x高x3/2
BGR24
格式圖像數據的大小是:寬x高x3
GRAY
格式圖像數據的大小是:寬x高
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; } }
解讀:
高度x每一個通道的步長
BGR24
、GRAY
、DEPTH_U16
格式圖像數據都只有一個通道,但上述示例組成方式說明中提到它們的步長不一樣,關係以下:
BGR24
格式圖像數據步長通常爲3 x width
GRAY
格式圖像數據步長通常爲width
DEPTH_U16
格式圖像數據步長通常爲2 x width
NV21
格式圖像數據的高度是偶數,有兩個通道,且第0個通道的數據大小是第1個通道數據大小的2倍。具體踩坑舉例
以下圖,這是在某臺手機上使用Camera2 API
時,指定了以1520x760
分辨率進行預覽時獲取的數據。雖然指定的分辨率是1520x760
,可是預覽數據的實際大小倒是1536x760
,解析存下的圖像數據,發現右邊填充的16像素內容均爲0,此時若咱們以1520x760的分辨率去將這組YUV數據取出並轉換爲NV21
,並在進行人臉檢測時傳入的寬度是1520,SDK將沒法檢測到人臉;若咱們以1536x760的分辨率去解析,生成的NV21
傳給SDK,而且傳入的寬度是1536時,SDK可以檢測到人臉。
步長的重要性 只是差了這幾個像素,爲何就致使人臉檢測不到了呢?以前說到過,步長能夠理解爲一行像素的字節數。若是第一行像素的讀取有誤差,那後續像素的讀取也會受到影響。<br>
如下是對一張大小爲1000x554
的NV21
圖像數據,以不一樣步長進行解析的結果:
以正確的步長解析 | 以錯誤的步長解析 |
---|---|
能夠看到,對於一張圖像,若是使用了錯誤的步長去解析,咱們可能就沒法看到正確的圖像內容。
結論:經過引入圖像步長可以有效的避免高字節對齊的問題。
Camera2 API回傳數據處理
對於以上場景,咱們可提取android.media.Image
對象的Y
、U
、V
通道數據,組成NV21
格式的ArcSoftImageInfo
對象,傳入SDK處理。示例代碼以下:
Camera2 API
回傳數據的Y
、U
、V
通道數據 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
對象並檢測的具體實現。如下是將Y
、U
、V
數據組成ArcSoftImageInfo
對象的具體實現。
Y
、U
、V
數據組成ArcSoftImageInfo
對象對於
Y
通道,直接拷貝便可,對於U
通道和V
通道,須要考慮這組YUV數據的格式是YUV420
仍是YUV422
,再獲取其中的U
、V
數據
/** * 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; } }
ArcSoftImageInfo
對象傳入分離的圖像數據可避免數據拼接所需的額外內存消耗。Android Demo可在虹軟人臉識別開放平臺下載