3、色彩空间与通道操作:BGR与RGB转换、灰度图、HSV色彩空间、通道拆分与合并

说到色彩空间,我刚开始接触OpenCV时就被坑过一次。那时候我以为OpenCV读进来的图片就是RGB,结果用matplotlib一显示,颜色全乱了。后来才明白——OpenCV默认用的是BGR,不是我们熟悉的RGB。这个坑,几乎每个初学者都会踩一次。

3.1 BGR与RGB:OpenCV的“反直觉”设计

你想想看,我们平时在屏幕上看到的颜色,红绿蓝(RGB)是标准。但OpenCV偏偏反着来,用蓝绿红(BGR)。为什么?

其实这跟历史有关。早期的一些图像采集设备和显示硬件,底层存储顺序就是BGR。OpenCV为了兼容这些硬件,就沿用了这个习惯。说白了,就是个历史遗留问题。

那怎么转换?很简单:

import cv2
import matplotlib.pyplot as plt

# 读取图片,默认是BGR
img_bgr = cv2.imread('example.jpg')

# 方法一:用cv2.cvtColor转换
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)

# 方法二:手动翻转通道(不推荐,容易出错)
# img_rgb = img_bgr[:, :, ::-1]

# 显示对比
plt.subplot(1, 2, 1)
plt.imshow(img_bgr)  # 颜色会偏蓝
plt.title('BGR (直接显示)')

plt.subplot(1, 2, 2)
plt.imshow(img_rgb)  # 颜色正常
plt.title('RGB (转换后)')
plt.show()
我的习惯:每次用OpenCV读图后,如果后续要用matplotlib显示,我第一件事就是转成RGB。不然调试时看着蓝汪汪的图,心态容易崩。

3.2 灰度图:丢掉颜色,保留结构

灰度图就是把彩色图像变成黑白。每个像素只有一个值,范围0(黑)到255(白)。

为什么要用灰度图?

  • 计算量小:一个通道 vs 三个通道,速度差三倍
  • 很多算法只认灰度:比如边缘检测、直方图均衡化,都是基于灰度设计的
  • 去除颜色干扰:有些场景下颜色反而是噪声,比如文字识别
# 转灰度图
img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)

# 或者直接读成灰度
# img_gray = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)

print(f'原图尺寸: {img_bgr.shape}')   # (H, W, 3)
print(f'灰度图尺寸: {img_gray.shape}') # (H, W)
注意:灰度图是二维数组,不是三维。很多人刚学时以为灰度图是(H, W, 1),其实不是。shape是(H, W),少了一个维度。

3.3 HSV色彩空间:更符合人眼直觉

RGB虽然直观,但有个问题——它把颜色和亮度混在一起了。你想调亮一点,结果颜色也跟着变了。

HSV就解决了这个问题。它把颜色拆成三个分量:

  • H(Hue,色调):0-180,代表颜色种类,比如红色、绿色
  • S(Saturation,饱和度):0-255,颜色鲜艳程度
  • V(Value,明度):0-255,亮度

我在做颜色识别项目时,HSV简直是神器。比如识别红色交通灯,用RGB你得考虑R通道高、G和B通道低,但光照一变就失效。用HSV,直接锁定H在0-10和170-180之间,S和V设个阈值,稳得很。

# BGR转HSV
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)

# 提取红色区域(红色在HSV中分布在0-10和170-180)
lower_red1 = np.array([0, 50, 50])
upper_red1 = np.array([10, 255, 255])
lower_red2 = np.array([170, 50, 50])
upper_red2 = np.array([180, 255, 255])

mask1 = cv2.inRange(img_hsv, lower_red1, upper_red1)
mask2 = cv2.inRange(img_hsv, lower_red2, upper_red2)
mask = cv2.bitwise_or(mask1, mask2)

# 提取红色区域
result = cv2.bitwise_and(img_bgr, img_bgr, mask=mask)
我曾经踩过的坑:OpenCV中H的范围是0-180,不是0-360。如果你用其他库的HSV范围(比如0-360),直接套进来会出问题。记得除以2。

3.4 通道拆分与合并:玩转颜色分量

有时候我们需要单独处理某个颜色通道。比如想增强蓝色,或者把红色通道置零。这时候就要用到通道拆分。

# 方法一:用cv2.split(推荐)
b, g, r = cv2.split(img_bgr)

# 方法二:用numpy索引(速度更快)
# b = img_bgr[:, :, 0]
# g = img_bgr[:, :, 1]
# r = img_bgr[:, :, 2]

# 修改通道:把红色通道置零
img_no_red = img_bgr.copy()
img_no_red[:, :, 2] = 0  # 红色通道归零

# 合并通道
img_merged = cv2.merge([b, g, r])

你可能会问:用split还是用numpy索引?

我个人习惯:小项目用split,代码更清晰;大项目用numpy索引,速度快。cv2.split会创建三个新的数组,内存开销大。numpy索引只是视图,不复制数据。

3.5 实战:颜色提取与替换

来,我们做个综合练习。把图片中的蓝色物体变成红色。

import cv2
import numpy as np

# 读取图片
img = cv2.imread('objects.jpg')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# 定义蓝色范围(HSV)
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([130, 255, 255])

# 创建掩码
mask = cv2.inRange(hsv, lower_blue, upper_blue)

# 把蓝色区域变成红色
img_result = img.copy()
img_result[mask > 0] = [0, 0, 255]  # BGR格式,红色是(0,0,255)

# 显示结果
cv2.imshow('Original', img)
cv2.imshow('Result', img_result)
cv2.waitKey(0)
cv2.destroyAllWindows()
小技巧:如果你不确定颜色的HSV范围,可以用cv2.cvtColor把单个像素转成HSV看看。比如蓝色(255,0,0)在BGR中,转成HSV后H值大约在120左右。

3.6 本章总结

色彩空间和通道操作,是图像处理的基础中的基础。记住几个要点:

  1. OpenCV默认BGR,显示前记得转RGB
  2. 灰度图是二维数组,不是三维
  3. HSV适合做颜色识别,H范围0-180
  4. 通道拆分用numpy索引更快,用split更清晰

嗯,这些概念看起来简单,但实际项目中处处都用得到。下一章我们会讲图像滤波,到时候这些色彩空间的知识还会用到。先消化好今天的内容,动手敲一遍代码,比看十遍都管用。