Implement yuv2rgb gpu filter

0x1 Introduction

本文我们将在GPU上实现yuv420p到rgb的转换,转换代码采用opengl es glsl实现。该转换代码在GPU上运行,为了更好地展示这个实现过程和体现转换效果,我们在Android平台上实现一个app应用。
详细代码请参考 YUVRender

YUV2RGB转换的算法原理如下,其中Y/U/V是从yuv420p数据中读取的三个分量。
B=1.164(Y−16)+2.018(U−128)
G=1.164(Y−16)−0.813(V−128)−0.391(U−128)
R=1.164(Y−16)+1.596(V−128)

0x2 Shader代码

0x21 vertex shader代码

vertex shader代码首先输入顶点坐标和纹理坐标。
输出变量gl_Position用于在计算顶点位置以后将其写入裁剪坐标。
输出变量vtexcoord是输入给fragment shader作为纹理坐标的。

1
2
3
4
5
6
7
8
9
precision mediump float;
varying mediump vec2 vtexcoord;
attribute mediump vec4 position;
attribute mediump vec2 texcoord;
void main()
{
gl_Position = position;
vtexcoord = texcoord;
}

0x22 fragment shader代码

fragment shader代码首先输入texture坐标, 该坐标首先经过了vertex shader处理,
然后又经过了Primitive Assembly阶段的clip和插值处理。
然后输入三个sampler2D,这三个sampler2D分别对应yuv420p的Y/U/V数据,通过texture2D这个内置的纹理访问函数,我们可以读取对应纹理坐标下的Y/U/V数据。
接下面的代码就是执行YUV到RGB的转换矩阵了,具体的矩阵可以参考前面的介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
precision mediump float;
varying mediump vec2 vtexcoord;
uniform lowp sampler2D samplerY;
uniform lowp sampler2D samplerU;
uniform lowp sampler2D samplerV;
void main()
{
mediump float y;
mediump float u;
mediump float v;
lowp vec3 rgb;
mat3 convmatrix = mat3(vec3(1.164, 1.164, 1.164),
vec3(0.0, -0.392, 2.017),
vec3(1.596, -0.813, 0.0));
y = (texture2D(samplerY, vtexcoord).r - (16.0 / 255.0));
u = (texture2D(samplerU, vtexcoord).r - (128.0 / 255.0));
v = (texture2D(samplerV, vtexcoord).r - (128.0 / 255.0));
rgb = convmatrix * vec3(y, u, v);
gl_FragColor = vec4(rgb, 1.0);
}

0x3 具体实现

0x31 texture的创建

为了让GPU能读取到YUV的数据,需要创建3个texture, 这三个texture分别存放Y/U/V分量。

1
2
3
4
5
6
7
8
9
10
11
GLES20.glGenTextures(3, mTexture, 0);
for(int i = 0; i < 3; i++)
{
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexture[i]);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_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);
}

0x32 yuv420p数据的读取

我们读取的yuv420p数据是foreman.qcif,分辨率是qcif(176x144), 从数据布局来看,首先是176x144大小的Y分量,然后是88x72大小的U分量, 最后是88x72大小的V分量。下面是读取YUV数据的详细代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void getYuvData(String fileName)
{
String res="";
try{
InputStream in = getResources().getAssets().open(fileName);
int length = in.available();
byte [] buffer = new byte[length];
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(length);
mYuvBuffer = byteBuffer;
in.read(buffer);
in.close();
mYuvBuffer.put(buffer);
mYuvBuffer.position(0);
mBufferU = ByteBuffer.allocateDirect(88*72);
mBufferV = ByteBuffer.allocateDirect(88*72);
mBufferU.put(buffer,176*144,88*72);
mBufferU.position(0);
mBufferV.put(buffer,176*144+88*72,88*72);
mBufferV.position(0);
}catch(Exception e){
e.printStackTrace();
}
}

0x33 texture的更新

YUV数据保持在pixels数组中,通过调用glTexImage2D把YUV数据输入到GPU driver中。
因为需要给YUV的三个texture分别输入,所以需要循环3次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int[] planes = { 0, 1, 2 };
int[] widths = { width, width/2, width/2 };
int[] heights = { height, height/2, height/2 };
for (int i = 0; i < 3; ++i)
{
int plane = planes[i];
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexture[i]);
ShaderManager.checkGlError("glBindTexture");
if(pixels[plane] == null)
Log.e("YUV2RGBFilter", "pixels[plane] == null");
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D,
0,
GLES20.GL_LUMINANCE,
widths[plane],
heights[plane],
0,
GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE,
pixels[plane]);
ShaderManager.checkGlError("glTexImage2D");
GLES20.glUniform1i(mTextureHandle[i], i);
ShaderManager.checkGlError("glUniform1i");
}

0x34 更新其他参数

下面的代码是把顶点坐标和纹理坐标输入给GPU driver。

1
2
3
4
5
6
7
8
9
10
11
12
13
GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT,
false, 0, mVertexBuffer);
ShaderManager.checkGlError("glVertexAttribPointer");
GLES20.glEnableVertexAttribArray(mPositionHandle);
ShaderManager.checkGlError("glEnableVertexAttribArray");
GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT,
false, 0, mTexCoorBuffer);
ShaderManager.checkGlError("glVertexAttribPointer");
GLES20.glEnableVertexAttribArray(mTexCoordHandle);

0x4 调试中碰到的问题

代码写好以后,一运行,不能出来正确的图像,一直是绿屏。下面介绍一下解决绿屏的过程。
首先检查api调用是否正确,通过启用swiftshader,我们可以打印出所有的opengl es api调用log, 检查该app调用的opengl es api,没有发现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
01-01 04:50:52.439 8547 8615 E libGLESv2_swiftshader: ClearColor external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:710 ((GLclampf red = 1.000000, GLclampf green = 0.000000, GLclampf blue = 1.000000, GLclampf alpha = 1.000000))
01-01 04:50:52.439 8547 8615 E libGLESv2_swiftshader: Clear external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:692 ((GLbitfield mask = 4100))
01-01 04:50:52.451 8547 8615 E libGLESv2_swiftshader: UseProgram external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:5925 ((GLuint program = 3))
01-01 04:50:52.451 8547 8615 E libGLESv2_swiftshader: BindTexture external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:354 ((GLenum target = 0xDE1, GLuint texture = 1))
01-01 04:50:52.452 8547 8615 E libGLESv2_swiftshader: Uniform1iv external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:5545 ((GLint location = 0, GLsizei count = 1, const GLint* v = 0x7b00d3a1621c))
01-01 04:50:52.452 8547 8615 E libGLESv2_swiftshader: BindTexture external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:354 ((GLenum target = 0xDE1, GLuint texture = 2))
01-01 04:50:52.452 8547 8615 E libGLESv2_swiftshader: Uniform1iv external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:5545 ((GLint location = 1, GLsizei count = 1, const GLint* v = 0x7b00d3a1621c))
01-01 04:50:52.452 8547 8615 E libGLESv2_swiftshader: BindTexture external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:354 ((GLenum target = 0xDE1, GLuint texture = 3))
01-01 04:50:52.452 8547 8615 E libGLESv2_swiftshader: Uniform1iv external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:5545 ((GLint location = 2, GLsizei count = 1, const GLint* v = 0x7b00d3a1621c))
01-01 04:50:52.452 8547 8615 E libGLESv2_swiftshader: VertexAttribPointer external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:6127 ((GLuint index = 0, GLint size = 3, GLenum type = 0x1406, GLboolean normalized = 0, GLsizei stride = 0, const GLvoid* ptr = 0x740e1d10))
01-01 04:50:52.452 3300 3300 E libGLESv2_swiftshader: BindTexture external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:354 ((GLenum target = 0x8D65, GLuint texture = 4))
01-01 04:50:52.452 3300 3300 E libGLESv2_swiftshader: EGLImageTargetTexture2DOES external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:6659 ((GLenum target = 0x8D65, GLeglImageOES image = 0x8))
01-01 04:50:52.452 8547 8615 E libGLESv2_swiftshader: EnableVertexAttribArray external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:1865 ((GLuint index = 0))
01-01 04:50:52.452 8547 8615 E libGLESv2_swiftshader: VertexAttribPointer external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:6127 ((GLuint index = 1, GLint size = 2, GLenum type = 0x1406, GLboolean normalized = 0, GLsizei stride = 0, const GLvoid* ptr = 0x740e0b80))
01-01 04:50:52.453 8547 8615 E libGLESv2_swiftshader: EnableVertexAttribArray external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:1865 ((GLuint index = 1))
01-01 04:50:52.453 8547 8615 E libGLESv2_swiftshader: DrawArrays external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:1537 ((GLenum mode = 0x5, GLint first = 0, GLsizei count = 4))
01-01 04:50:52.453 3300 3300 E libGLESv2_swiftshader: Disable external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:1493 ((GLenum cap = 0xC11))
01-01 04:50:52.454 3300 3300 E libGLESv2_swiftshader: Viewport external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:6189 ((GLint x = 0, GLint y = 0, GLsizei width = 1080, GLsizei height = 1920))
01-01 04:50:52.468 8547 8615 E libGLESv2_swiftshader: Finish external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:1922 (())
01-01 04:50:52.728 8547 8615 E libGLESv2_swiftshader: DisableVertexAttribArray external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:1520 ((GLuint index = 0))
01-01 04:50:52.729 8547 8615 E libGLESv2_swiftshader: DisableVertexAttribArray external/swiftshader/src/OpenGL/libGLESv2/libGLESv2.cpp:1520 ((GLuint index = 1))

再怀疑是因为texture没有被正确地送入到GPU中,通过dump glTexImage2D()输入的pixel,发现也是正确的。

最后在fragment shader中写hard code, 强制输出某种颜色,也是正确的,说明整个流程没有问题。

最后怀疑是texture的坐标没有正确地设置,在打印glVertexAttribPointer()的输入参数,发现这个时候的texture坐标都是0,明显不是正确的值,一步一步往上debug, 发现是因为Java ByteBuffer的order没有被设置成本机字节顺序(ByteOrder.nativeOrder()), 设置了以后,texture的坐标就正确了。

解决了绿屏的问题以后,yuv420p的数据能在Android上正确显示了,显示效果如下
result