OpenGL实现相机视频NV21格式转RGB格式

发表于2017-09-20
评论0 7k浏览

最近公司项目做人脸识别追踪,从相机出来的视频格式是YUV420或者说是NV21格式,在面板上显示出来的使用的是RGB格式,所以要将其转化成该格式,下面先了解一下YUV420格式,网上很多这方面的介绍,理解起来很麻烦,本篇文章通过使用简介的语言希望能够帮助读者更好的理解:

视频数据也是一张张图片组成的,每张图片的大小是由图片的(width * height)*3/2字节组成。图片分两部分:Y通道的长度是width * height。UV平面字节长度是:(width / 2) x (height / 2) x 2 = width x height / 2 。每两个连续的字节是2 x 2 = 4个原始像素的V,U(按照NV21规范的顺序)色度字节。换句话说,UV平面尺寸为(宽/ 2)×(高/ 2)像素,并且在每个维度中被下采样因子2, 此外,U,V色度字节是交错的。


下面给大家展示一副关于YUV-NV12, NV21存储的图片:


接下来介绍如何将其转化成RGB

如问题所述,如果在Android代码中完成,此转换将需要太多时间才能生效。 幸运的是,它可以在GPU上运行的GL着色器中完成。 这将允许它运行非常快。

一般的想法是将我们的图像的纹理作为纹理传递给着色器,并以RGB转换的方式渲染它们。 为此,我们必须首先将图像中的通道复制到可传递给纹理的缓冲区中:

  1. byte[] image;  
  2. ByteBuffer yBuffer, uvBuffer;  
  3.   
  4. ...  
  5.   
  6. yBuffer.put(image, 0, width*height);  
  7. yBuffer.position(0);  
  8.   
  9. uvBuffer.put(image, width*height, width*height/2);  
  10. uvBuffer.position(0);  
然后,我们将这些缓冲区传递给实际的GL纹理:
  1. /* 
  2.  * Prepare the Y channel texture 
  3.  */  
  4.   
  5. //Set texture slot 0 as active and bind our texture object to it  
  6. Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0);  
  7. yTexture.bind();  
  8.   
  9. //Y texture is (width*height) in size and each pixel is one byte;   
  10. //by setting GL_LUMINANCE, OpenGL puts this byte into R,G and B   
  11. //components of the texture  
  12. Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE,   
  13.     width, height, 0, GL20.GL_LUMINANCE, GL20.GL_UNSIGNED_BYTE, yBuffer);  
  14.   
  15. //Use linear interpolation when magnifying/minifying the texture to   
  16. //areas larger/smaller than the texture size  
  17. Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,   
  18.     GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);  
  19. Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,   
  20.     GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);  
  21. Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,   
  22.     GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE);  
  23. Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,   
  24.     GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE);  
  25.   
  26. /* 
  27.  * Prepare the UV channel texture 
  28.  */  
  29.   
  30. //Set texture slot 1 as active and bind our texture object to it  
  31. Gdx.gl.glActiveTexture(GL20.GL_TEXTURE1);  
  32. uvTexture.bind();  
  33.   
  34. //UV texture is (width/2*height/2) in size (downsampled by 2 in   
  35. //both dimensions, each pixel corresponds to 4 pixels of the Y channel)   
  36. //and each pixel is two bytes. By setting GL_LUMINANCE_ALPHA, OpenGL   
  37. //puts first byte (V) into R,G and B components and of the texture  
  38. //and the second byte (U) into the A component of the texture. That's   
  39. //why we find U and V at A and R respectively in the fragment shader code.  
  40. //Note that we could have also found V at G or B as well.   
  41. Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE_ALPHA,   
  42.     width/2, height/2, 0, GL20.GL_LUMINANCE_ALPHA, GL20.GL_UNSIGNED_BYTE,   
  43.     uvBuffer);  
  44.   
  45. //Use linear interpolation when magnifying/minifying the texture to   
  46. //areas larger/smaller than the texture size  
  47. Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,   
  48.     GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);  
  49. Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,   
  50.     GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);  
  51. Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,   
  52.     GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE);  
  53. Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D,   
  54.     GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE);  

接下来,我们渲染之前准备的网格(覆盖整个屏幕), 着色器将负责渲染网格上的绑定纹理:
copy
  1. shader.begin();  
  2.   
  3. //Set the uniform y_texture object to the texture at slot 0  
  4. shader.setUniformi("y_texture", 0);  
  5.   
  6. //Set the uniform uv_texture object to the texture at slot 1  
  7. shader.setUniformi("uv_texture", 1);  
  8.   
  9. mesh.render(shader, GL20.GL_TRIANGLES);  
  10. shader.end();  

最后,着色器接管将纹理渲染到网格的任务,
实现实际转换的片段着色器如下所示:
  1. String fragmentShader =   
  2.     "#ifdef GL_ES\n"    
  3.     "precision highp float;\n"    
  4.     "#endif\n"    
  5.   
  6.     "varying vec2 v_texCoord;\n"    
  7.     "uniform sampler2D y_texture;\n"    
  8.     "uniform sampler2D uv_texture;\n"    
  9.   
  10.     "void main (void){\n"    
  11.     "   float r, g, b, y, u, v;\n"    
  12.   
  13.     //We had put the Y values of each pixel to the R,G,B components by   
  14.     //GL_LUMINANCE, that's why we're pulling it from the R component,  
  15.     //we could also use G or B  
  16.     "   y = texture2D(y_texture, v_texCoord).r;\n"     
  17.   
  18.     //We had put the U and V values of each pixel to the A and R,G,B   
  19.     //components of the texture respectively using GL_LUMINANCE_ALPHA.   
  20.     //Since U,V bytes are interspread in the texture, this is probably   
  21.     //the fastest way to use them in the shader  
  22.     "   u = texture2D(uv_texture, v_texCoord).a - 0.5;\n"    
  23.     "   v = texture2D(uv_texture, v_texCoord).r - 0.5;\n"    
  24.   
  25.     //The numbers are just YUV to RGB conversion constants  
  26.     "   r = y   1.13983*v;\n"    
  27.     "   g = y - 0.39465*u - 0.58060*v;\n"    
  28.     "   b = y   2.03211*u;\n"    
  29.   
  30.     //We finally set the RGB color of our pixel  
  31.     "   gl_FragColor = vec4(r, g, b, 1.0);\n"    
  32.     "}\n";   
请注意,我们使用相同的坐标变量v_texCoord访问Y和UV纹理,这是由于v_texCoord在-1.0和1.0之间,从纹理的一端到另一端,而不是实际的纹理像素坐标, 这是着色器最好的功能之一。

最后为了方便读者学习,给出完整的代码:

由于libgdx是跨平台的,因此我们需要一个可以在处理设备摄像头和渲染的不同平台中进行不同扩展的对象。 例如,如果您可以让硬件为您提供RGB图像,则可能需要绕过YUV-RGB着色器转换。 因此,我们需要一个将由每个不同平台实现的设备摄像头控制器接口:

  1. public interface PlatformDependentCameraController {  
  2.   
  3.     void init();  
  4.   
  5.     void renderBackground();  
  6.   
  7.     void destroy();  
  8. }   
该界面的Android版本如下(实时摄像机图像假定为1280x720像素):
  1. public class AndroidDependentCameraController implements PlatformDependentCameraController, Camera.PreviewCallback {  
  2.   
  3.     private static byte[] image; //The image buffer that will hold the camera image when preview callback arrives  
  4.   
  5.     private Camera camera; //The camera object  
  6.   
  7.     //The Y and UV buffers that will pass our image channel data to the textures  
  8.     private ByteBuffer yBuffer;  
  9.     private ByteBuffer uvBuffer;  
  10.   
  11.     ShaderProgram shader; //Our shader  
  12.     Texture yTexture; //Our Y texture  
  13.     Texture uvTexture; //Our UV texture  
  14.     Mesh mesh; //Our mesh that we will draw the texture on  
  15.   
  16.     public AndroidDependentCameraController(){  
  17.   
  18.         //Our YUV image is 12 bits per pixel  
  19.         image = new byte[1280*720/8*12];  
  20.     }  
  21.   
  22.     @Override  
  23.     public void init(){  
  24.   
  25.         /* 
  26.          * Initialize the OpenGL/libgdx stuff 
  27.          */  
  28.   
  29.         //Do not enforce power of two texture sizes  
  30.         Texture.setEnforcePotImages(false);  
  31.   
  32.         //Allocate textures  
  33.         yTexture = new Texture(1280,720,Format.Intensity); //A 8-bit per pixel format  
  34.         uvTexture = new Texture(1280/2,720/2,Format.LuminanceAlpha); //A 16-bit per pixel format  
  35.   
  36.         //Allocate buffers on the native memory space, not inside the JVM heap  
  37.         yBuffer = ByteBuffer.allocateDirect(1280*720);  
  38.         uvBuffer = ByteBuffer.allocateDirect(1280*720/2); //We have (width/2*height/2) pixels, each pixel is 2 bytes  
  39.         yBuffer.order(ByteOrder.nativeOrder());  
  40.         uvBuffer.order(ByteOrder.nativeOrder());  
  41.   
  42.         //Our vertex shader code; nothing special  
  43.         String vertexShader =   
  44.                 "attribute vec4 a_position;                         \n"     
  45.                 "attribute vec2 a_texCoord;                         \n"     
  46.                 "varying vec2 v_texCoord;                           \n"     
  47.   
  48.                 "void main(){                                       \n"     
  49.                 "   gl_Position = a_position;                       \n"     
  50.                 "   v_texCoord = a_texCoord;                        \n"    
  51.                 "}                                                  \n";  
  52.   
  53.         //Our fragment shader code; takes Y,U,V values for each pixel and calculates R,G,B colors,  
  54.         //Effectively making YUV to RGB conversion  
  55.         String fragmentShader =   
  56.                 "#ifdef GL_ES                                       \n"    
  57.                 "precision highp float;                             \n"    
  58.                 "#endif                                             \n"    
  59.   
  60.                 "varying vec2 v_texCoord;                           \n"    
  61.                 "uniform sampler2D y_texture;                       \n"    
  62.                 "uniform sampler2D uv_texture;                      \n"    
  63.   
  64.                 "void main (void){                                  \n"    
  65.                 "   float r, g, b, y, u, v;                         \n"    
  66.   
  67.                 //We had put the Y values of each pixel to the R,G,B components by GL_LUMINANCE,   
  68.                 //that's why we're pulling it from the R component, we could also use G or B  
  69.                 "   y = texture2D(y_texture, v_texCoord).r;         \n"     
  70.   
  71.                 //We had put the U and V values of each pixel to the A and R,G,B components of the  
  72.                 //texture respectively using GL_LUMINANCE_ALPHA. Since U,V bytes are interspread   
  73.                 //in the texture, this is probably the fastest way to use them in the shader  
  74.                 "   u = texture2D(uv_texture, v_texCoord).a - 0.5;  \n"                                       
  75.                 "   v = texture2D(uv_texture, v_texCoord).r - 0.5;  \n"    
  76.   
  77.   
  78.                 //The numbers are just YUV to RGB conversion constants  
  79.                 "   r = y   1.13983*v;                              \n"    
  80.                 "   g = y - 0.39465*u - 0.58060*v;                  \n"    
  81.                 "   b = y   2.03211*u;                              \n"    
  82.   
  83.                 //We finally set the RGB color of our pixel  
  84.                 "   gl_FragColor = vec4(r, g, b, 1.0);              \n"    
  85.                 "}                                                  \n";   
  86.   
  87.         //Create and compile our shader  
  88.         shader = new ShaderProgram(vertexShader, fragmentShader);  
  89.   
  90.         //Create our mesh that we will draw on, it has 4 vertices corresponding to the 4 corners of the screen  
  91.         mesh = new Mesh(true, 4, 6,   
  92.                 new VertexAttribute(Usage.Position, 2, "a_position"),   
  93.                 new VertexAttribute(Usage.TextureCoordinates, 2, "a_texCoord"));  
  94.   
  95.         //The vertices include the screen coordinates (between -1.0 and 1.0) and texture coordinates (between 0.0 and 1.0)  
  96.         float[] vertices = {  
  97.                 -1.0f,  1.0f,   // Position 0  
  98.                 0.0f,   0.0f,   // TexCoord 0  
  99.                 -1.0f,  -1.0f,  // Position 1  
  100.                 0.0f,   1.0f,   // TexCoord 1  
  101.                 1.0f,   -1.0f,  // Position 2  
  102.                 1.0f,   1.0f,   // TexCoord 2  
  103.                 1.0f,   1.0f,   // Position 3  
  104.                 1.0f,   0.0f    // TexCoord 3  
  105.         };  
  106.   
  107.         //The indices come in trios of vertex indices that describe the triangles of our mesh  
  108.         short[] indices = {0, 1, 2, 0, 2, 3};  
  109.   
  110.         //Set vertices and indices to our mesh  
  111.         mesh.setVertices(vertices);  
  112.         mesh.setIndices(indices);  
  113.   
  114.         /* 
  115.          * Initialize the Android camera 
  116.          */  
  117.         camera = Camera.open(0);  
  118.   
  119.         //We set the buffer ourselves that will be used to hold the preview image  
  120.         camera.setPreviewCallbackWithBuffer(this);   
  121.   
  122.         //Set the camera parameters  
  123.         Camera.Parameters params = camera.getParameters();  
  124.         params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);  
  125.         params.setPreviewSize(1280,720);   
  126.         camera.setParameters(params);  
  127.   
  128.         //Start the preview  
  129.         camera.startPreview();  
  130.   
  131.         //Set the first buffer, the preview doesn't start unless we set the buffers  
  132.         camera.addCallbackBuffer(image);  
  133.     }  
  134.   
  135.     @Override  
  136.     public void onPreviewFrame(byte[] data, Camera camera) {  
  137.   
  138.         //Send the buffer reference to the next preview so that a new buffer is not allocated and we use the same space  
  139.         camera.addCallbackBuffer(image);  
  140.     }  
  141.   
  142.     @Override  
  143.     public void renderBackground() {  
  144.   
  145.         /* 
  146.          * Because of Java's limitations, we can't reference the middle of an array and  
  147.          * we must copy the channels in our byte array into buffers before setting them to textures 
  148.          */  
  149.   
  150.         //Copy the Y channel of the image into its buffer, the first (width*height) bytes are the Y channel  
  151.         yBuffer.put(image, 0, 1280*720);  
  152.         yBuffer.position(0);  
  153.   
  154.         //Copy the UV channels of the image into their buffer, the following (width*height/2) bytes are the UV channel; the U and V bytes are interspread  
  155.         uvBuffer.put(image, 1280*720, 1280*720/2);  
  156.         uvBuffer.position(0);  
  157.   
  158.         /* 
  159.          * Prepare the Y channel texture 
  160.          */  
  161.   
  162.         //Set texture slot 0 as active and bind our texture object to it  
  163.         Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0);  
  164.         yTexture.bind();  
  165.   
  166.         //Y texture is (width*height) in size and each pixel is one byte; by setting GL_LUMINANCE, OpenGL puts this byte into R,G and B components of the texture  
  167.         Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE, 1280, 720, 0, GL20.GL_LUMINANCE, GL20.GL_UNSIGNED_BYTE, yBuffer);  
  168.   
  169.         //Use linear interpolation when magnifying/minifying the texture to areas larger/smaller than the texture size  
  170.         Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);  
  171.         Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);  
  172.         Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE);  
  173.         Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE);  
  174.   
  175.   
  176.         /* 
  177.          * Prepare the UV channel texture 
  178.          */  
  179.   
  180.         //Set texture slot 1 as active and bind our texture object to it  
  181.         Gdx.gl.glActiveTexture(GL20.GL_TEXTURE1);  
  182.         uvTexture.bind();  
  183.   
  184.         //UV texture is (width/2*height/2) in size (downsampled by 2 in both dimensions, each pixel corresponds to 4 pixels of the Y channel)   
  185.         //and each pixel is two bytes. By setting GL_LUMINANCE_ALPHA, OpenGL puts first byte (V) into R,G and B components and of the texture  
  186.         //and the second byte (U) into the A component of the texture. That's why we find U and V at A and R respectively in the fragment shader code.  
  187.         //Note that we could have also found V at G or B as well.   
  188.         Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_LUMINANCE_ALPHA, 1280/2, 720/2, 0, GL20.GL_LUMINANCE_ALPHA, GL20.GL_UNSIGNED_BYTE, uvBuffer);  
  189.   
  190.         //Use linear interpolation when magnifying/minifying the texture to areas larger/smaller than the texture size  
  191.         Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_LINEAR);  
  192.         Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_LINEAR);  
  193.         Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE);  
  194.         Gdx.gl.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE);  
  195.   
  196.         /* 
  197.          * Draw the textures onto a mesh using our shader 
  198.          */  
  199.   
  200.         shader.begin();  
  201.   
  202.         //Set the uniform y_texture object to the texture at slot 0  
  203.         shader.setUniformi("y_texture", 0);  
  204.   
  205.         //Set the uniform uv_texture object to the texture at slot 1  
  206.         shader.setUniformi("uv_texture", 1);  
  207.   
  208.         //Render our mesh using the shader, which in turn will use our textures to render their content on the mesh  
  209.         mesh.render(shader, GL20.GL_TRIANGLES);  
  210.         shader.end();  
  211.     }  
  212.   
  213.     @Override  
  214.     public void destroy() {  
  215.         camera.stopPreview();  
  216.         camera.setPreviewCallbackWithBuffer(null);  
  217.         camera.release();  
  218.     }  
  219. }  
主应用程序部分只是确保在开始时调用一次init(),renderBackground()每帧渲染循环,并且destroy()最后调用一次:
  1. public class YourApplication implements ApplicationListener {  
  2.   
  3.     private final PlatformDependentCameraController deviceCameraControl;  
  4.   
  5.     public YourApplication(PlatformDependentCameraController cameraControl) {  
  6.         this.deviceCameraControl = cameraControl;  
  7.     }  
  8.   
  9.     @Override  
  10.     public void create() {                
  11.         deviceCameraControl.init();  
  12.     }  
  13.   
  14.     @Override  
  15.     public void render() {        
  16.         Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());  
  17.         Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);  
  18.   
  19.         //Render the background that is the live camera image  
  20.         deviceCameraControl.renderBackground();  
  21.   
  22.         /* 
  23.          * Render anything here (sprites/models etc.) that you want to go on top of the camera image 
  24.          */  
  25.     }  
  26.   
  27.     @Override  
  28.     public void dispose() {  
  29.         deviceCameraControl.destroy();  
  30.     }  
  31.   
  32.     @Override  
  33.     public void resize(int width, int height) {  
  34.     }  
  35.   
  36.     @Override  
  37.     public void pause() {  
  38.     }  
  39.   
  40.     @Override  
  41.     public void resume() {  
  42.     }  
  43. }  
唯一的其他Android特定部分是以下非常短的主要Android代码,您只需创建一个新的Android特定设备相机处理程序并将其传递到主要的libgdx对象:

  1. public class MainActivity extends AndroidApplication {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.   
  7.         AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration();  
  8.         cfg.useGL20 = true//This line is obsolete in the newest libgdx version  
  9.         cfg.a = 8;  
  10.         cfg.b = 8;  
  11.         cfg.g = 8;  
  12.         cfg.r = 8;  
  13.   
  14.         PlatformDependentCameraController cameraControl = new AndroidDependentCameraController();  
  15.         initialize(new YourApplication(cameraControl), cfg);  
  16.   
  17.         graphics.getView().setKeepScreenOn(true);  
  18.     }  
  19. }  
运行速度是非常快的,测试结果如下:

三星Galaxy Note II LTE - (GT-N7105):拥有ARM Mali-400 MP4 GPU。
渲染一帧需要大约5-6毫秒,每隔几秒偶尔会跳到15 ms左右
实际渲染线(mesh.render(着色器,GL20.GL_TRIANGLES);)一致地需要0-1 ms
两个纹理的创建和绑定总共需要1-3毫秒
ByteBuffer副本通常需要1-3毫秒,但偶尔跳到大约7ms,这可能是由于图像缓冲区在JVM堆中移动。


三星Galaxy Note 10.1 2014 - (SM-P600):具有ARM Mali-T628 GPU。
渲染一帧需要大约2-4毫秒,罕见的跳到大约6-10毫秒
实际渲染线(mesh.render(着色器,GL20.GL_TRIANGLES);)一致地需要0-1 ms
两个纹理的创建和绑定总共需要1-3毫秒,但每两秒钟跳到大约6-9毫秒
ByteBuffer副本通常总共需要0-2 ms,但很少跳到大约6ms

另外将Shader的顶点和片段着色器展现如下: copy

  1. attribute vec4 position;  
  2. attribute vec2 inputTextureCoordinate;  
  3. varying vec2 v_texCoord;  
  4. void main()  
  5. {  
  6.    gl_Position = position;  
  7.    v_texCoord = inputTextureCoordinate;  
  8. }  
precision mediump float; varying vec2 v_texCoord; uniform sampler2D yTexture; uniform sampler2D uvTexture; const mat3 yuv2rgb = mat3( 1, 0, 1.2802, 1, -0.214821, -0.380589, 1, 2.127982, 0 ); void main() { vec3 yuv = vec3( 1.1643 * (texture2D(yTexture, v_texCoord).r - 0.0627), texture2D(uvTexture, v_texCoord).a - 0.5, texture2D(uvTexture, v_texCoord).r - 0.5 ); vec3 rgb = yuv * yuv2rgb; gl_FragColor = vec4(rgb, 1.0); }

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引