2. OpenCV基础(一):图像读取、显示、保存、色彩空间转换与ROI提取

好,咱们正式开始动手。这一章是OpenCV的入门基本功,说白了就是怎么把一张图片弄进来、看一眼、改个颜色、再存回去。别小看这些操作,我做了这么多年视觉项目,90%的坑都出在最基础的读写和色彩空间上。

本章核心知识点一览

  • 图像读取:imread() 的坑与正确姿势
  • 图像显示:imshow() + waitKey() 的黄金搭档
  • 图像保存:imwrite() 的格式与质量控制
  • 色彩空间转换:BGR ↔ RGB ↔ HSV ↔ GRAY
  • 像素操作:遍历、修改、性能对比
  • ROI提取:用numpy切片搞定一切
OpenCV基础操作知识体系 图像基础操作 读取 imread 显示 imshow 保存 imwrite 色彩空间转换 IMREAD_COLOR / GRAYSCALE waitKey() 必须加 PNG/JPEG 参数 BGR→RGB→HSV→GRAY 像素操作 ROI 提取 item() / numpy索引 / 遍历 img[y1:y2, x1:x2] 切片 注意:OpenCV默认 使用BGR顺序

2.1 图像读取:imread() 的正确打开方式

先说说读取。OpenCV的 cv2.imread() 看起来简单,但坑不少。我见过太多新手直接这么写:

import cv2
img = cv2.imread('cat.jpg')  # 默认以彩色模式读取
print(type(img))  # <class 'numpy.ndarray'>
print(img.shape)  # (height, width, 3)

嗯,这里有个关键点——OpenCV默认读取的是BGR格式,不是RGB。你想想看,如果你用matplotlib直接显示,颜色就全乱了。为什么会这样?历史原因,OpenCV早期用BGR是因为Windows的BMP格式就是BGR,后来就一直沿用下来了。

我个人习惯在读取时就明确指定模式,避免后续混淆:

标志 值 说明
cv2.IMREAD_COLOR 1 默认,读取彩色图(BGR),忽略透明度
cv2.IMREAD_GRAYSCALE 0 读取为单通道灰度图
cv2.IMREAD_UNCHANGED -1 保留所有通道(包括Alpha通道)

我的经验:读取灰度图时,很多人以为返回的是二维数组,其实shape是 (h, w),没错。但如果你用 cv2.IMREAD_COLOR 读彩色图再转灰度,shape会变成 (h, w, 3) 然后才降维。直接读灰度图省一步,性能也好一点。

我曾经踩过的坑:中文路径!OpenCV的imread不支持中文路径,返回None。我当时在项目里用中文命名图片,调试了半天才发现是路径问题。解决方案:用 cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR) 绕过去。

2.2 图像显示:imshow() 与 waitKey() 的黄金搭档

显示图像,核心就两个函数:imshow() 和 waitKey()。缺了后者,窗口一闪就没了。

cv2.imshow('My Window', img)
cv2.waitKey(0)      # 等待任意按键
cv2.destroyAllWindows()  # 关闭所有窗口

这里有个细节:waitKey(0) 表示无限等待,直到你按键盘。如果填 waitKey(30),那就是等待30毫秒,常用于视频流。返回值是按键的ASCII码,可以用来做交互。

常见问题:窗口显示出来是灰色的?大概率是图像数据类型不对。imshow要求数据是uint8类型,范围0-255。如果你做了归一化变成float32,记得先乘255再转uint8。

2.3 图像保存:imwrite() 的格式与质量

保存用 cv2.imwrite(),第一个参数是文件名(同样不支持中文路径),第二个参数是图像数据。

cv2.imwrite('output.jpg', img)  # 默认JPEG质量95
cv2.imwrite('output.png', img)   # PNG无损

如果想控制质量,可以加第三个参数:

# JPEG质量控制,范围0-100,默认95
cv2.imwrite('output_high.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 95])

# PNG压缩级别,范围0-9,默认3
cv2.imwrite('output_compressed.png', img, [cv2.IMWRITE_PNG_COMPRESSION, 9])
格式 参数 说明
JPEG IMWRITE_JPEG_QUALITY 值越高质量越好,文件越大。95以上人眼看不出区别
PNG IMWRITE_PNG_COMPRESSION 值越高压缩率越大,但保存越慢。9是最大压缩
WEBP IMWRITE_WEBP_QUALITY 类似JPEG,但同等质量下文件更小

我的建议:做YOLO训练时,标注图片尽量保存为PNG。JPEG有损压缩会改变像素值,虽然人眼看不出,但模型训练时可能会引入噪声。尤其是做分割任务,PNG是标配。

2.4 色彩空间转换:BGR、RGB、HSV、GRAY

色彩空间转换是OpenCV里最常用的操作之一。核心函数就一个:cv2.cvtColor()。

# BGR 转灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# BGR 转 RGB(用于matplotlib显示)
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# BGR 转 HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

为什么HSV这么重要?因为它在做颜色分割时特别好用。BGR对光照变化敏感,而HSV把色调(H)、饱和度(S)、明度(V)分开了。我在做交通灯检测项目时,就是用HSV提取红色和绿色区域,比BGR阈值稳定得多。

转换 OpenCV标志 应用场景
BGR → GRAY COLOR_BGR2GRAY 灰度化,减少计算量
BGR → RGB COLOR_BGR2RGB 与matplotlib/PIL兼容
BGR → HSV COLOR_BGR2HSV 颜色分割、物体跟踪
BGR → LAB COLOR_BGR2LAB 颜色距离计算、图像增强

注意:OpenCV中HSV的H范围是0-179,不是0-359。S和V范围是0-255。如果你用其他库(比如matplotlib)的HSV范围,直接套用阈值会出问题。我刚开始做颜色过滤时就吃过这个亏,红色阈值怎么调都不对,后来才发现是H范围搞错了。

2.5 像素操作:读取与修改

图像在OpenCV里就是numpy数组,所以像素操作本质上就是数组索引。

# 读取某个像素(BGR顺序)
b, g, r = img[100, 200]  # 第100行,第200列

# 修改某个像素
img[100, 200] = [255, 0, 0]  # 变成蓝色

# 使用item()方法(更快)
pixel_value = img.item(100, 200, 0)  # 获取B通道值
img.itemset((100, 200, 0), 255)      # 设置B通道值

遍历像素时,千万别用Python的for循环,慢到怀疑人生。用numpy的向量化操作:

# 错误示范(慢)
for i in range(img.shape[0]):
    for j in range(img.shape[1]):
        img[i, j] = [0, 0, 255]  # 全红

# 正确示范(快)
img[:, :] = [0, 0, 255]  # 全红,一行搞定

性能对比:一张1920x1080的图片,用Python双层for循环遍历需要几百毫秒,用numpy切片只需要几毫秒。差了两个数量级。做实时处理时,记住:能用numpy就别用for。

2.6 ROI提取:用numpy切片搞定一切

ROI(Region of Interest)就是感兴趣区域。在OpenCV里提取ROI,说白了就是numpy切片。

# 提取从(100, 200)到(300, 400)的区域
roi = img[100:300, 200:400]

# 复制ROI到另一个位置
img[50:250, 50:250] = roi

这里要注意:切片返回的是视图,不是副本。修改roi会影响原图。如果需要独立副本,用 .copy():

roi_copy = img[100:300, 200:400].copy()  # 深拷贝

我的经验:做YOLO数据预处理时,经常需要从大图中裁剪出目标区域。用切片提取ROI后,记得检查边界——如果坐标超出图像范围,会报错。我一般会加个边界裁剪:x1, y1 = max(0, x1), max(0, y1),避免训练时崩掉。

2.7 综合示例:从读取到ROI的一站式操作

最后,给一个完整的示例,把今天的内容串起来:

import cv2
import numpy as np

# 1. 读取图像
img = cv2.imread('test.jpg')
if img is None:
    print("图片读取失败,检查路径")
    exit()

# 2. 显示原图
cv2.imshow('Original', img)

# 3. 转灰度并保存
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imwrite('gray_output.jpg', gray)

# 4. 提取ROI并修改
roi = img[50:200, 100:300].copy()
# 将ROI转为HSV,做颜色过滤
hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
# 假设我们要提取蓝色区域
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([130, 255, 255])
mask = cv2.inRange(hsv_roi, lower_blue, upper_blue)
# 将蓝色区域在原图上标记为红色
img[50:200, 100:300][mask > 0] = [0, 0, 255]

# 5. 显示结果
cv2.imshow('Modified', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

这个例子涵盖了读取、显示、保存、色彩空间转换、ROI提取和像素修改。你可以在自己的图片上试试,把参数调一调,看看效果。

本章小结:

  • OpenCV默认BGR,不是RGB,显示前记得转换
  • imread不支持中文路径,用imdecode绕过去
  • imshow必须搭配waitKey,否则窗口一闪而过
  • HSV的H范围是0-179,不是0-359
  • 像素操作优先用numpy切片,别用for循环
  • ROI提取用切片,需要独立副本时加.copy()