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,稳了。

警告:DVP的走线长度要尽量相等,尤其是数据线。我见过一个项目,因为PCB布线没注意,数据线差了2cm,结果图像出现「鬼影」。后来加了一级FIFO才解决。

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);
}
提示:MIPI的LP模式可以用来传输控制命令,比如修改传感器增益。我习惯在LP模式下发送I2C命令,这样不用额外走线。

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);
}

这里有个性能问题:如果坏点很多(比如传感器老化),逐像素检测会拖慢帧率。我建议用查表法——在初始化时,把坏点坐标存到一个表里,运行时直接查表替换。

技巧:坏点表可以存储在Flash里,每次上电加载。我习惯在产线测试时生成坏点表,烧录到芯片中。这样,每台设备都有自己的「个性」校正参数。

总结

这一章的内容,说白了就是三个字:采、转、校

  • :DVP/MIPI接口,把传感器的原始数据搬进内存
  • :Bayer to RGB,把单色猜成彩色
  • :坏点校正,把瑕疵抹掉

每一步都有坑,但每一步也都有成熟的解决方案。我个人觉得,最难的是MIPI的物理层调试,最耗时的是坏点标定。但只要耐心,总能搞定。

下一章,我们会聊图像增强——如何让内窥镜图像更清晰、更真实。到时候见。