如何用Rust读取并转换Arduino UNO的OV7670摄像头YVYU输出?
Arduino OV7670摄像头图像解码问题
背景
我在Arduino UNO项目中使用OV7670摄像头,采用对应代码可正常运行,通过Windows平台的Serial Port Reader工具能查看摄像头图像。现在尝试用Rust重写该工具适配Mac,已成功连接并读取串口输出,但无法将原始字节转换为有效图像。
已知信息
- 摄像头通过
captureImg函数中的UDR0 = (PINC & 15) | (PIND & 240);语句输出原始像素; - 像素格式为YUV422,对应配置
const struct regval_list yuv422_ov7670; - 字节顺序为Y V Y U,由
yuv422_ov7670中的{ REG_COM13, COM13_UVSAT }配置,该信息来自OV7670 datasheet第18页。
疑问点
1. 输出字节数为何不固定?
- 摄像头发送像素前会先发送
*RDY*标识,用于确定解码起始点; - 代码调用
captureImg(320, 240),应输出320×240=76800个像素,但两次*RDY*间的字节数在79000-79500之间。
2. 如何将YUV422 YVYU格式字节转换为RGB?
- 搜索得知两种编码方式:2像素用4字节Y0 V Y1 U编码,或1像素用2字节(Y+高4位V+低4位U)编码;
- 找到yuv crate可实现转换,但需指定
MatrixCoefficients,不清楚yuv422_ov7670中的对应系数。
当前代码问题
以下是我编写的解码保存函数,通过image crate将每4字节转换为2个RGB像素,但因字节数不足导致图像一半为黑,且最终生成的图像与原工具生成的正常图像差异极大。
fn decode_and_save(data: &[u8]) { let mut imgbuf = image::ImageBuffer::new(IMAGE_WIDTH, IMAGE_HEIGHT); let mut ix = IMAGE_WIDTH; let mut iy = IMAGE_HEIGHT; let converter = RGBConvert::<u8>::new(Range::Full, MatrixCoefficients::BT709).unwrap(); for ch in data.chunks_exact(4) { let yuv1 = YUV { y: ch[0], v: ch[1], u: ch[3], }; let yuv2 = YUV { y: ch[2], v: ch[1], u: ch[3], }; let rgb1 = converter.to_rgb(yuv1); let rgb2 = converter.to_rgb(yuv2); imgbuf.put_pixel(ix - 1, iy - 1, Rgb([rgb1.r, rgb1.g, rgb1.b])); imgbuf.put_pixel(ix - 2, iy - 1, Rgb([rgb2.r, rgb2.g, rgb2.b])); ix -= 2; if ix == 0 { ix = IMAGE_WIDTH; iy -= 1; } if iy == 0 { break; } } imgbuf.save("./test.png").unwrap(); }
问题解答
关于字节数不固定的问题
Arduino UNO的串口在高速传输摄像头数据时,易因硬件中断优先级、IO延迟出现传输丢包、帧错误或额外填充字节。另外你混淆了像素数与字节数:YUV422 YVYU格式下,320×240像素需要320*240*2=153600字节(每2像素占4字节),当前读取的79000-79500字节远不足有效数据量。
解决方法:找到*RDY*标识后,跳过标识本身的5字节,持续读取直到获取153600字节,而非依赖两次*RDY*的间隔。
关于YUV转RGB的正确方式
OV7670默认使用BT.601转换系数(你代码中误用了BT709),这是色彩差异的核心原因。同时你的像素写入顺序完全错误:摄像头是逐行从左到右、从上到下输出像素,而你从图像右下角开始写入,导致画面颠倒混乱。
修正后的解码代码
use image::{ImageBuffer, Rgb}; use yuv::{RGBConvert, Range, MatrixCoefficients, YUV}; const IMAGE_WIDTH: u32 = 320; const IMAGE_HEIGHT: u32 = 240; const TOTAL_BYTES: usize = (IMAGE_WIDTH * IMAGE_HEIGHT * 2) as usize; // YVYU格式下2字节/像素 fn decode_and_save(raw_data: &[u8]) { // 定位*RDY*标识,跳过标识获取有效数据起始位置 let rdy_mark = b"*RDY*"; let start_idx = raw_data.windows(rdy_mark.len()) .position(|window| window == rdy_mark) .map(|idx| idx + rdy_mark.len()) .unwrap_or(0); // 截取固定长度的有效数据,避免多余字节干扰 let valid_data = &raw_data[start_idx..start_idx + TOTAL_BYTES.min(raw_data.len() - start_idx)]; let mut imgbuf = ImageBuffer::new(IMAGE_WIDTH, IMAGE_HEIGHT); // 使用OV7670默认的BT.601转换系数 let converter = RGBConvert::<u8>::new(Range::Full, MatrixCoefficients::BT601).unwrap(); let mut pixel_idx = 0; // 逐4字节处理,对应2个连续像素 for chunk in valid_data.chunks_exact(4) { let (y0, v, y1, u) = (chunk[0], chunk[1], chunk[2], chunk[3]); // 转换并写入第一个像素(左) let rgb1 = converter.to_rgb(YUV { y: y0, u, v }); let x = (pixel_idx % IMAGE_WIDTH as usize) as u32; let y = (pixel_idx / IMAGE_WIDTH as usize) as u32; imgbuf.put_pixel(x, y, Rgb([rgb1.r, rgb1.g, rgb1.b])); pixel_idx += 1; // 转换并写入第二个像素(右) let rgb2 = converter.to_rgb(YUV { y: y1, u, v }); let x = (pixel_idx % IMAGE_WIDTH as usize) as u32; let y = (pixel_idx / IMAGE_WIDTH as usize) as u32; imgbuf.put_pixel(x, y, Rgb([rgb2.r, rgb2.g, rgb2.b])); pixel_idx += 1; } imgbuf.save("./test.png").unwrap(); }
额外注意事项
- 确保串口波特率与Arduino代码完全一致(通常为115200);
- Mac下串口读取需禁用RTS/CTS流控,避免传输中断;
- 若仍有丢包,可在Arduino代码中增大串口缓冲区,或降低摄像头输出分辨率测试。
内容的提问来源于stack exchange,提问作者Виталий Свинчак
相关产品推荐
相关产品推荐

