0x00 背景
常见的颜色空间有 RGB 和 YCbCr。YCbCr 能够提供比 RGB 更好的压缩比,因为只需要保证 Y 分量精度够高,Cb 和 Cr 进行适当压缩不影响最后的体感质量,所以在视频相关领域中被广泛使用。
在 OpenCV 中,有方便的 cvtColor 函数可以执行该转换,但在 iOS 中则没有相应的方法,只能手撸。 幸运的是转换公式是现成的,可以参考这里: http://www.equasys.de/colorconversion.html , 里面有很多套公式,通过对 GPUImage 代码的研读,可以推算出 iOS 中一般采用的是 HDTV 那一套(BT.709)。
0x01 CPU 方式
有了公式就能进行转换了,最简单的办法就是进行 pixel-wise 的操作,但即便这个简单的需求发现资料也挺少,这里就简单提一下。
首先需要将 Image 转换为 Bitmap,Bitmap 有几种不同的 Pixel format,最常用的是 kCGImageAlphaPremultipliedLast 代表 RGB 分量已经预乘了 alpha,并且 alpha 在最高位地址。当我们取出一个像素点时(32bits integer),拿pixel 的信息可以拆分为两步:
- 拿到 pixel 的 value。首先可以通过 bitmapcreate+drawimage 的形式拿到 rawData 的 pointer。然后用 i 遍历 height,j 遍历 width,并通过:
uint *pixel_data = rawData + i * width + j
即可拿到单像素的值,value 格式为 RGBA8888
- 然后结合 iOS big endian的特性,通过如下的宏拿到各个 channel 的分量
#define Mask8(x) ( (x) & 0xFF )
#define R(x) ( Mask8(x) )
#define G(x) ( Mask8(x >> 8 ) )
#define B(x) ( Mask8(x >> 16) )
能够进行 pixel-wise operation 理论上来说就可以转换了,但 CPU 方式在做 pixel-wise operation 往往不如 GPU,所以 CPU 方式在此不继续展开。
0x02 GPU 方式
iOS 上进行 GPU 编程有几个选择,比如 OpenGL ES 和 metal。但 GPUImage 良好的架构,当仁不让的成为第一选择。
核心的思想就是将 pixel-wise 的转换放到 shader 中实现,GPUImage 内部包含了一个 YUV-> RGB 的转换流程,通过研究其实现结合0x01中介绍的公式可以得出转换的 shader
PS:
- OpenGL 的 mat 时 column major,所以从公式中拿出矩阵来乘的时候需要进行一次 transpose
- iOS 的颜色空间 RGB 分别是[0,1]而不是[0,255] ,所以公式中的16 128 128 需要分别转换为 16.0/255.0 0.5 0.5
RGB->YUV:
varying mediump vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
mediump vec3 yuv;
mediump vec3 rgb;
rgb = texture2D(inputImageTexture, textureCoordinate).rgb;
yuv = mat3(0.256, -.148, .439,
.504, -.291, -.368,
0.098, .439,-.071 ) * rgb + vec3(16.0 / 255.0, 0.5, 0.5);
gl_FragColor = vec4(yuv.r,0,0, 1);
}
YUV->RGB:
varying mediump vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
mediump vec3 yuv;
mediump vec3 rgb;
yuv = texture2D(inputImageTexture, textureCoordinate).rgb - vec3(16.0/255.0, 0.5, 0.5);
rgb = mat3(1.164, 1.164, 1.164,
0, -.392, 2.017,
1.596, -.813,0 ) * yuv;
gl_FragColor = vec4(rgb, 1);
}
最后分别为这两个 shader 配套建立一个 custom filter 即可。