YUV <-> RGB
- 2025-09-27 四川
本文字数:3178 字
阅读完需:约 10 分钟

前面犯了一个小小的错误,我实现了了一个 HSV 到 RGB 算法,但是这是个基础算法,在实际的图像处理中,我们面临的是各种 YUV 的格式和 RGB 格式的转化,也就是 24bit 数据之间的转化,所以我们 的 转换算法应该是
fn yuv2rgb(yuv) {
let y = ((yuv[0] - 16.0) * 255.0 / 219.0).clamp(0.0, 255.0);
let u = yuv[1] - 128.0;
let v = yuv[2] - 128.0;
let r = (y + 1.402 * v).clamp(0.0, 255.0).round();
let g = (y - 0.344136 * u - 0.714136 * v).clamp(0.0, 255.0).round();
let b = (y + 1.772 * u).clamp(0.0, 255.0).round();
let rgb = (r as u32) << 16 | (g as u32) << 8 | (b as u32);
return rgb;
}
这是一个使用浮点数 的 yuv 转换到 RGB 的算法
我们使用 spirv 的代码来实现几个 内建的函数 clamp 和 round
"clamp"=> {
let (ty, ty_id, s1, s2) = self.binary(BinaryOp::Null, &args[0], &args[1])?;//-> Result<(Type, u32, u32, u32)> {
let v3 = self.get(&args[2])?;
let s3 = self.get_var(v3);
let ids: Vec<rspirv::dr::Operand> = [s1, s2, s3.0].iter().map(|id| rspirv::dr::Operand::IdRef(*id) ).collect();
let id = self.builder.ext_inst(ty_id, None, self.glsl_std_450, spirv::GLOp::FClamp as u32, ids)?;
self.set(ret, Var{id, ty: SpirvType::Var(ty)});
}
"round"=> {
let v = self.get(&args[0])?;
let s = self.get_var(v);
let id = self.builder.ext_inst(s.2, None, self.glsl_std_450, spirv::GLOp::Round as u32, [rspirv::dr::Operand::IdRef(s.0)])?;
self.set(ret, Var{id, ty: SpirvType::Var(s.1)});
}
其中 self.glsl_std_450 是标准的扩展库, 需要在创建 builder 的时候加载
let glsl_std_450 = builder.ext_inst_import("GLSL.std.450");
我们使用给定的测试值来测试 YUV 转换函数
设定 main 函数这样
pub fn main(param: Param) {
let rgb = yuv2rgb([81u8, 90u8, 240u8]);
let id = spirv_local();
let z = id[1] * 32 + id[0];
param.id[z] = rgb;
}
"#;
测试 [81,90,240]这一组,结果应该是 RGB 的 [233,9,8]
使用下面的函数调用
let spirv_code = b.assemble();
let mut rt = spirv::Runtime::default();
let buf = rt.run::<[u32; 1024], _>(&spirv_code, |buf| {} , [1, 1, 1])?;
println!("{:x?}", buf.read().unwrap()[0]);
结果是这样

完美!
现在我们开始 实际的转换
我们先使用 yuv 库将 rgb 图像转换为 YUV
let img = image::open("demo.jpg")?.to_rgb8();
let (w, h) = (img.width(), img.height());
let mut yuv = yuv::YuvPlanarImageMut::alloc(w, h, yuv::YuvChromaSubsampling::Yuv444);
let start = std::time::Instant::now();
for _ in 0..1 {
yuv::rgb_to_yuv444(&mut yuv, img.as_raw(), w * 3, yuv::YuvRange::Full, yuv::YuvStandardMatrix::Bt601, yuv::YuvConversionMode::Balanced)?;
}
let elapsed = start.elapsed();
YUV 数据 分三个平面存放 我们的图像大小是 2560 * 1440
我们的 Zeta 转换函数是这样的 没有做任何优化
fn yuv2rgb(y, u, v) {
let r = (y + 1.402f32 * (v - 128.0)).clamp(0.0, 255.0);
let g = (y - 0.344136f32 * (u - 128.0) - 0.714136f32 * (v - 128.0)).clamp(0.0, 255.0);
let b = (y + 1.772f32 * (u - 128.0)).clamp(0.0, 255.0);
let rgb = (r as u32) | (g as u32) << 8 | (b as u32) << 16 | 0xFF000000u32;
return rgb;
}
转换回来的结果应该是 RGBA Alpha 固定设置为 0xFF
我们使用固定大小的缓冲区 传递数据
pub struct Param {
y: [u8; 3686400],
u: [u8; 3686400],
v: [u8; 3686400],
rgb: [u32; 3686400],
}
这个 Zeta 和 Rust 可以共用
整个 Zeta Shader 代码是这样的
pub struct Param {
y: [u8; 3686400],
u: [u8; 3686400],
v: [u8; 3686400],
rgb: [u32; 3686400],
}
fn yuv2rgb(y, u, v) {
let r = (y + 1.402f32 * (v - 128.0)).clamp(0.0, 255.0);
let g = (y - 0.344136f32 * (u - 128.0) - 0.714136f32 * (v - 128.0)).clamp(0.0, 255.0);
let b = (y + 1.772f32 * (u - 128.0)).clamp(0.0, 255.0);
let rgb = (r as u32) | (g as u32) << 8 | (b as u32) << 16 | 0xFF000000u32;
return rgb;
}
pub fn main(param: Param) {
let g_id = spirv_group();
let id = spirv_local();
let x = g_id[0] * 32 + id[0]; // x 方向的坐标 分成 32 x 32 的小块
let y = g_id[1] * 32 + id[1]; // y 方向的坐标 分成 32 x 32 的小块
let offset = y * 2560 + x;
let rgb = yuv2rgb(param.y[offset], param.u[offset], param.v[offset]);
param.rgb[offset] = rgb;
}
"#;
每个组 32 x 32 一个组同时启动 1024 个线程 我们
运行的代码是这样的
let mut rt = spirv::Runtime::default();
let start = std::time::Instant::now();
let buf = rt.run::<Param, _>(spirv_code, |param| {
param.y.copy_from_slice(yuv.y_plane.borrow());
param.u.copy_from_slice(yuv.u_plane.borrow());
param.v.copy_from_slice(yuv.v_plane.borrow());
//param.rgb.copy_from_slice(&yuv.rgb());
} , [80, 44, 1])?;
let elapsed = start.elapsed();
println!("spirv use {}", elapsed.as_micros());
我们启动 80 个 x 44 个 y 正好处理了 2560 x 1440 的数据
验证的记过完美!!
yuv 是高度优化的代码,我们执行 1000 次看看
let start = std::time::Instant::now();
for _ in 0..1000 {
yuv::rgb_to_yuv444(&mut yuv, img.as_raw(), w * 3, yuv::YuvRange::Full, yuv::YuvStandardMatrix::Bt601, yuv::YuvConversionMode::Balanced)?;
}
let elapsed = start.elapsed();
耗时

微秒,极度强大!!
但是我们的 spirv 版本更牛,不能用 只能计算执行时间 如果算上拷贝时间那就太不公平了
所以我们在 spirv Runtime 里面计算时间
let start = std::time::Instant::now();
for _ in 0..1000 {
let mut builder = AutoCommandBufferBuilder::primary(command_buffer_allocator.clone(), self.queue.queue_family_index(), CommandBufferUsage::OneTimeSubmit).unwrap();
builder.bind_pipeline_compute(pipeline.clone())?;
builder.bind_descriptor_sets(PipelineBindPoint::Compute, pipeline_layout.clone(), 0, set.clone())?;
unsafe { builder.dispatch(dim) }.unwrap();
let command_buffer = builder.build().unwrap();
let future = sync::now(self.device.clone()).then_execute(self.queue.clone(), command_buffer).unwrap().then_signal_fence_and_flush().unwrap();
future.wait(None).unwrap();
}
let elapsed = start.elapsed();
println!("spirv use {}", elapsed.as_micros());
就不去优化缓冲区的批量处理了

我们的结果是这个,显然 还是有数量级的提高!!!

Miracle
三十年资深码农 2019-10-25 加入
还未添加个人简介
评论