作者:位元組流動
來源:
https://blog.csdn.net/Kennethdroid/article/details/94031821YUV 的由來
YUV 是一種色彩編碼模型,也叫做 YCbCr,其中 “Y” 表示明亮度(Luminance),“U” 和 “V” 分别表示色度(Chrominance)和濃度(Chroma)。
YUV 色彩編碼模型,其設計初衷為了解決彩色電視機與黑白電視的相容問題,利用了人類眼睛的生理特性(對亮度敏感,對色度不敏感),允許降低色度的帶寬,降低了傳輸帶寬。
在計算機系統中應用尤為廣泛,利用 YUV 色彩編碼模型可以降低圖檔資料的記憶體占用,提高資料處理效率。
另外,YUV 編碼模型的圖像資料一般不能直接用于顯示,還需要将其轉換為 RGB(RGBA) 編碼模型,才能夠正常顯示圖像。
YUV 幾種常見采樣方式

YUV 圖像主流的采樣方式有三種:
- YUV 4:4:4,每一個 Y 分量對于一對 UV 分量,每像素占用 (Y + U + V = 8 + 8 + 8 = 24bits)3 位元組
- YUV 4:2:2,每兩個 Y 分量共用一對 UV 分量,每像素占用 (Y + 0.5U + 0.5V = 8 + 4 + 4 = 16bits)2 位元組
- YUV 4:2:0,每四個 Y 分量共用一對 UV 分量,每像素占用 (Y + 0.25U + 0.25V = 8 + 2 + 2 = 12bits)1.5 位元組
其中最常用的采樣方式是 YUV422 和 YUV420 。
YUV 格式也可按照 YUV 三個分量的組織方式分為打包(Packed)格式和平面格式(Planar)。
- 打包(Packed)格式:每個像素點的 YUV 分量是連續交叉存儲的,如 YUYV 格式;
- 平面格式(Planar):YUV 圖像資料的三個分量分别存放在不同的矩陣中,這種格式适用于采樣,如 YV12、YU12 格式;
YUV 幾種常用的格式
下面以一幅分辨率為 4x4 的 YUV 圖為例,說明在不同 YUV 格式下的存儲方式(括号内範圍表示記憶體位址索引範圍,預設以下不同格式圖檔存儲使用的都是連續記憶體)。
YUYV (YUV422 采樣方式)
YUYV 格式的存儲格式
(0 ~ 7) Y00 U00 Y01 V00 Y02 U01 Y03 V01
(8 ~ 15) Y10 U10 Y11 V10 Y12 U11 Y13 V11
(16 ~ 23) Y20 U20 Y21 V20 Y22 U21 Y23 V21
(24 ~ 31) Y30 U30 Y31 V30 Y32 U31 Y33 V31
一幅 720P (1280x720分辨率) 的圖檔,使用 YUV422 采樣時占用存儲大小為:
Y 分量:1280 * 720 = 921600 位元組
U 分量:1280 * 720 * 0.5 = 460800 位元組
V 分量:1280 * 720 * 0.5 = 460800 位元組
總大小:Y 分量 + U 分量 + V 分量 = (1280 * 720 + 1280 * 720 * 0.5 * 2) / 1024 / 1024 = 1.76 MB
由上面計算可以看出 YUV422 采樣的圖像比 RGB 模型圖像節省了 1/3 的存儲空間。,在傳輸時占用的帶寬也會随之減小。
YV12/YU12 (YUV420 采樣方式)
YV12/YU12 也屬于 YUV420P ,即 YUV420 采樣方式的平面模式,YUV 三個分量分别存儲于 3 個不同的矩陣(平面)。
YV12 格式的存儲方式
(0 ~ 3) Y00 Y01 Y02 Y03
(4 ~ 7) Y10 Y11 Y12 Y13
(8 ~ 11) Y20 Y21 Y22 Y23
(12 ~ 15) Y30 Y31 Y32 Y33
(16 ~ 17) V00 V01
(18 ~ 19) V10 V11
(20 ~ 21) U00 U01
(22 ~ 23) U10 U11
YU12(也稱 I420) 格式的存儲方式
(0 ~ 3) Y00 Y01 Y02 Y03
(4 ~ 7) Y10 Y11 Y12 Y13
(8 ~ 11) Y20 Y21 Y22 Y23
(12 ~ 15) Y30 Y31 Y32 Y33
(16 ~ 17) U00 U01
(18 ~ 19) U10 U11
(20 ~ 21) V00 V01
(22 ~ 23) V10 V11
一幅 720P (1280x720分辨率) 的圖檔,使用 YUV420 采樣時(格式 YV12/YU12 )占用存儲大小為:
Y 分量:1280 * 720 = 921600 位元組
U 分量:1280 * 720 * (1/4) = 230400 位元組
V 分量:1280 * 720 * (1/4) = 230400 位元組
總大小:Y 分量 + U 分量 + V 分量 = (1280 * 720 + 1280 * 720 * (1/4)* 2) / 1024 / 1024 = 1.32 MB
由上面計算可以看出 YUV420 采樣(格式 YV12/YU12 )的圖像比 RGB 模型圖像節省了 1/2 的存儲空間。
NV21/NV12 (YUV420 采樣方式)
NV21/NV12 屬于 YUV420SP ,YUV420SP 格式有 2 個平面,Y 分量存儲于一個平面,UV 分量交錯存儲于另一個平面。
NV21 格式的存儲方式
(0 ~ 3) Y00 Y01 Y02 Y03
(4 ~ 7) Y10 Y11 Y12 Y13
(8 ~ 11) Y20 Y21 Y22 Y23
(12 ~ 15) Y30 Y31 Y32 Y33
(16 ~ 19) V00 U00 V01 U01
(20 ~ 23) V10 U10 V11 U11
NV12 格式的存儲方式
(0 ~ 3) Y00 Y01 Y02 Y03
(4 ~ 7) Y10 Y11 Y12 Y13
(8 ~ 11) Y20 Y21 Y22 Y23
(12 ~ 15) Y30 Y31 Y32 Y33
(16 ~ 19) U00 V00 U01 V01
(20 ~ 23) U10 V10 U11 V11
NV21 與 NV12 格式的差別僅在于 UV 分量排列的先後順序不同。
一幅 720P (1280x720分辨率) 的圖檔,使用 YUV420 采樣時(格式 NV21/NV12 )占用存儲大小為:
Y 分量:1280 * 720 = 921600 位元組
UV 分量:1280 * 720 * (1/2) = 460800 位元組
總大小:Y 分量 + UV 分量 = (1280 * 720 + 1280 * 720 * (1/2)) / 1024 / 1024 = 1.32 MB
由上面計算可以看出 YUV420 采樣(格式 NV21/NV12 )的圖像比 RGB 模型圖像也節省了 1/2 的存儲空間。
YUV 圖像的基本操作
下面以最常用的 NV21 圖為例介紹其旋轉、縮放和剪切的基本方法。
YUV 圖檔的定義、加載、儲存及記憶體釋放。
//YUV420SP NV21 or NV12
typedef struct
{
int width; // 圖檔寬
int height; // 圖檔高
unsigned char *yPlane; // Y 平面指針
unsigned char *uvPlane; // UV 平面指針
} YUVImage;
void LoadYUVImage(const char *filePath, YUVImage *pImage)
{
FILE *fpData = fopen(filePath, "rb+");
if (fpData != NULL)
{
fseek(fpData, 0, SEEK_END);
int len = ftell(fpData);
pImage->yPlane = malloc(len);
fseek(fpData, 0, SEEK_SET);
fread(pImage->yPlane, 1, len, fpData);
fclose(fpData);
fpData = NULL;
}
pImage->uvPlane = pImage->yPlane + pImage->width * pImage->height;
}
void SaveYUVImage(const char *filePath, YUVImage *pImage)
{
FILE *fp = fopen(filePath, "wb+");
if (fp)
{
fwrite(pImage->yPlane, pImage->width * pImage->height, 1, fp);
fwrite(pImage->uvPlane, pImage->width * (pImage->height >> 1), 1, fp);
}
}
void ReleaseYUVImage(YUVImage *pImage)
{
if (pImage->yPlane)
{
free(pImage->yPlane);
pImage->yPlane = NULL;
pImage->uvPlane = NULL;
}
}
NV21 圖檔旋轉
以順時針旋轉 90 度為例,Y 和 UV 兩個平面分别從平面左下角進行縱向拷貝,需要注意的是每對 UV 分量作為一個整體進行拷貝。 以此類比,順時針旋轉 180 度時從平面右下角進行橫向拷貝,順時針旋轉 270 度時從平面右上角進行縱向拷貝。
Y00 Y01 Y02 Y03 Y30 Y20 Y10 Y00
Y10 Y11 Y12 Y13 旋轉90度 Y31 Y21 Y11 Y01
Y20 Y21 Y22 Y23 -----> Y32 Y22 Y12 Y02
Y30 Y31 Y32 Y33 Y33 Y23 Y13 Y03
V00 U00 V01 U01 -----> V10 U10 V00 U00
V10 U10 V11 U11 V11 U11 V01 U01
代碼實作:
//angle 90, 270, 180
void RotateYUVImage(YUVImage *pSrcImg, YUVImage *pDstImg, int angle)
{
int yIndex = 0;
int uvIndex = 0;
switch (angle)
{
case 90:
{
// y plane
for (int i = 0; i < pSrcImg->width; i++) {
for (int j = 0; j < pSrcImg->height; j++) {
*(pDstImg->yPlane + yIndex) = *(pSrcImg->yPlane + (pSrcImg->height - j - 1) * pSrcImg->width + i);
yIndex++;
}
}
//uv plane
for (int i = 0; i < pSrcImg->width; i += 2) {
for (int j = 0; j < pSrcImg->height / 2; j++) {
*(pDstImg->uvPlane + uvIndex) = *(pSrcImg->uvPlane + (pSrcImg->height / 2 - j - 1) * pSrcImg->width + i);
*(pDstImg->uvPlane + uvIndex + 1) = *(pSrcImg->uvPlane + (pSrcImg->height / 2 - j - 1) * pSrcImg->width + i + 1);
uvIndex += 2;
}
}
}
break;
case 180:
{
// y plane
for (int i = 0; i < pSrcImg->height; i++) {
for (int j = 0; j < pSrcImg->width; j++) {
*(pDstImg->yPlane + yIndex) = *(pSrcImg->yPlane + (pSrcImg->height - 1 - i) * pSrcImg->width + pSrcImg->width - 1 - j);
yIndex++;
}
}
//uv plane
for (int i = 0; i < pSrcImg->height / 2; i++) {
for (int j = 0; j < pSrcImg->width; j += 2) {
*(pDstImg->uvPlane + uvIndex) = *(pSrcImg->uvPlane + (pSrcImg->height / 2 - 1 - i) * pSrcImg->width + pSrcImg->width - 2 - j);
*(pDstImg->uvPlane + uvIndex + 1) = *(pSrcImg->uvPlane + (pSrcImg->height / 2 - 1 - i) * pSrcImg->width + pSrcImg->width - 1 - j);
uvIndex += 2;
}
}
}
break;
case 270:
{
// y plane
for (int i = 0; i < pSrcImg->width; i++) {
for (int j = 0; j < pSrcImg->height; j++) {
*(pDstImg->yPlane + yIndex) = *(pSrcImg->yPlane + j * pSrcImg->width + (pSrcImg->width - i - 1));
yIndex++;
}
}
//uv plane
for (int i = 0; i < pSrcImg->width; i += 2) {
for (int j = 0; j < pSrcImg->height / 2; j++) {
*(pDstImg->uvPlane + uvIndex + 1) = *(pSrcImg->uvPlane + j * pSrcImg->width + (pSrcImg->width - i - 1));
*(pDstImg->uvPlane + uvIndex) = *(pSrcImg->uvPlane + j * pSrcImg->width + (pSrcImg->width - i - 2));
uvIndex += 2;
}
}
}
break;
default:
break;
}
}
NV21 圖檔縮放
将 2x2 的 NV21 圖縮放成 4x4 的 NV21 圖,原圖橫向每個像素的 Y 分量向右拷貝 1(放大倍數-1)次,縱向每列元素以列為機關向下拷貝 1(放大倍數-1)次.
将 4x4 的 NV21 圖縮放成 2x2 的 NV21 圖,實際上就是進行采樣。
void ResizeYUVImage(YUVImage *pSrcImg, YUVImage *pDstImg)
{
if (pSrcImg->width > pDstImg->width)
{
//縮小
int x_scale = pSrcImg->width / pDstImg->width;
int y_scale = pSrcImg->height / pDstImg->height;
for (size_t i = 0; i < pDstImg->height; i++)
{
for (size_t j = 0; j < pDstImg->width; j++)
{
*(pDstImg->yPlane + i*pDstImg->width + j) = *(pSrcImg->yPlane + i * y_scale *pSrcImg->width + j * x_scale);
}
}
for (size_t i = 0; i < pDstImg->height / 2; i++)
{
for (size_t j = 0; j < pDstImg->width; j += 2)
{
*(pDstImg->uvPlane + i*pDstImg->width + j) = *(pSrcImg->uvPlane + i * y_scale *pSrcImg->width + j * x_scale);
*(pDstImg->uvPlane + i*pDstImg->width + j + 1) = *(pSrcImg->uvPlane + i * y_scale *pSrcImg->width + j * x_scale + 1);
}
}
}
else
{
// 放大
int x_scale = pDstImg->width / pSrcImg->width;
int y_scale = pDstImg->height / pSrcImg->height;
for (size_t i = 0; i < pSrcImg->height; i++)
{
for (size_t j = 0; j < pSrcImg->width; j++)
{
int yValue = *(pSrcImg->yPlane + i *pSrcImg->width + j);
for (size_t k = 0; k < x_scale; k++)
{
*(pDstImg->yPlane + i * y_scale * pDstImg->width + j * x_scale + k) = yValue;
}
}
unsigned char *pSrcRow = pDstImg->yPlane + i * y_scale * pDstImg->width;
unsigned char *pDstRow = NULL;
for (size_t l = 1; l < y_scale; l++)
{
pDstRow = (pDstImg->yPlane + (i * y_scale + l)* pDstImg->width);
memcpy(pDstRow, pSrcRow, pDstImg->width * sizeof(unsigned char ));
}
}
for (size_t i = 0; i < pSrcImg->height / 2; i++)
{
for (size_t j = 0; j < pSrcImg->width; j += 2)
{
int vValue = *(pSrcImg->uvPlane + i *pSrcImg->width + j);
int uValue = *(pSrcImg->uvPlane + i *pSrcImg->width + j + 1);
for (size_t k = 0; k < x_scale * 2; k += 2)
{
*(pDstImg->uvPlane + i * y_scale * pDstImg->width + j * x_scale + k) = vValue;
*(pDstImg->uvPlane + i * y_scale * pDstImg->width + j * x_scale + k + 1) = uValue;
}
}
unsigned char *pSrcRow = pDstImg->uvPlane + i * y_scale * pDstImg->width;
unsigned char *pDstRow = NULL;
for (size_t l = 1; l < y_scale; l++)
{
pDstRow = (pDstImg->uvPlane + (i * y_scale + l)* pDstImg->width);
memcpy(pDstRow, pSrcRow, pDstImg->width * sizeof(unsigned char ));
}
}
}
}
NV21 圖檔裁剪
圖例中将 6x6 的 NV21 圖按照橫縱坐标偏移量為(2,2)裁剪成 4x4 的 NV21 圖。
// x_offSet ,y_offSet % 2 == 0
void CropYUVImage(YUVImage *pSrcImg, int x_offSet, int y_offSet, YUVImage *pDstImg)
{
// 確定裁剪區域不存在記憶體越界
int cropWidth = pSrcImg->width - x_offSet;
cropWidth = cropWidth > pDstImg->width ? pDstImg->width : cropWidth;
int cropHeight = pSrcImg->height - y_offSet;
cropHeight = cropHeight > pDstImg->height ? pDstImg->height : cropHeight;
unsigned char *pSrcCursor = NULL;
unsigned char *pDstCursor = NULL;
//crop yPlane
for (size_t i = 0; i < cropHeight; i++)
{
pSrcCursor = pSrcImg->yPlane + (y_offSet + i) * pSrcImg->width + x_offSet;
pDstCursor = pDstImg->yPlane + i * pDstImg->width;
memcpy(pDstCursor, pSrcCursor, sizeof(unsigned char ) * cropWidth);
}
//crop uvPlane
for (size_t i = 0; i < cropHeight / 2; i++)
{
pSrcCursor = pSrcImg->uvPlane + (y_offSet / 2 + i) * pSrcImg->width + x_offSet;
pDstCursor = pDstImg->uvPlane + i * pDstImg->width;
memcpy(pDstCursor, pSrcCursor, sizeof(unsigned char ) * cropWidth);
}
}
Sample 測試
原圖
測試代碼
void main()
{
YUVImage srcImg = { 0 };
srcImg.width = 840;
srcImg.height = 1074;
LoadYUVImage("IMG_840x1074.NV21", &srcImg);
YUVImage rotateDstImg = { 0 };
rotateDstImg.width = 1074;
rotateDstImg.height = 840;
rotateDstImg.yPlane = malloc(rotateDstImg.width * rotateDstImg.height*1.5);
rotateDstImg.uvPlane = rotateDstImg.yPlane + rotateDstImg.width * rotateDstImg.height;
RotateYUVImage(&srcImg, &rotateDstImg, 270);
SaveYUVImage("D:\\material\\IMG_1074x840_270.NV21", &rotateDstImg);
RotateYUVImage(&srcImg, &rotateDstImg, 90);
SaveYUVImage("D:\\material\\IMG_1074x840_90.NV21", &rotateDstImg);
rotateDstImg.width = 840;
rotateDstImg.height = 1074;
RotateYUVImage(&srcImg, &rotateDstImg, 180);
SaveYUVImage("D:\\material\\IMG_840x1074_180.NV21", &rotateDstImg);
YUVImage resizeDstImg = { 0 };
resizeDstImg.width = 420;
resizeDstImg.height = 536;
resizeDstImg.yPlane = malloc(resizeDstImg.width * resizeDstImg.height*1.5);
resizeDstImg.uvPlane = resizeDstImg.yPlane + resizeDstImg.width * resizeDstImg.height;
ResizeYUVImage(&srcImg, &resizeDstImg);
SaveYUVImage("D:\\material\\IMG_420x536_Resize.NV21", &resizeDstImg);
YUVImage cropDstImg = { 0 };
cropDstImg.width = 300;
cropDstImg.height = 300;
cropDstImg.yPlane = malloc(cropDstImg.width * cropDstImg.height*1.5);
cropDstImg.uvPlane = cropDstImg.yPlane + cropDstImg.width * cropDstImg.height;
CropYUVImage(&srcImg, 100, 500, &cropDstImg);
SaveYUVImage("D:\\material\\IMG_300x300_crop.NV21", &cropDstImg);
ReleaseYUVImage(&srcImg);
ReleaseYUVImage(&rotateDstImg);
ReleaseYUVImage(&resizeDstImg);
ReleaseYUVImage(&cropDstImg);
}
測試結果
聯系與交流
技術交流可以添加我的微信:Byte-Flow
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。