图像预处理: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) |
crop=False,因为 letterbox 填充比中心裁剪更能保留完整信息。我测试过,在目标靠近图像边缘时,letterbox 的 mAP 能高出 2-3 个百分点。
知识结构总览
下面这张图,帮你把今天的内容串起来。从原始图像到模型输入,每一步都清晰可见。
避坑指南:我踩过的那些坑
- 通道顺序搞反:我曾经用
swapRB=False跑 YOLOv5,结果检测猫变成了检测“蓝猫”。后来才发现是 BGR 没转 RGB。 - 归一化范围错误:有人用
scalefactor=1.0然后手动除以 255,结果数值变成 0.0039 左右,模型直接“罢工”。记住,scalefactor是乘法因子,不是除法。 - 尺寸不匹配:YOLOv8 默认输入是 640x640,但你用 416x416 也能跑,只是精度会下降。我建议始终和模型训练时的尺寸保持一致。
- 数据类型遗忘:归一化后忘记转
float32,模型推理时类型不匹配,报错信息还不明显。排查了半天才发现是uint8的问题。