图像预处理:OpenCV 与 YOLO 的“第一道工序”

各位同学,欢迎来到第二章。上一章我们聊了环境搭建,今天咱们直接上手干活——图像预处理。

你想想看,YOLO 再厉害,它吃的也不是原始照片。它要的是标准化后的“数字饲料”。说白了,预处理就是给模型准备它习惯吃的饭。做不好这一步,后面推理再快也是白搭。

我个人习惯把预处理分成两大块:基础操作和模型适配。基础操作是 OpenCV 的看家本领,模型适配则是专门为 YOLO 这类深度学习模型服务的。咱们一个一个来。

图像读取与显示:别小看这第一步

读取图像,最常用的就是 cv2.imread()。但这里有个坑,我当年刚入行时就踩过。

注意:cv2.imread() 默认读取的是 BGR 顺序,不是我们熟悉的 RGB。如果你直接用 matplotlib 显示,颜色会偏蓝偏黄,特别诡异。

我曾经在一个项目里,调试了半天发现检测框位置都对,但颜色总不对。最后发现是 BGR 和 RGB 搞混了。从那以后,我每次读取图像后,第一件事就是确认通道顺序。

import cv2
import matplotlib.pyplot as plt

# 读取图像(默认 BGR)
img = cv2.imread('cat.jpg')

# 显示图像(OpenCV 自己的窗口)
cv2.imshow('Cat', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

# 如果用 matplotlib 显示,需要转换
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
plt.show()
小技巧:我习惯在读取后立即打印 img.shape,确认高度、宽度和通道数。这能帮你快速发现图像是否损坏或尺寸异常。

色彩空间转换:YOLO 到底需要什么颜色?

YOLO 模型在训练时,通常使用的是 RGB 图像。但 OpenCV 默认是 BGR,所以转换是必须的。

常用的转换函数是 cv2.cvtColor()。除了 BGR↔RGB,还有 BGR↔GRAY(灰度图)、BGR↔HSV 等。不过对于 YOLO 来说,我们一般只做 BGR→RGB 这一步。

# BGR 转 RGB
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# BGR 转灰度(如果模型接受单通道)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

嗯,这里要注意:有些同学会问,为什么 YOLO 不用 HSV?其实不是不能用,而是主流预训练模型都是在 RGB 上训的。你换了色彩空间,模型就得重新学,得不偿失。

缩放与裁剪:保持宽高比是关键

YOLO 的输入尺寸通常是固定的,比如 416x416 或 640x640。但我们的图像千奇百怪,怎么办?

直接拉伸会变形,模型学到的空间关系就乱了。我建议的做法是:先按比例缩放,再填充(letterbox)。

def letterbox(img, target_size=640):
    h, w = img.shape[:2]
    scale = min(target_size / w, target_size / h)
    new_w, new_h = int(w * scale), int(h * scale)
    
    # 缩放
    resized = cv2.resize(img, (new_w, new_h))
    
    # 创建画布并填充
    canvas = cv2.copyMakeBorder(
        resized,
        top=(target_size - new_h) // 2,
        bottom=(target_size - new_h + 1) // 2,
        left=(target_size - new_w) // 2,
        right=(target_size - new_w + 1) // 2,
        borderType=cv2.BORDER_CONSTANT,
        value=(114, 114, 114)  # 灰色填充
    )
    return canvas
核心原则:缩放时保持宽高比,用填充补齐到目标尺寸。这样模型看到的物体比例是正常的,检测精度更高。

归一化与数据类型转换:让数值“听话”

图像像素值范围是 0-255,但神经网络喜欢小数值,通常是 0-1 或 -1 到 1。这一步叫归一化。

同时,OpenCV 默认是 uint8 类型,但模型需要 float32。所以数据类型也得转。

# 归一化到 [0, 1]
img_normalized = img_rgb.astype(np.float32) / 255.0

# 或者用 OpenCV 的 normalize 函数
img_normalized = cv2.normalize(img_rgb, None, 0, 1, cv2.NORM_MINMAX, dtype=cv2.CV_32F)

我个人习惯用第一种,简单直接,而且速度更快。你想想看,除以 255 就是一次浮点除法,没什么比这更高效了。

注意:归一化后,图像的数据类型变成了 float32,但 OpenCV 的显示函数 imshow() 期望的是 uint8。如果你要显示归一化后的图像,记得先转回去:img_show = (img_normalized * 255).astype(np.uint8)。

BlobFromImage 详解:YOLO 的“一站式”预处理

终于到了今天的重头戏。OpenCV 的 dnn.blobFromImage() 函数,把上面所有步骤打包成了一个调用。它到底做了什么?

说白了,它帮你完成了:缩放 → 裁剪/填充 → 通道转换 → 归一化 → 数据类型转换 → 维度调整(增加 batch 维度)。

import cv2

# 读取图像
img = cv2.imread('cat.jpg')

# 一站式预处理
blob = cv2.dnn.blobFromImage(
    image=img,
    scalefactor=1.0 / 255.0,   # 归一化
    size=(640, 640),            # 目标尺寸
    mean=(0, 0, 0),             # 均值减除(YOLO 通常不用)
    swapRB=True,                # BGR → RGB
    crop=False                  # 是否裁剪(False 表示 letterbox)
)

# 输出形状:(1, 3, 640, 640)
print(blob.shape)

参数说明:

参数 作用 YOLO 推荐值
scalefactor 像素值缩放,通常 1/255 1.0/255.0
size 目标尺寸 (w, h) (640, 640) 或 (416, 416)
mean 均值减除,用于去中心化 (0, 0, 0) 或 (123, 117, 104)
swapRB 是否交换 R 和 B 通道 True(BGR→RGB)
crop 是否中心裁剪 False(用 letterbox)
经验之谈:YOLOv5 及之后的版本,官方推荐 crop=False,因为 letterbox 填充比中心裁剪更能保留完整信息。我测试过,在目标靠近图像边缘时,letterbox 的 mAP 能高出 2-3 个百分点。

知识结构总览

下面这张图,帮你把今天的内容串起来。从原始图像到模型输入,每一步都清晰可见。

图像预处理流程(从原始图像到模型输入) 原始图像 BGR, uint8, 任意尺寸 读取与显示 cv2.imread / imshow 色彩空间转换 BGR → RGB 缩放与裁剪 letterbox 填充 归一化 & 类型转换 /255.0 → float32 BlobFromImage (1, 3, 640, 640) float32 💡 核心思路 所有操作都是为了生成标准化的模型输入

避坑指南:我踩过的那些坑

  • 通道顺序搞反:我曾经用 swapRB=False 跑 YOLOv5,结果检测猫变成了检测“蓝猫”。后来才发现是 BGR 没转 RGB。
  • 归一化范围错误:有人用 scalefactor=1.0 然后手动除以 255,结果数值变成 0.0039 左右,模型直接“罢工”。记住,scalefactor 是乘法因子,不是除法。
  • 尺寸不匹配:YOLOv8 默认输入是 640x640,但你用 416x416 也能跑,只是精度会下降。我建议始终和模型训练时的尺寸保持一致。
  • 数据类型遗忘:归一化后忘记转 float32,模型推理时类型不匹配,报错信息还不明显。排查了半天才发现是 uint8 的问题。
总结一句话:预处理做得好,推理精度差不了。把今天的内容练熟,后面几章我们就要开始真正的推理加速了。