影象格式轉化在人臉識別應用中的實踐
ArcFace 2.0 API目前支援多種影象格式:BGR24、NV21、NV12、I420、YUYV(Android、IOS只支援其中的部分)。接下來將開始介紹這幾種影象格式以及部分轉換方式。
一、相關影象顏色空間介紹
1.RGB顏色空間
RGB顏色空間以Red、Green、Blue三種基本色為基礎,進行不同程度的疊加,產生豐富而廣泛的顏色,所以俗稱三基色模式。
常見的RGB格式有:RGB_565、RGB_888、ARGB_8888、ARGB_4444等。
2.YUV顏色空間
在YUV顏色空間中,Y用來表示亮度,U和V用來表示色度。
常見的YUV格式有以下幾大類:
planar: Y、U、V全部連續儲存,如I420、YV12
packed: Y、U、V交叉儲存,如YUYV
semi-planar: Y連續儲存,U、V交叉儲存,如NV21、NV12
二、相關影象格式介紹
1.BGR24影象格式
BGR24影象格式是一種採用24bpp(bit per pixel)的格式。每個顏色通道B、G、R各佔8bpp。
排列方式如:
B G RB G RB G RB G RB G RB G RB G RB G R B G RB G RB G RB G RB G RB G RB G RB G R B G RB G RB G RB G RB G RB G RB G RB G R B G RB G RB G RB G RB G RB G RB G RB G
2.NV21影象格式
NV21影象格式屬於 YUV顏色空間中的YUV420SP格式,每四個Y分量共用一組U分量和V分量,Y連續排序,U與V交叉排序。
排列方式如:
Y YY YY YY Y Y YY YY YY Y Y YY YY YY Y Y YY YY YY Y V UV UV UV U V UV UV UV U
3.NV12影象格式
NV12影象格式屬於 YUV顏色空間中的YUV420SP格式,每四個Y分量共用一組U分量和V分量,Y連續排序,U與V交叉排序(NV12和NV21只是U與V的位置相反)。
排列方式如:
Y YY YY YY Y Y YY YY YY Y Y YY YY YY Y Y YY YY YY Y U VU VU VU V U VU VU VU V
4.I420影象格式
I420影象格式屬於 YUV顏色空間中的YUV420P格式,每四個Y分量共用一組U分量和V分量,Y、U、V各自連續排序。
排列方式如:
Y YY YY YY Y Y YY YY YY Y Y YY YY YY Y Y YY YY YY Y UUUUUUUU VVVVVVVV
5.YV12影象格式
YV12影象格式屬於 YUV顏色空間中的YUV420P格式,每四個Y分量共用一組U分量和V分量,Y、U、V各自連續排序(YV12和I420只是U與V的位置相反)。
排列方式如:
Y YY YY YY Y Y YY YY YY Y Y YY YY YY Y Y YY YY YY Y VVVVVVVV UUUUUUUU
6.YUYV影象格式 YUYV影象格式屬於 YUV顏色空間中的YUV422格式,每兩個Y分量公用一組U分量和V分量,Y、U、V交叉排序。 排列方式如:
Y U Y VY U Y VY U Y VY U Y V Y U Y VY U Y VY U Y VY U Y V Y U Y VY U Y VY U Y VY U Y V Y U Y VY U Y VY U Y VY U Y V
三、影象格式轉換
由於影象的格式多種多樣,轉換的方法也不勝列舉,以下只列出部分的影象轉換參考程式碼。
1.從Bitmap中獲取ARGB_8888影象格式資料(Android平臺)
Bitmap支援多種格式:ALPHA_8,RGB_565,ARGB_4444,ARGB_8888,RGBA_F16,HARDWARE。我們目前主要選擇ARGB_8888進行格式轉換。
我們可使用Bitmap類中的
public void getPixels(@ColorInt int[] pixels, int offset, int stride, int x, int y, int width, int height)
方法獲取int[]型別的argb資料或
public void copyPixelsToBuffer (Buffer dst)方法獲取byte[]型別的ARGB_8888資料。
2.ARGB_8888轉換為NV21
根據一個比較常見的rgb轉yuv的演算法:
int y = (66 * r + 129 * g + 25 * b + 128 >> 8) + 16; int u = (-38 * r - 74 * g + 112 * b + 128 >> 8) + 128; int v = (112 * r - 94 * g - 18 * b + 128 >> 8) + 128;
即可編寫ARGB轉NV21的方法。 int[]型別的ARGB_8888資料轉換為NV21:
private static byte[] argbToNv21(int[] argb, int width, int height) { int yIndex = 0; int uvIndex = width * height; int argbIndex = 0; byte[] nv21 = new byte[width * height * 3 / 2]; for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { //對於int型color資料,格式為0xAARRGGBB,可進行與運算後移位取對應A R G B, //但是該YUV轉換公式中不需要ALPHA,因此我們只需要取R G B 即可。 int r = (argb[argbIndex] & 0xFF0000) >> 16; int g = (argb[argbIndex] & 0x00FF00) >> 8; int b = argb[argbIndex] & 0x0000FF; //獲取該畫素點的R G B,並轉換為Y U V,但byte範圍是0x00~0xFF,因此在賦值時還需進行判斷 int y = (66 * r + 129 * g + 25 * b + 128 >> 8) + 16; int u = (-38 * r - 74 * g + 112 * b + 128 >> 8) + 128; int v = (112 * r - 94 * g - 18 * b + 128 >> 8) + 128; nv21[yIndex++] = (byte) (y < 0 ? 0 : (y > 0xFF ? 0xFF : y)); if ((j & 1) == 0 && (argbIndex & 1) == 0 && uvIndex < nv21.length - 2) { nv21[uvIndex++] = (byte) (v < 0 ? 0 : (v > 0xFF ? 0xFF : v)); nv21[uvIndex++] = (byte) (u < 0 ? 0 : (u > 0xFF ? 0xFF : u)); } ++argbIndex; } } return nv21; }
byte[]型別的ARGB_8888資料轉換為NV21(原理同方法1):
private static byte[] argbToNv21(byte[] argb, int width, int height) { int yIndex = 0; int uvIndex = width * height; int argbIndex = 0; byte[] nv21 = new byte[width * height * 3 / 2]; for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { argbIndex++; int r = argb[argbIndex++]; int g = argb[argbIndex++]; int b = argb[argbIndex++]; /** * byte在強制轉換為int時高位會自動以符號位擴充,如: * 0x80(byte型別,十六進位制)-> 10000000(byte型別,二進位制) -> 11111111_11111111_11111111_10000000(int型別,二進位制) -> -128(int型別,十進位制) * 0x7F(byte型別,十六進位制)-> 01111111(byte型別,二進位制) -> 00000000_00000000_00000000_01111111(int型別,二進位制) -> 127(int型別,十進位制) * 因此需要取低八位獲取原byte的無符號值 */ r &= 0x000000FF; g &= 0x000000FF; b &= 0x000000FF; int y = ((66 * r + 129 * g + 25 * b + 128 >> 8) + 16); int u = ((-38 * r - 74 * g + 112 * b + 128 >> 8) + 128); int v = ((112 * r - 94 * g - 18 * b + 128 >> 8) + 128); nv21[yIndex++] = (byte) (y > 0xFF ? 0xFF : (y < 0 ? 0 : y)); if ((j & 1) == 0 && ((argbIndex >> 2) & 1) == 0 && uvIndex < nv21.length - 2) { nv21[uvIndex++] = (byte) (v > 0xFF ? 0xFF : (v < 0 ? 0 : v)); nv21[uvIndex++] = (byte) (u > 0xFF ? 0xFF : (u < 0 ? 0 : u)); } }
3.ARGB_8888轉換為BGR_24
舉個例子,對於4x2的圖片,ARGB_8888格式內容為:
A1 R1 G1 B1A2 R2 G2 B2A3 R3 G3 B3A4 R4 G4 B4 A5 R5 G5 B5A6 R6 G6 B6A7 R7 G7 B7A8 R8 G8 B8
那麼若需要轉化為BGR_24,內容將變成:
B1 G1 R1B2 G2 R2B3 G3 R3B4 G4 R4 B5 G5 R5B6 G6 R6B7 G7 R7B8 G8 R8
BGR_24內容為3個byte一組,ARGB_8888內容為4個byte一組。因此,對於第一組ARGB_8888(A1 R1 G1 B1)和第一組BGR_24(B1 G1 R1),其對應關係為:
bgr24[0] = argb8888[3]; bgr24[1] = argb8888[2]; bgr24[2] = argb8888[1];
對應的轉換程式碼:
public static byte[] argb8888ToBgr24(byte[] argb8888) { if (argb8888 == null){ throw new IllegalArgumentException("invalid image params!"); } int groupNum = argb8888.length / 4; byte[] bgr24 = new byte[groupNum * 3]; int bgr24Index = 0; int argb8888Index = 0; for (int i = 0; i < groupNum; i++) { bgr24[bgr24Index + 0] = argb8888[argb8888Index + 2]; bgr24[bgr24Index + 1] = argb8888[argb8888Index + 1]; bgr24[bgr24Index + 2] = argb8888[argb8888Index + 0]; bgr24Index += 3; argb8888Index += 4; } return bgr24; }
4.NV12和NV21的互換
NV21和NV12只是U與V的資料位置不同,因此,NV21轉換為NV12的程式碼同樣適用於NV12轉換為NV21。可參考如下程式碼:
public static byte[] nv21ToNv12(byte[] nv21, int width, int height) { if (nv21 == null || nv21.length == 0 || width * height * 3 / 2 != nv21.length) { throw new IllegalArgumentException("invalid image params!"); } final int ySize = width * height; int totalSize = width * height * 3 / 2; byte[] nv12 = new byte[nv21.length]; //複製Y System.arraycopy(nv21, 0, nv12, 0, ySize); //UV互換 for (int uvIndex = ySize; uvIndex < totalSize; uvIndex += 2) { nv12[uvIndex] = nv21[uvIndex + 1]; nv12[uvIndex + 1] = nv21[uvIndex]; } return nv12; }
5.NV21轉YV12
NV21轉化為YV12的過程主要是將其UV資料的交叉排序修改為連續排序。可參考如下程式碼:
public static byte[] nv21ToYv12(byte[] nv21, int width, int height) { if (nv21 == null || nv21.length == 0 || width * height * 3 / 2 != nv21.length) { throw new IllegalArgumentException("invalid image params!"); } final int ySize = width * height; int totalSize = width * height * 3 / 2; byte[] yv12 = new byte[nv21.length]; int yv12UIndex = ySize; int yv12VIndex = ySize * 5 / 4; //複製Y System.arraycopy(nv21, 0, yv12, 0, ySize); //複製UV for (int uvIndex = ySize; uvIndex < totalSize; uvIndex += 2) { yv12[yv12UIndex++] = nv21[uvIndex]; yv12[yv12VIndex++] = nv21[uvIndex + 1]; } return yv12; }
6.YUYV轉NV12
在YUYV格式中,兩個Y共用一組U和V,而NV12是四個Y公用一組U和V,因此,若需要將YUYV轉化為NV12,需要捨棄一半的U和V。可參考如下程式碼:
public static byte[] yuyvToNv12(byte[] yuyv, int width, int height) { if (yuyv == null || yuyv.length == 0) { throw new IllegalArgumentException("invalid image params!"); } int ySize = yuyv.length / 2; byte[] nv12 = new byte[yuyv.length * 3 / 4]; int nv12YIndex = 0; int nv12UVIndex = ySize; boolean copyUV = false; int lineDataSize = width * 2; for (int i = 0, yuyvIndex = 0; i < height; i++, yuyvIndex += lineDataSize) { if (copyUV) { for (int lineOffset = 0; lineOffset < lineDataSize; lineOffset += 4) { //複製Y nv12[nv12YIndex++] = yuyv[yuyvIndex + lineOffset]; nv12[nv12YIndex++] = yuyv[yuyvIndex + lineOffset + 2]; //複製UV nv12[nv12UVIndex++] = yuyv[yuyvIndex + lineOffset + 1]; nv12[nv12UVIndex++] = yuyv[yuyvIndex + lineOffset + 3]; } } else { for (int lineOffset = 0; lineOffset < lineDataSize; lineOffset += 4) { //複製Y nv12[nv12YIndex++] = yuyv[yuyvIndex + lineOffset]; nv12[nv12YIndex++] = yuyv[yuyvIndex + lineOffset + 2]; } } copyUV = !copyUV; } return nv12; }
7.I420和YV12的互換
I420和YV12只是U與V的資料位置不同,因此,I420轉換為YV12的程式碼同樣適用於YV12轉換為I420。可參考如下程式碼:
public static byte[] i420ToYv12(byte[] i420) { if (i420 == null || i420.length == 0 || i420.length % 6 != 0) { throw new IllegalArgumentException("invalid image params!"); } int ySize = i420.length * 2 / 3; int uvSize = i420.length / 6; byte[] yv12 = new byte[i420.length]; //複製Y System.arraycopy(i420, 0, yv12, 0, ySize); //UV互換 System.arraycopy(i420, ySize, yv12, ySize + uvSize, uvSize); System.arraycopy(i420, ySize + uvSize, yv12, ySize, uvSize); return yv12; }