https://www.cnblogs.com/xiaxveliang/archive/2020/03/02/12395861.htmlhtml
QQ視頻通話、抖音的視頻回顯 是如何實現的
先說爲何會有這一篇文章:
2014年聯想曾經作過一款 短視頻軟件,叫「魔力秀」。能夠說和如今的抖音基本是同樣的,但由於「魔力秀App」出生於聯想,註定沒法在一個硬件公司成長爲一棵參天大樹,最終只發了一個版本就結束了。
當時「魔力秀App」的視頻回顯模塊是我設計實現的,因此就有了這篇文章。
事過多年,將這篇文章拿出來整理,由於這項技術依然不過期,反而被普遍應用...
java
這篇文章以前叫作 Opengl ES中YUV420轉RGB 是一個技術標題。整理時,發現用這個標題,你們實際是不知道這個技術有什麼用,所以換了這個比較醒目的名字。網絡
Opengl ES中YUV420轉RGB 這項技術主要是實現視頻高效、節省帶寬的回顯視頻圖像。優化
- 爲何說高效?
由於直接用 OpenGL ES 實現,自己繞開了Androi的層層封裝;
並且Opengl 自己就是圖形學接口,實現效率自然高效。 - 爲何說節省帶寬?
由於網絡傳輸中,採用的YUV420數據格式,自己是一種有損的數據格式。但因爲格式的特性,色彩還原後基本對圖像顯示效果沒有影響,所以在視頻通話場景中普遍使用。
這裏經過如下幾個方面具體說明Opengl ES中YUV420轉RGB 這項技術的實現方式:spa
- 先了解一個概念「灰度圖」
- YUV數據格式
- YUV444和YUV420
- YUV420轉RGB
- OpenGL ES中YUV420P轉RGB
1、先了解一個概念「灰度圖」
這裏先了解一下灰度 Y 的概念。不知道你們是否看過老式的黑白電視機?
老式黑白電視機的圖像就只有Y一個通道,老式黑白電視機上的圖像就是灰度圖成像(只用接收一個Y通道數據就能播放出電視畫面,前輩們果真厲害... ;然後來的彩色電視用的是YUV數據信號,這樣既兼容了老的黑白電視,又能夠在新式彩色電視上顯示彩色圖像,前輩們太厲害了...)
設計
- 灰度圖的定義:
- 灰度值與RGB的計算公式
- 將「彩色圖轉」轉化爲「灰度圖」shader實現
1.一、灰度圖的定義:
把白色與黑色之間按對數關係分爲若干等級,稱爲灰度。灰度分爲256階。指針
1.二、灰度值Y與RGB的計算公式:
Y = 0.299R + 0.587G + 0.114*B
電視臺發出信號時,將RGB數據這樣轉化爲Y 數據。老式黑白電視機接收到Y信號,就能展現圖象了。code
1.四、將「彩色圖轉」轉化爲「灰度圖」shader實現
這裏說一個技術實現,在OpenGL ES中,如何用shader片元着色器,把一個彩色紋理圖轉化爲一個灰度圖?orm
效果以下:視頻
轉化固然要用到咱們上邊說道的RGB 轉 Y的公式,下邊咱們看具體的片源着色器 shader 代碼實現:
// shader 片元着色器 precision mediump float; varying vec2 vTextureCoord; uniform sampler2D sTexture; void main() { // 從紋理圖sTexture 讀取當前片元的RGB顏色 vec4 color=texture2D(sTexture, vTextureCoord); // 公式計算灰度值 float col=color.r*0.299+color.g*0.587+color.b*0.114; // 將生成的Y 灰度值設置給RGB通道 color.r=col; color.g=col; color.b=col; // 傳給片源着色器 gl_FragColor =color; }
在shader實現中,我特地加了註釋。
瞭解glsl語法的同窗,能夠仔細讀一下上邊的代碼實現;
固然不瞭解語法的同窗,更要簡單讀一遍(glsl是一種類C語言,只要學過C語言應該就能讀懂)
2、YUV數據格式
上邊咱們瞭解了灰度圖的實現,這裏咱們介紹一個YUV數據格式。
主要分爲如下幾個部分:
- YUV定義
- 使用YUV的好處
- YUV與RGB轉換公式
- YUV444和YUV420
2.一、YUV
YUV的具體定義以下:
Y:就是灰度值; UV:用來指定像素的顏色。
對於UV如今有些懵不要緊,咱們繼續往下看
2.二、YUV與RGB轉換公式
// RGB轉YUV Y= 0.299*R + 0.587*G + 0.114*B U= -0.147*R - 0.289*G + 0.436*B = 0.492*(B- Y) V= 0.615*R - 0.515*G - 0.100*B = 0.877*(R- Y) //############################################ // YUV轉RGB R = Y + 1.140*V G = Y - 0.394*U - 0.581*V B = Y + 2.032*U
2.三、使用YUV的好處:
- 傳輸信號向後兼容老式黑白電視機(用於優化彩色視頻信號的傳輸,使其向後相容老式黑白電視)
- YUV420佔用的帶寬少(這個咱們在前邊提過,至於具體爲何,後邊會有詳細介紹)
YUV420與RGB視頻信號傳輸相比,它最大的優勢在於只需佔用極少的頻寬(後面來介紹)
2.四、YUV444和YUV420
前邊咱們一直說的YUV數據,實際上是YUV420數據格式。YUV420數據格式在傳輸上UV色彩是有損傳輸,而YUV444 實際上是一種無損的數據格式。
那爲何這裏咱們仍是要說一下YUV444格式呢?
實際上是爲了後邊實現 將YUV420數據還原成RGB作準備
首先介紹YUV444 數據格式:
- YUV444:
一個像素點對應一個Y一個U一個V(YUV一一對應)
YUV444數據格式 以下圖所示:
YUV444 中YUV通道一一對應,理解簡單。下邊這個是YUV420數據格式,UV數據有損。
- YUV420:
一個像素點對應一個Y;
四個像素點對應一個U一個V;
具體數據格式以下:
從上圖能夠看到,UV色彩通道是有損失的,這也是爲何YUV420在展現時,佔用的帶寬更少一下。
a、Y、U、V沒有一一對應,圖像有顏色損失
b、這也就是爲何佔用的帶寬少了;
c、一樣網絡傳輸中,佔用的流量也一樣減小了;
d、但對圖像的色彩展現幾乎沒有影響
由於佔用的流量較少,對色彩展現幾乎沒有影響,所以普遍應用於各中視頻通話場景,視頻回顯場景等。
## 3、YUV420轉RGB
哇去,基礎知識終於說完了,這裏說到咱們的核心技術點:YUV420轉RGB
- 第一個步驟YUV420轉YUV444;
- 第二個步驟YUV444轉RGB。
爲何要把YUV420轉爲YUV444?
由於在傳輸時,YUV420中的UV通道數據損失了。但咱們渲染時,須要把這損失掉的UV色彩數據通道還原回來,再進行YUV444 轉 RGB
先說 YUV420 轉 YUV444
3.一、YUV420轉YUV444
要把YUV420轉爲YUV444就得把「上圖 YUV420」 U與V中 「?」 的部分填滿。
經過YUV420數據中,已有的U 與 Y數據,經過差值計算的方式,填補上空缺的部分。如下是差值運算的具體實現公式,差值計算以下(建議參照YUV420數據格式圖來看,要不容易懵):
U01 = (U00 + U02)/2; // 利用已有的 U00、U02來計算U01 U10 = (U00 + U20)/2; // 利用已有的 U00、U20來計算U10 U11 = (U00 + U02 + U20 + U22)/4;// 利用已有的 U00、U0二、U20、U22來計算U11 //###################### V01 = (V00 + V02)/2; // 利用已有的 V00、V02來計算V01 V10 = (V00 + V20)/2; // 利用已有的 V00、V20來計算V10 V11 = (V00 + V02 + V20 + V22)/4; // 利用已有的 V00、V0二、V20、V22來計算V11
通過以上公式,YUV420轉YUV444 完成(數據補全成功),下邊來講YUV444如何轉RGB。
3.二、YUV444轉RGB
YUV444轉RGB是有現成公式的,咱們直接拿來用就好了,YUV轉RGB的公式:
R = Y + 1.140*V G = Y - 0.394*U - 0.581*V B = Y + 2.032*U
公式有了,那具體的代碼實現是怎麼實現的呢?
注:
1、2、3、四,這四點介紹的是YUV轉RGB的基本原理,下邊是具體實現。
4、OpenGL ES中YUV420P轉RGB
這一節介紹具體技術實現,但開始時,仍是要介紹兩種數據格式(哎、我知道大家都煩了,我其實也煩,但仍是得說)
- YUV420p的數據格式
- YUV420sp的數據格式(YUV420sp轉RGB這裏不作介紹)
- YUV420sp 轉RGB
4.一、YUV420p的數據格式
YUV420p的數據格式以下圖所示(爲一個byte[]):
其中數據的4/6爲Y;1/6爲U;1/6爲V。
4.二、YUV420sp的數據格式(YUV420sp轉RGB這裏不作介紹)
YUV420sp的數據格式以下圖所示(爲一個byte[]):
其中數據的4/6爲Y;1/6爲U;1/6爲V。
4.三、YUV420sp 轉RGB
其實大概原理就是:
- 將YUV420數據中的Y U V 數據分別取出來,分別生成三張紋理圖
- 利用片元着色器每一個片元執行一次的特性,將YUV420數據轉爲YUV444數據
- 從YUV444數據中,取出一一對應的YUV數據
- 最後,利用公式YUV444 轉 RGB
- 完事大吉
如下爲YUV三張紋理圖效果圖:
YUV420轉YUV444
這裏如何補全YUV420數據中UV部分的顏色數據?
這裏有一個討巧的方式:
在OpenGL ES生成紋理時,採用線性紋理採樣方式。線性採樣出U、V紋理中「?」部分的顏色值。這樣就就能夠拿到一一對應的YUV444數據。
對應的Java代碼以下:
/** * * @param w * @param h * @param date * 數據 * @param textureY * @param textureU * @param textureV * @param isUpdate * 是否爲更新 */ public static boolean bindYUV420pTexture(int frameWidth, int frameHeight, byte frameData[], int textureY, int textureU, int textureV, boolean isUpdate) { if (frameData == null || frameData.length == 0) { return false; } Log.d(TAG, "----bindYUV420pTexture-----"); if (isUpdate == false) { /** * 數據緩衝區 */ // Y ByteBuffer buffer = LeBuffer.byteToBuffer(frameData); // GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureY); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); /** * target 指定目標紋理,這個值必須是GL_TEXTURE_2D; level * 執行細節級別,0是最基本的圖像級別,n表示第N級貼圖細化級別; internalformat * 指定紋理中的顏色組件,可選的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, * GL_LUMINANCE_ALPHA 等幾種; width 指定紋理圖像的寬度; height 指定紋理圖像的高度; border * 指定邊框的寬度; format 像素數據的顏色格式,可選的值參考internalformat; type * 指定像素數據的數據類型,可使用的值有GL_UNSIGNED_BYTE * ,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4 * ,GL_UNSIGNED_SHORT_5_5_5_1; pixels 指定內存中指向圖像數據的指針; * */ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, frameWidth, frameHeight, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, buffer); /** * */ // U buffer.clear(); buffer = LeBuffer.byteToBuffer(frameData); buffer.position(frameWidth * frameHeight); // // GLES20.glActiveTexture(GLES20.GL_TEXTURE1); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureU); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, frameWidth / 2, frameHeight / 2, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, buffer); /** * */ // V buffer.clear(); buffer = LeBuffer.byteToBuffer(frameData); buffer.position(frameWidth * frameHeight * 5 / 4); // // GLES20.glActiveTexture(GLES20.GL_TEXTURE2); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureV); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, frameWidth / 2, frameHeight / 2, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, buffer); } else { /** * Y */ ByteBuffer buffer = LeBuffer.byteToBuffer(frameData); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureY); GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, frameWidth, frameHeight, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, buffer); /** * U */ // buffer.clear(); buffer = LeBuffer.byteToBuffer(frameData); buffer.position(frameWidth * frameHeight); // GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureU); GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, buffer); /** * V */ // buffer.clear(); buffer = LeBuffer.byteToBuffer(frameData); buffer.position(frameWidth * frameHeight * 5 / 4); // GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureV); GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, buffer); } return true; }
代碼說明:
已上代碼即是將傳入的幀數據byte frameData[],轉爲三張紋理圖的代碼。
代碼的16行、5051行、7576行分別爲從byte frameData[]中分別取出Y、U、V數據的代碼。
代碼5659行、代碼8184行分別爲設置U、V紋理的採樣方式爲線性採樣的代碼。
以上代碼運行結束,內存中會生成三張紋理圖像。
將三張紋理圖像傳入「片元着色器」執行下一步驟。
YUV444轉RGB
YUV一一對應的紋理有了,這裏該介紹如何實現YUV444轉RGB了:
按照YUV轉RGB的公式,將Y、U、V一一對應的取出,進行YUV轉RGB操做,生成像素點。
對應片元着色器 shader 代碼實現:
recision mediump float; // 片元着色器中 輸入了Y U V三張紋理 uniform sampler2D sTexture_y; uniform sampler2D sTexture_u; uniform sampler2D sTexture_v; varying vec2 vTextureCoord; //YUV 轉 RGB的 shader 實現 void getRgbByYuv(in float y, in float u, in float v, inout float r, inout float g, inout float b){ // y = 1.164*(y - 0.0625); u = u - 0.5; v = v - 0.5; // r = y + 1.596023559570*v; g = y - 0.3917694091796875*u - 0.8129730224609375*v; b = y + 2.017227172851563*u; } void main() { // float r,g,b; // 從YUV三張紋理中,採樣出一一對應的YUV數據 float y = texture2D(sTexture_y, vTextureCoord).r; float u = texture2D(sTexture_u, vTextureCoord).r; float v = texture2D(sTexture_v, vTextureCoord).r; // YUV 轉 RGB getRgbByYuv(y, u, v, r, g, b); // 最終顏色賦值 gl_FragColor = vec4(r,g,b, 1.0); }
5、完事大吉
源碼真的是懶得整理,因此,你們仍是理解了實現原理,本身動手去敲吧,不要找我要代碼了!!!
源碼真的是懶得整理,因此,你們仍是理解了實現原理,本身動手去敲吧,不要找我要代碼了!!!
源碼真的是懶得整理,因此,你們仍是理解了實現原理,本身動手去敲吧,不要找我要代碼了!!!