21、图像采集与处理:OV5640摄像头配置、DVP接口时序、RGB转灰度、边缘检测(Sobel算子)
图像处理这块,说实话是FPGA最能体现价值的地方之一。CPU做图像处理,一帧一帧地搬数据,延时大得吓人。FPGA不一样,像素流进来,流水线一过,结果就出来了。今天咱们就聊聊怎么在紫光同创的板子上,把OV5640摄像头的数据抓进来,再做点简单的处理。
21.1 OV5640摄像头配置:I2C总线上的那些坑
OV5640这颗传感器,500万像素,性价比很高。我项目里用过好几次,每次配置I2C寄存器都得小心翼翼。为什么?因为一旦某个寄存器写错了,图像可能就花屏或者干脆没输出。
配置OV5640,说白了就是通过I2C总线往它内部寄存器写值。比如设置输出分辨率、帧率、数据格式等等。我个人习惯先把初始化序列整理成一张表,然后用状态机去遍历这张表。
关键寄存器配置示例:
- 0x3103 -> 0x11:软复位,让摄像头重新启动
- 0x3008 -> 0x02:进入系统控制模式
- 0x3035 -> 0x21:设置PLL分频,得到想要的像素时钟
- 0x3036 -> 0x46:PLL倍频系数,这个值直接影响帧率
- 0x3820 -> 0x46:设置数据格式为RGB565
- 0x3821 -> 0x00:关闭镜像翻转
写I2C的时候,有个细节很容易忽略:OV5640的I2C地址是0x3C(写地址),但有些资料写的是0x78。其实0x3C左移一位就是0x78,别搞混了。我曾经在这个问题上折腾了半天,最后发现是地址搞错了。
// I2C写寄存器状态机片段
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
i2c_start <= 1'b0;
end else begin
case (state)
IDLE: begin
if (start_config) begin
// 取出当前要写的寄存器地址和数据
reg_addr <= config_rom[addr_cnt][15:8];
reg_data <= config_rom[addr_cnt][7:0];
state <= START_I2C;
end
end
START_I2C: begin
// 发送起始条件 + 设备地址
i2c_start <= 1'b1;
state <= SEND_ADDR;
end
// ... 后续状态
endcase
end
end
小技巧:配置完成后,建议读回几个关键寄存器验证一下。比如读0x300A和0x300B,看看芯片ID是不是0x5640。如果读回来不对,说明I2C通信有问题,先查硬件连接。
21.2 DVP接口时序:同步信号不能马虎
OV5640支持DVP(Digital Video Port)并行接口,说白了就是一组数据线加上行同步、场同步和像素时钟。DVP接口的时序其实不复杂,但有个地方特别容易出问题——同步信号的极性。
OV5640的HREF(行同步)默认是高电平有效,VSYNC(场同步)默认是低电平有效。但有些摄像头模组可能被配置成相反的极性。我建议你在配置寄存器的时候,明确设置一下0x3820和0x3821这两个寄存器,把极性固定下来。
DVP接口的时序,说白了就是:
- VSYNC拉高(或拉低)表示一帧开始
- HREF拉高表示一行有效数据开始
- 在PCLK的上升沿(或下降沿)采集D[9:0]上的数据
嗯,这里要注意:PCLK的边沿选择也很关键。OV5640默认是在PCLK上升沿输出数据,但有些配置可以改成下降沿。你想想看,如果FPGA这边采集边沿和摄像头输出边沿不匹配,那采到的数据全是错的。
// DVP数据采集模块
always @(posedge pclk or negedge rst_n) begin
if (!rst_n) begin
pixel_data <= 16'd0;
href_delay <= 2'b0;
end else begin
// 打两拍同步,消除亚稳态
href_delay <= {href_delay[0], href};
vsync_delay <= {vsync_delay[0], vsync};
// 在HREF有效时采集数据
if (href_delay[1]) begin
// 拼接两个8位数据得到16位RGB565
pixel_data <= {data_in, pixel_data[7:0]};
end
end
end
注意:DVP接口的数据线是10位的(D[9:0]),但RGB565模式只用高8位(D[9:2])。低两位可以悬空或者接地。我见过有人把低两位也接上了,结果图像颜色不对,排查了半天。
21.3 RGB转灰度:加权平均才是正道
拿到RGB565数据后,很多时候我们需要把它转成灰度图。为什么?因为后续的边缘检测、二值化等算法,都是在灰度图上做的。彩色图数据量大,处理起来也慢。
RGB转灰度,最简单的办法是取三个分量的平均值。但这样做出来的灰度图,人眼看起来不太自然。为什么?因为人眼对绿色最敏感,对蓝色最不敏感。所以标准的转换公式是:
Gray = 0.299 * R + 0.587 * G + 0.114 * B
在FPGA里做浮点运算太慢了,我一般把它转成整数运算:
// RGB565转灰度(整数运算)
// Gray = (77*R + 150*G + 29*B) >> 8
always @(posedge clk) begin
// 先从RGB565中提取各分量
R <= {pixel_in[15:11], 3'b0}; // 5位扩展到8位
G <= {pixel_in[10:5], 2'b0}; // 6位扩展到8位
B <= {pixel_in[4:0], 3'b0}; // 5位扩展到8位
// 加权求和
gray_temp <= 77 * R + 150 * G + 29 * B;
gray_out <= gray_temp[15:8]; // 右移8位
end
你看,这样只用乘法和加法,一个时钟周期就能出结果。流水线一搭,每个像素进来都能实时输出灰度值。
经验之谈:如果你觉得上面的系数算出来图像偏暗,可以试试调整系数。比如用 (R+G+B)/3 的平均值法,虽然不精确,但胜在简单。我有个项目里,客户觉得加权法太暗,最后改成了平均值法,效果反而更好。说白了,工程应用要灵活。
21.4 边缘检测(Sobel算子):梯度计算的艺术
边缘检测,说白了就是找图像中亮度变化剧烈的地方。Sobel算子是最常用的方法之一,它通过两个3x3的卷积核,分别计算水平和垂直方向的梯度。
Sobel算子的两个核:
| Gx(水平梯度) | Gy(垂直梯度) |
|---|---|
|
-1 0 +1 -2 0 +2 -1 0 +1 |
+1 +2 +1 0 0 0 -1 -2 -1 |
在FPGA里实现Sobel,核心是两件事:一是构建3x3的窗口,二是做卷积运算。构建窗口需要用到行缓存(Line Buffer),说白了就是先把两行数据存起来,等第三行来了,三个行数据一起送到卷积模块。
// 3x3窗口构建(使用两个行缓存)
// 假设图像宽度为WIDTH
reg [7:0] line_buf0 [0:WIDTH-1]; // 第一行缓存
reg [7:0] line_buf1 [0:WIDTH-1]; // 第二行缓存
reg [7:0] p11, p12, p13; // 第一行三个像素
reg [7:0] p21, p22, p23; // 第二行三个像素
reg [7:0] p31, p32, p33; // 第三行三个像素
// 每个时钟周期更新窗口
always @(posedge clk) begin
// 更新第三行数据
p31 <= p32;
p32 <= p33;
p33 <= line_buf1[col_cnt];
// 更新第二行数据
p21 <= p22;
p22 <= p23;
p23 <= line_buf0[col_cnt];
// 更新第一行数据
p11 <= p12;
p12 <= p13;
p13 <= pixel_in; // 当前输入的像素
// 更新行缓存
line_buf0[col_cnt] <= pixel_in;
line_buf1[col_cnt] <= line_buf0[col_cnt];
end
卷积运算就简单了,就是乘加:
// Sobel梯度计算
// Gx = (p13 - p11) + 2*(p23 - p21) + (p33 - p31)
// Gy = (p31 - p11) + 2*(p32 - p12) + (p33 - p13)
wire signed [10:0] gx = (p13 - p11) + 2*(p23 - p21) + (p33 - p31);
wire signed [10:0] gy = (p31 - p11) + 2*(p32 - p12) + (p33 - p13);
// 计算梯度幅值(近似法:|Gx| + |Gy|)
wire [10:0] grad = (gx[10] ? -gx : gx) + (gy[10] ? -gy : gy);
// 阈值比较,输出二值化边缘
assign edge_out = (grad > THRESHOLD) ? 8'hFF : 8'h00;
关键点:梯度幅值的计算,理论上应该是 sqrt(Gx² + Gy²),但FPGA里开方太费资源。我一般用绝对值之和近似,效果差不多,资源省一大半。阈值THRESHOLD需要根据实际图像调整,我建议先设成50左右,再微调。
21.5 完整流水线架构
把上面这些模块串起来,就是一个完整的图像采集与处理流水线:
- 摄像头配置模块:通过I2C初始化OV5640,设置输出格式为RGB565
- DVP采集模块:根据HREF和PCLK采集像素数据,拼接成16位RGB565
- RGB转灰度模块:将RGB565转换为8位灰度值
- Sobel边缘检测模块:使用3x3窗口计算梯度,输出二值化边缘
- 显示输出模块:将处理后的数据送到HDMI或VGA显示
整个流水线,每个像素只需要几个时钟周期的延时。这就是FPGA的优势——真正的实时处理。CPU要做到同样的帧率,得用多核并行或者GPU加速,功耗和成本都上去了。
避坑指南:我曾经在调试Sobel的时候,发现边缘检测结果总是有偏移。查了半天,原来是行缓存没有处理好边界像素。图像的第一列和最后一列,3x3窗口是残缺的。我的处理方法是:直接丢弃边界像素,或者用复制的方式补齐。建议你一开始就考虑这个问题,不然图像边缘会出现奇怪的条纹。
好了,这一章的内容就到这里。图像采集与处理是个大话题,今天我们只讲了最基础的流水线。下一章我们会聊聊更高级的图像处理算法,比如高斯滤波和形态学操作。到时候见。