面試官:請使用 OpenGL ES 將 RGB 圖像轉換爲 YUV 格式。我 ……

該原創文章首發於微信公衆號:字節流動git

最近,有位讀者大人在後臺反饋:在參加一場面試的時候,面試官要求他用 shader 實現圖像格式 RGB 轉 YUV ,他聽了以後一臉懵,而後悻悻地對面試官說,他只用 shader 作過 YUV 轉 RGB,不知道 RGB 轉 YUV 是個什麼思路。github

針對他的這個疑惑,今天專門寫文章介紹一下如何使用 OpenGL 實現 RGB 到 YUV 的圖像格式轉換,幫助讀者大人化解此類問題。面試

YUV 看圖工具推薦

有讀者大人讓推薦一個 YUV 看圖軟件,因爲手頭的工具無法分享出來,又在 Github 上找了一圈發現這一類開源軟件用起來都很多 BUG 。微信

YUV 看圖工具

最後發現一款免費的商業軟件 YUV Viewer ,用起來還行。markdown

https://www.elecard.com/products/video-analysis/yuv-viewer
複製代碼

就是下載起來比較慢,我這裏給讀者大人已經下載好了,【字節流動】 後臺回覆關鍵字 yuvViewer 便可獲取。ide

好處

使用 shader 實現 RGB 到 YUV 的圖像格式轉換有什麼使用場景呢?在生產環境中使用極爲廣泛。工具

前文曾經介紹過 Android OpenGL 渲染圖像的讀取方式,分別是 glReadPixels、 PBO、 ImageReader 以及 HardwareBuffer 。oop

glReadPixels 你們常常用來讀取 RGBA 格式的圖像,那麼我用它來讀取 YUV 格式的圖像行不行呢?答案是確定的,這就要用到 shader 來實現 RGB 到 YUV 的圖像格式轉換。性能

glReadPixels 性能瓶頸通常出如今大分辨率圖像的讀取,在生產環境中通用的優化方法是在 shader 中將處理完成的 RGBA 轉成 YUV (通常是 YUYV),而後基於 RGBA 的格式讀出 YUV 圖像,這樣傳輸數據量會下降一半,性能提高明顯,不用考慮兼容性問題。優化

YUV 轉 RGB

這一節先作個鋪墊簡單介紹下 YUV 轉 RGB 實現,在前面的文章中曾經介紹過 OpenGL 實現 YUV 的渲染,實際上就是利用 shader 實現了 YUV(NV21) 到 RGBA 的轉換,而後渲染到屏幕上。

以渲染 NV21 格式的圖像爲例,下面是 (4x4) NV21 圖像的 YUV 排布:

(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
複製代碼

YUV 渲染步驟:

  • 生成 2 個紋理,編譯連接着色器程序;
  • 肯定紋理座標及對應的頂點座標;
  • 分別加載 NV21 的兩個 Plane 數據到 2 個紋理,加載紋理座標和頂點座標數據到着色器程序;
  • 繪製。

片斷着色器腳本:

#version 300 es 
precision mediump float;                            
in vec2 v_texCoord;                                 
layout(location = 0) out vec4 outColor;             
uniform sampler2D y_texture;                        
uniform sampler2D uv_texture;                        
void main() {                                                   
vec3 yuv;										
yuv.x = texture(y_texture, v_texCoord).r;  	
yuv.y = texture(uv_texture, v_texCoord).a-0.5;	
yuv.z = texture(uv_texture, v_texCoord).r-0.5;	
vec3 rgb =mat3( 1.0,       1.0,       	1.0,					
                0.0, 		-0.344, 	1.770,					
                1.403,  -0.714,       0.0) * yuv; 			
outColor = vec4(rgb, 1);						
}

複製代碼

y_texture 和 uv_texture 分別是 NV21 Y Plane 和 UV Plane 紋理的採樣器,對兩個紋理採樣以後組成一個(y,u,v)三維向量,以後左乘變換矩陣轉換爲(r,g,b)三維向量。

上面 YUV 轉 RGB shader 中,面試官喜歡問的問題(一臉壞笑):爲何 UV 份量要減去 0.5 啊?

(迷之自信)答曰:由於歸一化。YUV 格式圖像 UV 份量的默認值分別是 127 ,Y 份量默認值是 0 ,8 個 bit 位的取值範圍是 0 ~ 255,因爲在 shader 中紋理採樣值須要進行歸一化,因此 UV 份量的採樣值須要分別減去 0.5 ,確保 YUV 到 RGB 正確轉換。

**須要注意的是 OpenGL ES 實現 YUV 渲染須要用到 GL_LUMINANCE 和 GL_LUMINANCE_ALPHA 格式的紋理,其中 GL_LUMINANCE 紋理用來加載 NV21 Y Plane 的數據,GL_LUMINANCE_ALPHA 紋理用來加載 UV Plane 的數據,**這一點很重要,初學的讀者大人請好好捋一捋。

關於 shader 實現 YUV 轉 RGB (NV2一、NV十二、I420 格式圖像渲染)能夠參考文章: OpenGL ES 3.0 開發(三):YUV 渲染FFmpeg 播放器視頻渲染優化,本文主要重點講 shader 如何實現 RGB 轉 YUV 。

RGB 轉 YUV

來到本文的重點,那麼如何利用 shader 實現 RGB 轉 YUV 呢?

前面小節已經提到,先說下一個簡單的思路:先將 RGBA 按照公式轉換爲 YUV 如(YUYV),而後將 YUYV 按照 RGBA 進行排布,最後使用 glReadPixels 讀取 YUYV 數據,因爲 YUYV 數據量爲 RGBA 的一半,須要注意輸出 buffer 的大小,以及 viewport 的寬度(寬度爲原來的一半)。

RGB to YUV 的轉換公式:

RGB to YUV 的轉換公式

開門見山,先貼實現 RGBA 轉 YUV 的 shader 腳本:

#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;//RGBA 紋理
uniform float u_Offset;//採樣偏移

//RGB to YUV
//Y = 0.299R + 0.587G + 0.114B
//U = -0.147R - 0.289G + 0.436B
//V = 0.615R - 0.515G - 0.100B

const vec3 COEF_Y = vec3( 0.299,  0.587,  0.114);
const vec3 COEF_U = vec3(-0.147, -0.289,  0.436);
const vec3 COEF_V = vec3( 0.615, -0.515, -0.100);

void main() {
    vec2 texelOffset = vec2(u_Offset, 0.0);
    vec4 color0 = texture(s_TextureMap, v_texCoord);
    //偏移 offset 採樣
    vec4 color1 = texture(s_TextureMap, v_texCoord + texelOffset);

    float y0 = dot(color0.rgb, COEF_Y);
    float u0 = dot(color0.rgb, COEF_U) + 0.5;
    float v0 = dot(color0.rgb, COEF_V) + 0.5;
    float y1 = dot(color1.rgb, COEF_Y);

    outColor = vec4(y0, u0, y1, v0);
}

複製代碼

shader 實現 RGB 轉 YUV 原理圖:

shader 實現 RGB 轉 YUV 原理圖

咱們要將 RGBA 轉成 YUYV,數據量相比於 RGBA 少了一半,這就至關於將兩個像素點合併成一個像素點。

如圖所示,咱們在 shader 中執行兩次採樣,RGBA 像素(R0,G0,B0,A0)轉換爲(Y0,U0,V0),像素(R1,G1,B1,A1)轉換爲(Y1),而後組合成(Y0,U0,Y1,V0),這樣 8 個字節表示的 2 個 RGBA 像素就轉換爲 4 個字節表示的 2 個 YUYV 像素。

轉換成 YUYV 時數據量減半,那麼 glViewPort 時 width 變爲原來的一半,一樣 glReadPixels 時 width 也變爲原來的一半。

實現 RGBA 轉成 YUYV 要保證原圖分辨率不變,建議使用 FBO 離屏渲染 ,這裏注意綁定給 FBO 的紋理是用來容納 YUYV 數據,其寬度應該設置爲原圖的一半。

bool RGB2YUVSample::CreateFrameBufferObj() {
	// 建立並初始化 FBO 紋理
	glGenTextures(1, &m_FboTextureId);
	glBindTexture(GL_TEXTURE_2D, m_FboTextureId);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glBindTexture(GL_TEXTURE_2D, GL_NONE);

	// 建立並初始化 FBO
	glGenFramebuffers(1, &m_FboId);
	glBindFramebuffer(GL_FRAMEBUFFER, m_FboId);
	glBindTexture(GL_TEXTURE_2D, m_FboTextureId);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_FboTextureId, 0);
	//FBO 紋理是用來容納 YUYV 數據,其寬度應該設置爲原圖的一半
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width / 2, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
	if (glCheckFramebufferStatus(GL_FRAMEBUFFER)!= GL_FRAMEBUFFER_COMPLETE) {
		LOGCATE("RGB2YUVSample::CreateFrameBufferObj glCheckFramebufferStatus status != GL_FRAMEBUFFER_COMPLETE");
		return false;
	}
	glBindTexture(GL_TEXTURE_2D, GL_NONE);
	glBindFramebuffer(GL_FRAMEBUFFER, GL_NONE);
	return true;

}
複製代碼

離屏渲染和讀取 YUYV 數據:

glBindFramebuffer(GL_FRAMEBUFFER, m_FboId);

// 渲染成 yuyv 寬度像素減半,glviewport 寬度減半
glViewport(0, 0, m_RenderImage.width / 2, m_RenderImage.height);

glUseProgram(m_FboProgramObj);
glBindVertexArray(m_VaoIds[1]);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_ImageTextureId);
glUniform1i(m_FboSamplerLoc, 0);

//參考原理圖,偏移量應該設置爲 1/(width / 2) * 1/2 = 1 / width; 理論上紋素的一半
float texelOffset = (float) (1.f / (float) m_RenderImage.width);
GLUtils::setFloat(m_FboProgramObj, "u_Offset", texelOffset);

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);

//YUYV buffer = width * height * 2; 轉換成 YUYV 時數據量減半,注意 buffer
uint8_t *pBuffer = new uint8_t[m_RenderImage.width * m_RenderImage.height * 2];

NativeImage nativeImage = m_RenderImage;
nativeImage.format = IMAGE_FORMAT_YUYV;
nativeImage.ppPlane[0] = pBuffer;

//glReadPixels 時 width 變爲原來的一半
glReadPixels(0, 0, m_RenderImage.width / 2, nativeImage.height, GL_RGBA, GL_UNSIGNED_BYTE, pBuffer);

DumpNativeImage(&nativeImage, "/sdcard/DCIM");
delete []pBuffer;

glBindFramebuffer(GL_FRAMEBUFFER, 0);
複製代碼

完整代碼參考下面項目,選擇 OpenGL RGB to YUV demo:

https://github.com/githubhaohao/NDK_OpenGLES_3_0
複製代碼

那麼面試官的問題又來了(一臉壞笑): RGBA 轉 YUV 的 shader 中 uv 份量爲何要加 0.5 ? 請讀者大人結合上面文章給予有力回擊。

技術交流

技術交流/獲取源碼能夠添加個人微信:Byte-Flow

相關文章
相關標籤/搜索