4. 图像预处理基础:色彩空间转换、缩放、ROI与归一化

图像预处理,说白了就是给AI模型“喂饭”前的准备工作。你想想看,摄像头采集的原始数据,模型根本没法直接吃——格式不对、尺寸不对、数据范围也不对。我刚开始做边缘AI项目时,就吃过这个亏:模型在PC上跑得好好的,一部署到STM32上,推理结果全乱套。查了半天,原来是图像预处理没对齐。

这一章,咱们就把这四步基础操作讲透。每一步都直接影响模型精度和推理速度,尤其是双线性插值和归一化,坑特别多。

核心要点:图像预处理是STM32边缘AI推理的“第一道关卡”。预处理做不好,后面模型再牛也白搭。

4.1 色彩空间转换:RGB888 → RGB565

STM32内部通常用RGB565格式存储图像。为什么?因为省内存。RGB888一个像素占3字节,RGB565只占2字节,直接省掉1/3。对于只有几百KB RAM的MCU来说,这很关键。

转换原理:

  • RGB888:R(8bit) + G(8bit) + B(8bit) = 24bit
  • RGB565:R(5bit) + G(6bit) + B(5bit) = 16bit

说白了就是高位截断。把R和B的高5位保留,G的高6位保留,丢掉低位。我见过有人直接移位就完事,但这样会丢失亮度信息。我个人习惯的做法是:先做低位舍入,再做移位,能保留更多细节。

// RGB888 → RGB565 转换(带舍入优化)
uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
    uint16_t r5 = (r * 31 + 128) / 255;  // 舍入到5bit
    uint16_t g6 = (g * 63 + 128) / 255;  // 舍入到6bit
    uint16_t b5 = (b * 31 + 128) / 255;  // 舍入到5bit
    return (r5 << 11) | (g6 << 5) | b5;
}

经验之谈:如果追求极致速度,可以用查表法。把256种R/B值和256种G值提前算好存成数组,转换时直接查表,速度能快3-5倍。代价是多占1KB左右的Flash。

4.2 图像缩放:双线性插值

模型输入尺寸通常是固定的,比如96×96或128×128。摄像头采集的原始图像必须缩放到这个尺寸。最常用的就是双线性插值——它比最近邻插值平滑,又比双三次插值快,正好适合MCU。

双线性插值的核心思想:目标像素的值,由它在原图中对应的四个邻近像素加权平均得到。权重由距离决定。

我记得第一次在STM32上实现双线性插值时,踩了个大坑——浮点运算太慢了。一个320×240的图像缩放到96×96,用浮点算要200多毫秒,根本没法用。后来改成定点数运算,直接降到30毫秒以内。

// 定点数双线性插值(Q16格式)
uint8_t bilinear_interpolate(uint8_t p00, uint8_t p01, 
                              uint8_t p10, uint8_t p11, 
                              uint16_t fx, uint16_t fy) {
    // fx, fy 为 Q16 定点小数,范围 0~65535
    uint16_t w00 = (65536 - fx) * (65536 - fy) >> 16;
    uint16_t w01 = fx * (65536 - fy) >> 16;
    uint16_t w10 = (65536 - fx) * fy >> 16;
    uint16_t w11 = fx * fy >> 16;
    
    uint32_t sum = p00 * w00 + p01 * w01 + p10 * w10 + p11 * w11;
    return (uint8_t)(sum >> 16);
}

避坑指南:我曾经在缩放时忘记处理边界像素——当目标像素映射到原图边缘时,邻近像素可能越界。一定要做边界检查或镜像填充,否则会出现“黑边”或“撕裂”现象。

4.3 ROI裁剪:只关注关键区域

ROI(Region of Interest)裁剪,就是只保留图像中我们关心的区域。比如人脸检测,我们只需要脸部那块,背景全是噪声。裁剪掉背景,不仅能减少计算量,还能提高模型精度。

ROI裁剪的步骤:

  1. 确定ROI的起始坐标 (x, y) 和宽高 (w, h)
  2. 检查ROI是否越界(必须做!)
  3. 从原图缓冲区中逐行拷贝像素到目标缓冲区

你想想看,如果原图是640×480,ROI只有100×100,裁剪后数据量直接减少96%。这对STM32来说,意味着推理时间从秒级降到毫秒级。

// ROI裁剪(RGB565格式)
void roi_crop(uint16_t *src, uint16_t *dst, 
              int src_w, int src_h,
              int roi_x, int roi_y, 
              int roi_w, int roi_h) {
    // 边界检查
    if (roi_x + roi_w > src_w) roi_w = src_w - roi_x;
    if (roi_y + roi_h > src_h) roi_h = src_h - roi_y;
    
    for (int y = 0; y < roi_h; y++) {
        memcpy(dst + y * roi_w, 
               src + (roi_y + y) * src_w + roi_x, 
               roi_w * sizeof(uint16_t));
    }
}

优化技巧:如果ROI位置固定(比如摄像头安装位置不变),可以提前算好偏移地址,用DMA直接搬运,CPU零开销。

4.4 数据归一化:让模型“看懂”数据

模型训练时,输入数据通常归一化到[0,1]或[-1,1]区间。但摄像头采集的是0-255的整数。如果不做归一化,模型推理时数值范围对不上,精度会大幅下降。

常见的归一化方式:

方式 公式 适用场景
Min-Max归一化 pixel / 255.0 输入范围固定为[0,255]
Z-Score归一化 (pixel - mean) / std 数据分布已知,精度要求高
定点数归一化 pixel * scale_factor >> shift MCU上避免浮点运算

我个人在STM32上最常用定点数归一化。举个例子,如果模型要求归一化到[0,1],scale_factor可以取255,shift取8,这样pixel * 255 >> 8就等价于pixel / 255.0,但全是整数运算,快得多。

我曾经踩过的坑:有一次模型在PC上精度95%,部署到STM32上直接掉到60%。查了两天,发现是归一化时用了整数除法,直接把小数部分截断了。记住:整数除法要转成定点乘法,或者先乘后除。

4.5 知识体系总览

下面这张图把本章四个预处理步骤的流程和关系画清楚了。你可以把它当作一个“预处理流水线”来理解——数据从摄像头进来,经过四步处理,最后变成模型能吃的格式。

图像预处理流水线 图像预处理流水线 摄像头原始图像 步骤1:色彩空间转换 (RGB888→RGB565) 步骤2:图像缩放 (双线性插值) 步骤3:ROI裁剪 步骤4:数据归一化 模型推理输入 关键指标 • 内存节省:33% • 速度提升:5-10x • 精度损失:<1%

4.6 性能调优要点总结

最后,我把这四步操作在STM32上的性能调优要点整理成一张表,方便你对照优化:

预处理步骤 计算瓶颈 优化方案 预期提升
色彩空间转换 浮点除法 查表法 / 定点数移位 3-5倍
双线性插值 浮点乘法 Q16定点数运算 5-8倍
ROI裁剪 逐像素拷贝 DMA搬运 / 行拷贝优化 10倍以上
数据归一化 整数除法 定点乘法 + 移位 2-3倍