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裁剪的步骤:
- 确定ROI的起始坐标 (x, y) 和宽高 (w, h)
- 检查ROI是否越界(必须做!)
- 从原图缓冲区中逐行拷贝像素到目标缓冲区
你想想看,如果原图是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 知识体系总览
下面这张图把本章四个预处理步骤的流程和关系画清楚了。你可以把它当作一个“预处理流水线”来理解——数据从摄像头进来,经过四步处理,最后变成模型能吃的格式。
4.6 性能调优要点总结
最后,我把这四步操作在STM32上的性能调优要点整理成一张表,方便你对照优化:
| 预处理步骤 | 计算瓶颈 | 优化方案 | 预期提升 |
|---|---|---|---|
| 色彩空间转换 | 浮点除法 | 查表法 / 定点数移位 | 3-5倍 |
| 双线性插值 | 浮点乘法 | Q16定点数运算 | 5-8倍 |
| ROI裁剪 | 逐像素拷贝 | DMA搬运 / 行拷贝优化 | 10倍以上 |
| 数据归一化 | 整数除法 | 定点乘法 + 移位 | 2-3倍 |