3. 图像预处理基础(上):图像缩放、裁剪与仿射变换

各位同学,欢迎来到第三章。前两章我们把嵌入式视觉的硬件选型和工具链捋了一遍,从这章开始,咱们正式碰图像处理算法本身。

图像预处理,说白了就是给原始图像“洗个澡、理个发”,让它变成算法能消化的样子。我见过不少项目,模型结构选得挺好,数据集也标得认真,结果一上板子效果稀烂——查到最后,往往是预处理这步没做好。

这一章我们先讲三个最基础的操作:缩放、裁剪、旋转。别觉得简单,这里面的坑,一个比一个深。

图像预处理基础(上)核心操作 原始图像 图像缩放 (Resize) 最近邻插值 双线性插值 双三次插值 裁剪与ROI提取 旋转与仿射变换 三个操作常组合使用:先裁剪ROI → 缩放到模型输入尺寸 → 必要时做旋转校正

3.1 图像缩放算法

嵌入式视觉里,缩放可能是调用最频繁的操作。模型输入尺寸通常是固定的,比如 224x224、416x416、640x640。你摄像头采集回来的图可能是 1920x1080,不缩放根本塞不进网络。

缩放的核心问题就一个:新像素的值从哪来? 不同的插值方法,决定了这个问题的答案。

3.1.1 最近邻插值

这是最粗暴的方法。新像素的值,直接取原图中离它最近的像素值。

举个例子:原图 4x4,要缩到 8x8。新图 (0,0) 对应原图 (0,0);新图 (1,0) 对应原图 (0.5,0),四舍五入后还是 (0,0)。说白了,就是“就近复制”。

优点: 计算量极小,不产生新颜色值。适合像素风格、调色板图像、或者对速度要求极高的场景。

缺点: 放大后出现明显锯齿(马赛克效应),边缘不平滑。

我的经验: 在嵌入式设备上做实时显示预览时,我偶尔会用最近邻。比如在 STM32 上驱动一块小 LCD,CPU 资源紧张,最近邻能省下不少算力。但如果是给算法模型做输入,我基本不用——锯齿会引入高频噪声,影响模型推理。

3.1.2 双线性插值

这是嵌入式视觉里最常用的缩放方法。它不直接取一个点,而是取目标像素在原图中对应的四个相邻像素,按距离做加权平均。

具体步骤:

  1. 计算目标像素在原图中的浮点坐标 (x, y)
  2. 找到它周围的四个整数像素点:左上 (x1,y1)、右上 (x2,y1)、左下 (x1,y2)、右下 (x2,y2)
  3. 先在 x 方向做两次线性插值,再在 y 方向做一次线性插值
// 双线性插值核心代码(C语言风格)
uint8_t bilinear_interpolate(uint8_t* src, int src_w, int src_h,
                             float x, float y, int channels) {
    int x1 = (int)x, y1 = (int)y;
    int x2 = (x1 + 1 < src_w) ? x1 + 1 : x1;
    int y2 = (y1 + 1 < src_h) ? y1 + 1 : y1;
    
    float dx = x - x1, dy = y - y1;
    float dx_inv = 1.0f - dx, dy_inv = 1.0f - dy;
    
    // 假设单通道灰度图
    float v1 = src[y1 * src_w + x1] * dx_inv + src[y1 * src_w + x2] * dx;
    float v2 = src[y2 * src_w + x1] * dx_inv + src[y2 * src_w + x2] * dx;
    return (uint8_t)(v1 * dy_inv + v2 * dy);
}

优点: 边缘平滑,计算量适中,效果在大多数场景下够用。

缺点: 会轻微模糊图像,相当于一个低通滤波器。缩放倍数越大,模糊越明显。

我在项目中做过对比:用双线性缩放到 640x480 再送入 YOLO 模型,mAP 比最近邻高约 2-3 个百分点。代价是每帧多了不到 1ms 的预处理时间——在嵌入式平台上,这个 trade-off 非常划算。

3.1.3 双三次插值

双三次插值用 16 个相邻像素 做加权平均,权重函数是一个三次多项式。它比双线性更“聪明”——不仅考虑距离,还考虑像素值的变化趋势。

数学上,它用 BiCubic 基函数计算权重:

// BiCubic 权重函数
float cubic_weight(float t) {
    float a = -0.5f;  // 常用参数
    t = fabs(t);
    if (t <= 1.0f) return (a + 2.0f)*t*t*t - (a + 3.0f)*t*t + 1.0f;
    if (t < 2.0f) return a*t*t*t - 5.0f*a*t*t + 8.0f*a*t - 4.0f*a;
    return 0.0f;
}
插值方法计算量图像质量嵌入式适用性
最近邻极低差(锯齿)实时预览、像素图
双线性低中等(轻微模糊)最常用,适合大多数模型
双三次高好(边缘锐利)算力充足时使用,如 RK3588、Jetson

注意: 双三次插值在 ARM Cortex-M 系列单片机上跑,一帧 640x480 的图可能要几十毫秒。我建议只在有硬件加速(如 NPU 内置缩放模块)或算力较强的 SoC 上使用。

3.2 图像裁剪与 ROI 提取

裁剪听起来简单——不就是取图像的一部分吗?但实际工程里,ROI(Region of Interest)提取的学问不少。

我遇到过最典型的场景:工业相机拍电路板,整张图 500 万像素,但缺陷可能只出现在某个焊盘区域。如果整图送进模型,不仅浪费算力,还会因为背景干扰降低准确率。

ROI 提取的两种常见方式:

  • 固定坐标裁剪: 已知目标区域在图像中的固定位置,直接截取。比如车牌识别中,车牌通常出现在画面下半部分。
  • 动态 ROI 提取: 先通过一个轻量级检测器或传统图像处理(如边缘检测、颜色阈值)定位目标,再裁剪。比如人脸检测中的第一级粗检。
// 固定坐标裁剪示例(OpenCV风格)
// 假设原图 1920x1080,我们只取中心 640x640 区域
int start_x = (1920 - 640) / 2;  // 640
int start_y = (1080 - 640) / 2;  // 220
cv::Mat roi = image(cv::Rect(start_x, start_y, 640, 640));

避坑指南: 我曾经在裁剪时忘了检查边界——目标检测框跑到了图像边缘外,导致程序直接崩溃。从那以后,我每次做裁剪都会加边界检查:start_x = max(0, min(start_x, src_w - roi_w))。嵌入式设备上,一个越界访问就可能让整个系统挂掉。

3.3 图像旋转与仿射变换

旋转和仿射变换,本质上是坐标映射。你有一个像素在原图的位置 (x, y),通过一个变换矩阵,算出它在目标图中的位置 (x', y')。

3.3.1 旋转矩阵

绕原点逆时针旋转 θ 角度的变换矩阵:

[ x' ]   [ cosθ  -sinθ ] [ x ]
[ y' ] = [ sinθ   cosθ ] [ y ]

但实际图像旋转通常绕中心点,所以需要三步:

  1. 将图像中心平移到原点
  2. 旋转
  3. 将原点平移回新图像中心

3.3.2 仿射变换

仿射变换是旋转、平移、缩放、剪切的组合。它保持“平行线依然平行”,但角度和长度可以改变。

一个仿射变换由 2x3 矩阵表示,需要 三对对应点 来求解:

// 仿射变换矩阵求解示例
// 原图三个点:左上(0,0)、右上(100,0)、左下(0,100)
// 目标图三个点:(10,10)、(90,20)、(15,90)
cv::Point2f src_pts[3] = {cv::Point2f(0,0), cv::Point2f(100,0), cv::Point2f(0,100)};
cv::Point2f dst_pts[3] = {cv::Point2f(10,10), cv::Point2f(90,20), cv::Point2f(15,90)};
cv::Mat affine_mat = cv::getAffineTransform(src_pts, dst_pts);
cv::Mat warped;
cv::warpAffine(image, warped, affine_mat, image.size());

嵌入式部署要点:

  • 旋转后图像会出现“黑边”区域(无像素值的位置)。通常用边界填充处理:复制边缘像素、填充固定值(如 0 或 128)、或者镜像填充。
  • 仿射变换涉及浮点运算和插值,在 MCU 上开销较大。如果只是 90°、180°、270° 的旋转,直接用像素块搬移,不要走仿射变换。
  • 我在 RK3588 上做过测试:warpAffine 处理一帧 1080p 图像大约耗时 8-12ms。如果每帧都要做,建议用硬件 RGA(Rockchip 的图像处理单元)加速。

3.4 本章小结

图像预处理不是花哨的技术,但它是嵌入式视觉系统的地基。地基不稳,上层模型再强也白搭。

三个操作的核心要点:

  • 缩放: 双线性插值是默认选择。最近邻用于速度优先,双三次用于质量优先。
  • 裁剪: 永远检查边界。动态 ROI 能大幅降低后续处理的计算量。
  • 旋转/仿射: 理解坐标映射的本质。能用查表法就别实时算浮点。