说明
一般来说,手机摄像头直接获取到的视频数据,都是 YUV 格式的,而要在屏幕上显示最终需要转换为 RGB 的。而这一步的转换,可以用一个矩阵乘法来直接完成。那么为什么呢?
YCbCr 是在世界数字组织视频标准研制过程中作为ITU - R BT1601 建议的一部分, 其实是YUV经过缩放和偏移的翻版。其中Y与YUV 中的Y含义一致, Cb , Cr 同样都指色彩, 只是在表示方法上不同而已。在YUV 家族中, YCbCr 是在计算机系统中应用最多的成员, 其应用领域很广泛,JPEG、MPEG均采用此格式。一般人们所讲的YUV大多是指YCbCr。
几何
YUV 与 RGB 的转换公式不止一种,主要原因是具体格式下,标准不同。本文目的是介绍这个转换的几何意义,所以我们这里采用苹果 Demo 中给出的转换矩阵,其它转换公式中,具体数值可能不同:
let ycbcrToRGBTransform = float4x4(
simd_float4(+1.0000, +1.0000, +1.0000, +0.0000),
simd_float4(+0.0000, -0.3441, +1.7720, +0.0000),
simd_float4(+1.4020, -0.7141, +0.0000, +0.0000),
simd_float4(-0.7010, +0.5291, -0.8860, +1.0000)
);
将上面向量与矩阵乘法写成行列式形式,可能更符合大家的直觉:
R = Y + 1.402*V - 0.701
G = Y - 0.3441*U - 0.7141*V + 0.5291
B = Y + 1.772*U - 0.886
这里我们可以发现,这个 YUV 转 RGB 的公式其实是个线性变换,用几何的方式表达就是说:
- 将一个 RGB 的颜色用 xyz 坐标表示,那么将这个坐标(旋转、缩放、平移)之后,新的 xyz 坐标就可以表示 YUV 颜色值;
- 反之也是,将一个 YUV 颜色分量当做 xyz 坐标,那么将这个坐标逆向(旋转、缩放、平移)之后,新的 xyz 坐标就可以表示 RGB 颜色值;
于是,我们可以在 3D 空间中画一个边长为 1 的正方体,后方左下角(0, 0, 0) 就代表黑色,前方右上角(1, 1, 1) 就代表白色,如下图右下角立方体。同样复制一个,并将其坐标用矩阵转换到 YUV 空间,如下图左上角倾斜的长方体。
对于 RGB 的立方体,比较简单:它的 x 坐标越大,越往右方,颜色越红;y 坐标越大,越往上方,颜色越绿;z 坐标越大,越往前方,颜色越蓝。
而 YUV 的长方体,它的 x 坐标越大,越往右方,亮度越大;y 坐标越大,越往上方,颜色从黄到蓝;z 坐标越大,越往前方,颜色从青绿到红。
代码
let box1 = scene.rootNode.childNode(withName: "box", recursively: true)!
let box2 = scene.rootNode.childNode(withName: "box2", recursively: true)!
simpleProgram(node: box1)
simpleProgram(node: box2)
//YUV 到 RGB
let ycbcrToRGBTransform = float4x4(
simd_float4(+1.0000, +1.0000, +1.0000, +0.0000),
simd_float4(+0.0000, -0.3441, +1.7720, +0.0000),
simd_float4(+1.4020, -0.7141, +0.0000, +0.0000),
simd_float4(-0.7010, +0.5291, -0.8860, +1.0000)
);
let p = ycbcrToRGBTransform.inverse//RGB 到 YUV
box1.simdTransform = p
// box2.simdTransform = box2.simdTransform * ycbcrToRGBTransform.inverse * box2.simdTransform.inverse
//用 shader 进行可视化显示
func simpleProgram(node:SCNNode) {
let program = SCNProgram()
program.vertexFunctionName = "vertexShader"
program.fragmentFunctionName = "fragmentShader"
// 赋值给**SCNGeometry**或者**SCNMaterial**
guard let material = node.geometry?.materials.first else { fatalError() }
material.program = program
}
//默认的头文件
#include <metal_stdlib>
using namespace metal;
//与 SceneKit 配合使用时,需要的头文件
#include <SceneKit/scn_metal>
struct VertexInput {
float3 position [[attribute(SCNVertexSemanticPosition)]];
};
struct ColorInOut
{
float4 position [[position]];
float4 color;
};
struct MyNodeData
{
float4x4 modelViewProjectionTransform;
};
// 顶点着色器函数,输出为 ColorInOut 类型,输入为 VertexInput 类型的变量 in,和 MyNodeData 类型的变量指针 scn_node
vertex ColorInOut vertexShader(VertexInput in [[stage_in]], constant MyNodeData& scn_node [[buffer(0)]])
{
ColorInOut out;
// 将模型空间的顶点补全为 float4 类型,进行 MVP 变换
out.position = scn_node.modelViewProjectionTransform * float4(in.position, 1.0);
// 加 0.5,将坐标从[-0.5~0.5],转换到[0~1] 以代表颜色
out.color = float4(in.position + 0.5, 1);
return out;
}
// 片元着色器函数,输出为 half4,输入为 ColorInOut 类型的变量 in
fragment half4 fragmentShader(ColorInOut in [[stage_in]])
{
return half4(in.color);
}