該原創文章首發於微信公衆號:字節流動git
最近,有位讀者大人在後臺反饋:在參加一場面試的時候,面試官要求他用 shader 實現圖像格式 RGB 轉 YUV ,他聽了以後一臉懵,而後悻悻地對面試官說,他只用 shader 作過 YUV 轉 RGB,不知道 RGB 轉 YUV 是個什麼思路。github
針對他的這個疑惑,今天專門寫文章介紹一下如何使用 OpenGL 實現 RGB 到 YUV 的圖像格式轉換,幫助讀者大人化解此類問題。面試
有讀者大人讓推薦一個 YUV 看圖軟件,因爲手頭的工具無法分享出來,又在 Github 上找了一圈發現這一類開源軟件用起來都很多 BUG 。微信
最後發現一款免費的商業軟件 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 實現,在前面的文章中曾經介紹過 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 渲染步驟:
片斷着色器腳本:
#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 。
來到本文的重點,那麼如何利用 shader 實現 RGB 轉 YUV 呢?
前面小節已經提到,先說下一個簡單的思路:先將 RGBA 按照公式轉換爲 YUV 如(YUYV),而後將 YUYV 按照 RGBA 進行排布,最後使用 glReadPixels 讀取 YUYV 數據,因爲 YUYV 數據量爲 RGBA 的一半,須要注意輸出 buffer 的大小,以及 viewport 的寬度(寬度爲原來的一半)。
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 原理圖:
咱們要將 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