用Shader实现的YUV到RGB转换

来源:互联网 发布:九阴真经捏脸数据男 编辑:程序博客网 时间:2024/06/08 11:09

    ////本文由锈水管原创。

    网上有很多YUV到RGB的转化程序,不过他们基本上都是基于CPU进行计算,基于CPU计算大体上有一下的一些方法,最原始的肯定是根据转换公式直接进行浮点运算,要想提高速度,可以用左移和右移操作,将浮点运算变成整数运算,这样转化的速度会成倍的提高。另外还可以用查表法,因为YUV都是在0~255之间,他们总是有范围的,先生成一个很大的查找表,直接对每一个YUV分量查找出RGB值,当然这个查找表会很大,可以用部分查找的方法来缩小查找表的容量。

    用CPU计算比较好的方法就是利用CPU的SSE指令,有一个专门的名词是来形容SSE的,那就是“单指令流多数据流”,意思就是他可以一次对多个数据进行计算,当然速度是非常快的。

    在这里,我主要写一下我用shader做转化的方法,首先,当然是介绍一下YUV:

    Windows下YUV格式的介绍在MSDN上介绍的非常全面。YUV的格式有很多种,有444,422,420之分,并且每一种具体的格式也有许多不同的标准。具体在MSDN的以下地址上有详细的介绍:
http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/VideoRende8BitYUV.mspx?mfr=true

    不过对于写程序来说,我只关心每种格式的内存布局方式。
    YUV分为打包格式(packed)和平面格式(planar),平面格式中YUV的每个分量是分开存储的,一般先存储所有的Y分量,再存储所有的U分量,然后存储所有的V分量;打包方式则是YUV的每个分量并没有分开存储,而是存储在一起。例如:

    打包格式(图片来自于MSDN),422中的YUY2格式:

          

            
平面格式,420中的IMC4格式:

          


             

    我用到的就是YUV420的IMC4格式,是一种平面格式。这种格式的特点是先存储所有的Y分量,总共有多少像素,就有多少个Y分量。紧接着,存储U分量,再存储V分量,U和V分别都只占总像素的1/4,所以,我所用的YUV420格式的一帧所占的内存空间可以这样计算:
         m_FrameHeight;    //视频高度
        m_FrameWidth;    //视频宽度
        m_ImgSize = m_FrameWidth * m_FrameHeight; //宽 * 高
       m_FrameSize = m_ImgSize + (m_ImgSize >> 1); //YUV视频一帧的大小,其中的右移相当于/2

    所以每个分量的内存地址可以按如下方式计算:
         unsigned char* cTemp[3];
         cTemp[0] = m_yuv + m_FrameSize * n;     //y分量地址
        cTemp[1] = cTemp[0] + m_ImgSize;     //u分量地址
         cTemp[2] = cTemp[1] + (m_ImgSize >> 2);     //v分量地址
    m_yuv是整段视频的首地址,也就是指向整段视频的指针;n是当前帧的序号。
知道了每一帧的YUV各分量的地址,再进行转换就比较容易了。有很多公式可以进行转换,我所用到的转换公式是这样的:
        R = y + 1.4022 * (v - 128)
        G = y - 0.3456 * (u - 128) - 0.7145 * (v - 128)
        B = y + 1.771 * (u - 128)

    下面开始利用Shader按照这个转换公式来进行计算,由于我也是刚刚开始接触shader编程,对于shader的工作方式也不是十分熟悉,我也只是按照自己的理解来进行编程。
    首先,显卡在运行shader程序的时候,是很多像素并行计算的,所以转换的速度很快,但是我发现针对每一个像素,shader只知道他自己的像素信息,包括他自己的颜色(RGB)、深度、纹理坐标等等。对于其他像素的信息一无所知,例如,一个红色像素只知道他自己应该显示成红色,对于他左边的像素是什么颜色,他不会知道,更不用说隔他更远的像素了。于是他仅仅只能利用自己的信息来进行运算。这样就遇到一个问题,由于YUV的每个分量存储的位置相隔很远,要在一个shader程序中分别得到一个像素相应的YUV分量数据,就要对内存的数据排列格式做相应的调整。我所用的方法是将视频的数据格式调整成按照YUVYUVYUV...一一对应的格式来排列,这样,我可以先把YUV的数据假装是RGB的数据送入到纹理中,骗过显卡,然后再在shader中用转换公式来进行计算。由于YUV分量的数据范围也是0~255之间的,所以这种方式是可以成功的。
    (这里可能说的不对,我认为每一个像素在运行shader的时候,应该是有方法能够得到除自己以外其他像素信息的,但是我学习shader还只有一个星期的时间,可能还不知道方法,如果有这样的方法的话,那么转换的效率会更高,因为我不需要在内存中再花时间来做数据的排列工作。但这个可能要对shader的工作原理非常清楚,等以后我熟悉了,再来重新写一个程序。)
    下面开始,首先是排列过程,这个过程发生在内存中:
        for(y=0; y < m_FrameHeight; y++)
         {
            for(x=0; x < m_FrameWidth; x++)
             {
                l = y * m_FrameWidth + x;
                  m = (y / 2) * (m_FrameWidth / 2) + x / 2;
                m_Data[3 * l] = cTemp[0][l];    //y
                m_Data[3 * l + 1] = cTemp[1][m];   //u
                  m_Data[3 * l + 2] = cTemp[2][m];   //v
             }
        }
    排列的方法很简单,只要理解了上面的那个内存布局结构,就很容易知道了。
    然后将排列好的YUV数据送入到纹理中,在每一帧的渲染之前,都运行shader程序。其中shader程序代码如下:
    顶点着色器Vertex Shader:
         void main()
         {
             gl_TexCoord[0] = gl_MultiTexCoord0;
            gl_Position = ftransform();
        }
    片段着色器Fragment Shader:
         uniform sampler2D tex;
         void main()
         {
             vec4 yuv = texture2D(tex,gl_TexCoord[0].st);
              vec4 color;
              color.r =yuv.r + 1.4022 * yuv.b - 0.7011;
             color.r = (color.r < 0.0) ? 0.0 : ((color.r > 1.0) ? 1.0 : color.r);
               color.g =yuv.r - 0.3456 * yuv.g - 0.7145 * yuv.b + 0.53005;
             color.g = (color.g < 0.0) ? 0.0 : ((color.g > 1.0) ? 1.0 : color.g);
             color.b =yuv.r + 1.771 * yuv.g - 0.8855;
              color.b = (color.b < 0.0) ? 0.0 : ((color.b > 1.0) ? 1.0 : color.b);
             gl_FragColor = color;
         }
    其中为什么会有-0.7011、0.53005、0.8855这样的常数,那是因为颜色数据从着色器中的到的时候,已经转化成了浮点数的形式,范围为0.0~1.0之间,所以转换公式后面的系数也要做相应的变化。

    最终效果,其中程序框架就是上次我写的MFC的框架: