4. 视频采集与预处理:DVP/MIPI接口驱动开发、色彩空间转换(Bayer to RGB)、坏点校正算法
各位同学,今天我们聊一个硬核话题——视频采集与预处理。说白了,就是内窥镜的「眼睛」怎么把光信号变成数字图像,并且让图像看起来干净、真实。
我个人习惯把这一章分成三个部分:接口驱动、色彩转换、坏点校正。这三块是流水线关系,缺一不可。你想想看,如果接口没调通,后面全是白搭;如果色彩不对,医生看着蓝汪汪的组织,那还怎么诊断?
4.1 DVP/MIPI接口驱动开发
先说说接口。内窥镜的传感器输出,目前主流就两种:DVP(并行接口)和MIPI(差分串行接口)。
DVP接口,老派但简单。数据线、像素时钟、行同步、场同步,就这么几根线。我在项目中遇到过用DVP接口的OV7670传感器,调试起来确实方便,逻辑分析仪一挂,波形一目了然。但缺点也很明显——线多,抗干扰差,频率上不去。
MIPI接口,现在的主流。差分信号,高速串行,线少,抗干扰强。但调试起来嘛……嗯,这里要注意,MIPI的物理层(D-PHY)对时序要求极高,稍微有点抖动,图像就花屏。
我建议,新手先从DVP入手,理解「像素时钟+同步信号」的基本概念,再转MIPI会轻松很多。
4.1.1 DVP驱动要点
DVP驱动,核心就是配置GPIO和DMA。我贴一段伪代码,大家感受一下:
// DVP初始化
void dvp_init(void) {
// 1. 配置GPIO:数据线、VSYNC、HSYNC、PCLK
gpio_config(DVP_DATA_PINS, GPIO_MODE_INPUT);
gpio_config(DVP_VSYNC_PIN, GPIO_MODE_INPUT);
gpio_config(DVP_HSYNC_PIN, GPIO_MODE_INPUT);
gpio_config(DVP_PCLK_PIN, GPIO_MODE_INPUT);
// 2. 配置DMA:双缓冲模式,避免撕裂
dma_config(DMA_CHANNEL_0, DVP_DATA_PORT, frame_buffer_a,
FRAME_SIZE, DMA_MODE_CIRCULAR);
dma_config(DMA_CHANNEL_1, DVP_DATA_PORT, frame_buffer_b,
FRAME_SIZE, DMA_MODE_CIRCULAR);
// 3. 配置中断:VSYNC触发帧同步
nvic_enable_irq(DVP_VSYNC_IRQn);
}
这里有个坑:DVP的像素时钟PCLK,频率不能太高。我曾经在STM32H7上跑DVP,PCLK设到54MHz,结果DMA根本来不及搬运,数据丢得一塌糊涂。后来降到27MHz,稳了。
4.1.2 MIPI驱动要点
MIPI驱动,比DVP复杂一个数量级。你需要配置:
- PHY层:差分阻抗100Ω,摆幅、预加重、均衡
- 协议层:LP(低功耗)和HS(高速)模式切换
- CSI-2协议:数据包格式、虚拟通道、ECC校验
我个人的经验是,MIPI调试最怕「花屏」。花屏的原因很多,但80%是时钟问题。你想想看,MIPI的时钟是DDR(双沿采样),稍微有点抖动,数据就错位。
给个调试建议:先用示波器看MIPI的时钟和数据眼图。眼图清晰、开口大,再谈软件配置。否则,调三天三夜也白搭。
// MIPI初始化(简化版)
void mipi_init(void) {
// 1. 配置D-PHY
dphy_set_voltage(200); // 200mV摆幅
dphy_set_preemphasis(2); // 预加重2级
// 2. 配置CSI-2
csi2_set_lane_count(2); // 2-lane模式
csi2_set_virtual_channel(0);
csi2_enable_ecc(); // 开启ECC校验
// 3. 配置DMA
dma_config(DMA_CHANNEL_2, CSI2_DATA_PORT, frame_buffer,
FRAME_SIZE, DMA_MODE_CIRCULAR);
}
4.2 色彩空间转换:Bayer to RGB
传感器出来的原始数据,是Bayer格式。说白了,每个像素只记录一种颜色——R、G、B中的一个。但人眼需要全彩色,所以必须「猜」出缺失的颜色。这个过程叫去马赛克(Demosaicing)。
最常见的Bayer模式是RGGB。也就是第一行:R、G、R、G……第二行:G、B、G、B……
最简单的算法是双线性插值。比如,要算一个红色像素位置的绿色值,就取它上下左右四个绿色像素的平均值。
// 双线性插值:计算R像素位置的G值
uint8_t interpolate_g_at_r(uint8_t *bayer, int x, int y, int width) {
uint8_t g_top = bayer[(y-1)*width + x];
uint8_t g_bottom = bayer[(y+1)*width + x];
uint8_t g_left = bayer[y*width + (x-1)];
uint8_t g_right = bayer[y*width + (x+1)];
return (g_top + g_bottom + g_left + g_right) / 4;
}
但双线性插值有个问题——边缘模糊。你想想看,如果图像里有一根细血管,插值后可能就糊了。所以,实际项目中我更喜欢用边缘导向插值。
边缘导向插值的思路是:先判断当前像素是水平边缘还是垂直边缘,然后只沿着边缘方向插值。这样能保留细节。
核心要点:内窥镜图像对细节要求极高。我建议至少用边缘导向插值,如果算力允许,可以用自适应插值甚至深度学习去马赛克。
色彩空间转换还有一个重要步骤——白平衡。内窥镜的光源是冷光源,色温偏高,图像偏蓝。所以,转换完RGB后,要乘以一个白平衡系数。
// 白平衡校正
void white_balance(uint8_t *rgb, int size, float r_gain, float g_gain, float b_gain) {
for (int i = 0; i < size; i += 3) {
rgb[i] = (uint8_t)(rgb[i] * r_gain); // R
rgb[i+1] = (uint8_t)(rgb[i+1] * g_gain); // G
rgb[i+2] = (uint8_t)(rgb[i+2] * b_gain); // B
}
}
这个增益怎么算?我习惯用「灰度世界法」——假设整幅图像的RGB平均值相等。当然,更精确的做法是用色卡标定。
4.3 坏点校正算法
传感器不可能完美无缺。每个传感器都有坏点——要么一直亮(白点),要么一直暗(黑点)。内窥镜里,坏点会被医生误认为是病灶,所以必须校正。
坏点校正分两步:检测和替换。
4.3.1 坏点检测
检测方法很多,我推荐一种实用的:中值滤波差值法。
思路很简单:对每个像素,计算它和周围8个像素的中值的差值。如果差值超过阈值,就认为是坏点。
// 坏点检测
int is_bad_pixel(uint8_t *bayer, int x, int y, int width, int threshold) {
uint8_t neighbors[8];
// 收集周围8个像素
neighbors[0] = bayer[(y-1)*width + (x-1)];
neighbors[1] = bayer[(y-1)*width + x];
// ... 省略其他
uint8_t median = get_median(neighbors, 8);
int diff = abs(bayer[y*width + x] - median);
return (diff > threshold);
}
阈值怎么设?我建议先做一次全黑和全白标定。全黑下,坏点会特别亮;全白下,坏点会特别暗。取两者的平均值作为阈值。
4.3.2 坏点替换
检测到坏点后,用周围像素的中值或均值替换。我习惯用中值,因为中值对边缘更友好。
// 坏点替换:用中值替换
void correct_bad_pixel(uint8_t *bayer, int x, int y, int width) {
uint8_t neighbors[8];
// 收集周围8个像素
neighbors[0] = bayer[(y-1)*width + (x-1)];
// ... 省略其他
bayer[y*width + x] = get_median(neighbors, 8);
}
这里有个性能问题:如果坏点很多(比如传感器老化),逐像素检测会拖慢帧率。我建议用查表法——在初始化时,把坏点坐标存到一个表里,运行时直接查表替换。
总结
这一章的内容,说白了就是三个字:采、转、校。
- 采:DVP/MIPI接口,把传感器的原始数据搬进内存
- 转:Bayer to RGB,把单色猜成彩色
- 校:坏点校正,把瑕疵抹掉
每一步都有坑,但每一步也都有成熟的解决方案。我个人觉得,最难的是MIPI的物理层调试,最耗时的是坏点标定。但只要耐心,总能搞定。
下一章,我们会聊图像增强——如何让内窥镜图像更清晰、更真实。到时候见。