OpenCV 图像基础与预处理

本文系统讲解 OpenCV 4.x 的图像基础与预处理,回答图像在内存中怎么表示、色彩空间怎么选、几何变换用哪种插值、直方图与 CLAHE 如何提升对比度等实战问题。覆盖 BGR/RGB 陷阱、resize 插值方法、仿射与透视变换、滤波去噪、Canny 边缘与形态学,并给出完整预处理管线、直方图可视化与 PyTorch 张量衔接三个可运行示例,最后总结权衡与常见坑。

引言

无论下游模型多先进,输入永远是像素。预处理做错,模型再好也白搭。OpenCV 是 CV 工程里最常用的图像处理库,它用 numpy.ndarray 表示图像,读写快、算子全、跨语言。但它也有几个反直觉的默认行为:通道顺序是 BGR 而非 RGB、cv2.resize 的默认插值不是最省事的那个、cv2.imread 遇到不存在的路径返回 None 而不是抛异常。

本文按「表示 → 色彩空间 → 几何变换 → 对比度 → 滤波 → 边缘」的顺序拆解 OpenCV 4.x 的核心 API,并给出可直接运行的三段代码。目标是把「读图到张量」这段最容易被忽视的链路讲透。

目录

  1. 图像在内存中的表示
  2. 读写与色彩空间
  3. 几何变换
  4. 直方图与对比度增强
  5. 滤波与去噪
  6. 边缘检测与形态学
  7. 完整预处理管线
  8. 直方图可视化
  9. 与 PyTorch 的衔接
  10. 关键 API 汇总
  11. 权衡取舍
  12. 常见坑清单
  13. 小结

1. 图像在内存中的表示

OpenCV 读入的图像是一个三维 numpy.ndarray,形状为 (H, W, C),即高度、宽度、通道数。几个必须记住的性质:

  • dtype 通常是 uint8,取值范围 0 到 255。做算术运算前要转 float32,否则会溢出回绕。
  • 通道顺序是 BGR,不是 RGB。这是历史原因,也是新手第一大坑。
  • 灰度图是二维 (H, W),没有通道维。
  • 图像是行优先存储,img[y, x] 先 y 后 x,与数学里的 (x, y) 相反。

一个像素的访问与修改示例:

import cv2
import numpy as np

img = cv2.imread("cat.jpg")          # 返回 BGR,dtype=uint8
print(img.shape, img.dtype)          # (H, W, 3) uint8

h, w = img.shape[:2]
px = img[100, 200]                   # 第 100 行、第 200 列的 BGR 值
img[100, 200] = [255, 0, 0]          # 写为蓝色(BGR)

# 算术运算前先转 float,避免 uint8 溢出
bright = np.clip(img.astype(np.float32) + 30, 0, 255).astype(np.uint8)

注意最后一行:如果直接 img + 30,uint8 会在 255 处回绕成小值,画面出现黑斑。这是预处理里最隐蔽的 bug 之一。

2. 读写与色彩空间

2.1 BGR 与 RGB

cv2.imread 读出来是 BGR,而 PIL、matplotlib、torchvision 都按 RGB 处理。两者混用会导致颜色错乱(红蓝互换)。转换方式:

rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

从 PIL 读入的图像是 RGB,若要交给 OpenCV 处理,先 np.array(pil_img)[:, :, ::-1] 或 cv2.cvtColor 转成 BGR。

2.2 灰度与 HSV

  • 灰度:单通道,亮度信息,用于边缘、阈值、形态学。
  • HSV:色相(Hue)、饱和度(Saturation)、明度(Value)。做颜色分割时比 RGB 更稳,因为光照变化主要影响 V 通道。
  • LAB:感知均匀的色彩空间,L 是亮度,a/b 是色度。做颜色校正与色差计算时更符合人眼。
  • YCrCb:亮度与色度分离,常用于肤色检测与人脸。

颜色分割的典型写法:

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 提取红色(HSV 中红色跨 0 度,需要两段区间)
mask1 = cv2.inRange(hsv, (0, 70, 50), (10, 255, 255))
mask2 = cv2.inRange(hsv, (170, 70, 50), (180, 255, 255))
mask = cv2.bitwise_or(mask1, mask2)

2.3 色彩空间选择建议

色彩空间用途注意
BGROpenCV 默认读写与 RGB 库混用需转换
RGB深度学习输入PIL/torchvision 默认
GRAY边缘、阈值、形态学丢失颜色信息
HSV颜色分割、跟踪H 通道在红色处环绕
LAB颜色校正、色差转换稍慢
YCrCb肤色检测光照鲁棒

3. 几何变换

3.1 resize 与插值

cv2.resize 是最常用的预处理。插值方法的选择直接影响质量与速度:

  • INTER_NEAREST:最近邻,最快,有锯齿,适合标签图(掩码)缩放,保证像素值不插值出非法类别。
  • INTER_LINEAR:双线性,默认,速度质量平衡,适合放大。
  • INTER_AREA:区域重采样,适合缩小,能避免摩尔纹。
  • INTER_CUBIC:双三次,质量好但慢,适合放大。
  • INTER_LANCZOS4:质量最好,最慢。
small = cv2.resize(img, (224, 224), interpolation=cv2.INTER_AREA)   # 缩小
big = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)  # 放大
mask_small = cv2.resize(mask, (224, 224), interpolation=cv2.INTER_NEAREST)  # 掩码

关键点:缩放掩码与标签图必须用最近邻,否则会出现 0.5 这种不存在的类别。

3.2 仿射变换

仿射变换保持平行线,用 2×3 矩阵描述,可实现旋转、平移、缩放、错切。用 cv2.getRotationMatrix2D 生成旋转矩阵:

h, w = img.shape[:2]
M = cv2.getRotationMatrix2D((w / 2, h / 2), angle=30, scale=1.0)
rotated = cv2.warpAffine(img, M, (w, h))

3.3 透视变换

透视变换用 3×3 矩阵,可把倾斜拍摄的平面矫正为正视,例如车牌、文档、白板。先找四个角点,再映射到目标矩形:

src = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])
dst = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])
M = cv2.getPerspectiveTransform(src, dst)
warped = cv2.warpPerspective(img, M, (300, 300))

透视矫正是 OCR 前处理的标准步骤,能显著提升字符识别率。

4. 直方图与对比度增强

4.1 直方图计算

直方图统计各灰度级的像素数,是判断图像曝光与对比度的利器。

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hist = cv2.calcHist([gray], [0], None, [256], [0, 256])  # 256 个 bin

若直方图集中在低端,说明整体偏暗;集中在中间一小段,说明对比度低。

4.2 直方图均衡与 CLAHE

全局直方图均衡 cv2.equalizeHist 简单但容易放大噪声、在局部过曝。CLAHE(对比度受限自适应直方图均衡)分块均衡并限制对比度,效果更自然,是医学与低照度增强的常用手段。

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(gray)   # 注意:只接受单通道

clipLimit 越大增强越强,但噪声也越明显;tileGridSize 越小越局部。实战常用 clipLimit=2.0 到 4.0、tile=8×8。

5. 滤波与去噪

滤波器适用噪声特点
均值滤波高斯噪声模糊边缘
高斯滤波高斯噪声可调核,边缘稍糊
中值滤波椒盐噪声保边,非线性
双边滤波混合噪声保边最好,最慢
gauss = cv2.GaussianBlur(img, (5, 5), sigmaX=1.0)
median = cv2.medianBlur(img, 5)          # 去椒盐噪点
bilateral = cv2.bilateralFilter(img, 9, 75, 75)  # 保边平滑

去噪要克制:过度滤波会抹掉小目标与细纹理,反而降低检测性能。

6. 边缘检测与形态学

Canny 是经典边缘检测器,双阈值控制边缘的强与弱:

edges = cv2.Canny(gray, threshold1=100, threshold2=200)

形态学操作用于去噪与形状分析:

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
opened = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)   # 先腐蚀后膨胀,去小噪点
closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)  # 先膨胀后腐蚀,补小孔洞

掩码后处理里,开运算去毛刺、闭运算填空洞,几乎必用。

7. 完整预处理管线

下面把上述步骤串成一条从磁盘到模型输入的管线,覆盖读图、色彩转换、缩放、增强与归一化。

import cv2
import numpy as np

def preprocess(path, size=(224, 224), use_clahe=True):
    img = cv2.imread(path)
    if img is None:
        raise FileNotFoundError(path)          # 显式报错,避免 None 传播
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 统一为 RGB

    # 等比例缩放后中心裁剪,避免拉伸变形
    h, w = img.shape[:2]
    scale = max(size[0] / h, size[1] / w)
    nh, nw = int(round(h * scale)), int(round(w * scale))
    img = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
    top = (nh - size[0]) // 2
    left = (nw - size[1]) // 2
    img = img[top:top + size[0], left:left + size[1]]

    # 可选:在亮度通道做 CLAHE,提升低照度鲁棒性
    if use_clahe:
        lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)
        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
        lab[:, :, 0] = clahe.apply(lab[:, :, 0])
        img = cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)

    # 归一化:与预训练模型一致
    x = img.astype(np.float32) / 255.0
    mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
    std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
    x = (x - mean) / std
    x = np.transpose(x, (2, 0, 1))              # HWC -> CHW
    return x

tensor = preprocess("cat.jpg")
print(tensor.shape, tensor.dtype)  # (3, 224, 224) float32

这条管线做了三件容易漏的事:等比例缩放加中心裁剪(保持长宽比)、在 LAB 亮度通道做 CLAHE(不破坏颜色)、用与 ImageNet 预训练一致的均值方差归一化。

8. 直方图可视化

调参时把直方图画出来,能快速判断曝光与对比度问题。

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread("cat.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

hist = cv2.calcHist([gray], [0], None, [256], [0, 256]).ravel()
equalized = cv2.equalizeHist(gray)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(gray)

plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(hist, color="black")
plt.title("gray histogram")
plt.subplot(1, 2, 2)
plt.hist(equalized.ravel(), bins=256, range=(0, 256), color="gray")
plt.title("after equalize")
plt.tight_layout()
plt.savefig("hist.png")

print("mean before:", gray.mean(), "after CLAHE:", clahe.mean())

对比均衡前后的直方图,可以直观看到动态范围是否被拉开。

9. 与 PyTorch 的衔接

预处理最后的 HWC -> CHW 与归一化,可以直接交给 torchvision 的 transform,避免手写错误:

import torch
import numpy as np
import cv2
from torchvision import transforms

img = cv2.cvtColor(cv2.imread("cat.jpg"), cv2.COLOR_BGR2RGB)

tf = transforms.Compose([
    transforms.ToPILImage(),
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),                       # 自动 HWC->CHW 并除以 255
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225]),
])

tensor = tf(img).unsqueeze(0)   # (1, 3, 224, 224)
print(tensor.shape, tensor.dtype)

工程上建议训练与线上使用同一段预处理代码,最好把它抽成独立模块,两边都 import,从根上杜绝不一致。

10. 关键 API 汇总

需求API备注
读图cv2.imread返回 BGR,失败返回 None
写图cv2.imwriteJPEG 质量参数 0 到 100
色彩转换cv2.cvtColor转换码常量区分方向
缩放cv2.resize掩码用 INTER_NEAREST
旋转cv2.getRotationMatrix2D + warpAffine2×3 矩阵
透视getPerspectiveTransform + warpPerspective3×3 矩阵
直方图cv2.calcHist返回列向量
自适应均衡cv2.createCLAHE单通道
去噪medianBlur / bilateralFilter保边选双边
边缘cv2.Canny双阈值
形态学morphologyEx开闭运算

11. 权衡取舍

  • 质量与速度:INTER_CUBIC、双边滤波质量好但慢;实时管线优先 INTER_LINEAR 与高斯。
  • 增强与失真:CLAHE 提升低照度对比度,但过强会放大噪声、改变颜色分布,可能让模型在正常光照下变差。
  • 统一与灵活:线上固定一套预处理最稳,但不同来源(手机、监控、扫描件)差异大时,可能需要按来源分支处理。
  • 预处理位置:放 CPU 简单但可能成瓶颈,放 GPU 快但要重写算子;通常图像解码与几何变换在 CPU,归一化与张量转换在 GPU。

12. 常见坑清单

  • BGR 与 RGB 混淆:OpenCV 读入交给 torchvision 前忘记 cvtColor,红蓝互换。
  • cv2.imread 返回 None:路径错误或格式不支持时不报错,后续代码在 None 上崩溃。
  • 中文路径失败:cv2.imread 对非 ASCII 路径支持差,需 cv2.imdecode(np.fromfile(...))。
  • uint8 溢出:亮度加减不做 float 转换,出现回绕黑斑。
  • 掩码用双线性插值:缩小分割标签时插值出非法类别。
  • 拉伸变形:直接 resize 到方形而不保持长宽比,目标被拉扁。
  • CLAHE 用在彩色图上:必须只作用于亮度通道,否则颜色失真。
  • 训练与线上预处理不一致:一边 RGB 一边 BGR、归一化参数不同。
  • 忽略 EXIF 方向:手机图带旋转信息,读入后方向错误。

13. 小结

OpenCV 的预处理链路可以概括为:读图(BGR)→ 转 RGB → 保持长宽比缩放裁剪 → 可选 CLAHE 增强 → 归一化 → HWC 转 CHW。每一步都有默认行为和陷阱,最稳妥的做法是把预处理封装成一个函数,训练与线上共用。记住三条铁律:色彩空间要统一、掩码缩放用最近邻、归一化参数与预训练模型一致。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机视觉」更多文章

  1. 检测与分割的评估指标
  2. 多模态视觉语言模型
  3. 3D 视觉:点云与深度估计