2. 图像基础与色彩空间
各位同学,欢迎来到第二章。这一章我们聊点基础,但千万别小看它。我在工业现场摸爬滚打这些年,见过太多因为色彩空间搞错、分辨率没算对,导致整个检测项目翻车的案例。说白了,图像预处理的第一步,就是搞清楚你手里的图像到底是个什么东西。
你想想看,相机拍下来的那张图,在计算机眼里是什么?不是一张漂亮的照片,而是一堆数字。这一章,我们就来拆解这堆数字。
1. 像素:图像的最小单元
像素,英文叫 pixel,是 picture element 的缩写。一张图像,本质上就是一个二维像素矩阵。每个像素就是一个点,这个点有它的坐标 (x, y) 和颜色值。
我在项目中遇到过一件事:一个做 PCB 缺陷检测的同事,把 500 万像素的相机换成了 2000 万像素,结果检测精度没提升多少,处理时间却翻了三倍。为什么?因为他没搞清楚,像素数量翻倍,数据量可不是线性增长——是四倍。
核心概念:
- 像素坐标:通常左上角为原点 (0,0),x 轴向右,y 轴向下
- 像素值:灰度图是 0-255 的整数,彩色图是三个通道的向量
- 像素深度:8 位、16 位、32 位浮点,决定了图像的精度
2. 分辨率:图像的精细程度
分辨率这个词,大家天天听。但工业视觉里的分辨率,和手机屏幕上的分辨率,侧重点不太一样。
工业上我们更关心的是 空间分辨率——也就是每个像素对应实际物理世界的多大尺寸。单位通常是 mm/pixel 或 μm/pixel。
| 分辨率类型 | 定义 | 工业应用示例 |
|---|---|---|
| 图像分辨率 | 图像的总像素数(宽×高) | 1920×1080、2592×1944 |
| 空间分辨率 | 每个像素对应的物理尺寸 | 0.05 mm/pixel |
| 检测精度 | 能检测到的最小缺陷尺寸 | 通常为 3-5 个像素 |
我的经验:选相机时,先算空间分辨率。比如你要检测 0.1mm 的划痕,按 3 个像素算,空间分辨率至少要 0.033 mm/pixel。再根据视场大小反推需要的相机像素数。这个公式我用了十年,从来没出过问题。
3. RGB 色彩空间
RGB 是最常见的色彩模型。红、绿、蓝三个通道,每个通道 0-255,组合起来就是 1600 多万种颜色。听起来很美好,对吧?
但在工业视觉里,RGB 有个大问题——它受光照影响太大了。你想想看,同一个工件,上午的阳光和下午的灯光下,RGB 值能差出好几十。我早期做手机外壳划痕检测时,就因为这个吃了大亏。
import cv2
import numpy as np
# 读取图像
img = cv2.imread('pcb_board.jpg')
# 分离 RGB 通道(OpenCV 默认是 BGR 顺序)
b, g, r = cv2.split(img)
# 查看某个像素的 RGB 值
pixel = img[100, 200] # 第100行,第200列
print(f'BGR: {pixel}') # 输出 [B, G, R]
注意:OpenCV 读取图像默认是 BGR 顺序,不是 RGB。很多新手在这里翻车。显示时要用 cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 转换。
4. HSV 色彩空间
HSV 是解决 RGB 光照敏感问题的利器。它把颜色分解成三个分量:
- H(Hue,色调):0-180,代表颜色的种类,比如红色、蓝色
- S(Saturation,饱和度):0-255,颜色的纯度
- V(Value,明度):0-255,颜色的亮度
为什么 HSV 在工业里好用?因为 H 通道对光照变化不敏感。你只需要在 H 通道上设阈值,就能稳定地分割出目标区域。
我记得有一次做药片颜色分拣,不同批次的药片因为包衣厚度不同,RGB 值差异很大。但转到 HSV 后,H 值几乎不变。用 H 通道做阈值分割,准确率从 85% 直接干到了 99.5%。
import cv2
import numpy as np
# 读取图像
img = cv2.imread('pill.jpg')
# BGR 转 HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 提取 H 通道
h = hsv[:, :, 0]
# 设定红色范围(OpenCV 中红色 H 值在 0-10 和 170-180)
lower_red = np.array([0, 50, 50])
upper_red = np.array([10, 255, 255])
mask1 = cv2.inRange(hsv, lower_red, upper_red)
lower_red2 = np.array([170, 50, 50])
upper_red2 = np.array([180, 255, 255])
mask2 = cv2.inRange(hsv, lower_red2, upper_red2)
# 合并两个红色区域
mask = cv2.bitwise_or(mask1, mask2)
避坑指南:OpenCV 中 H 的范围是 0-180,不是 0-360。这是为了用 uint8 存储。另外,红色的 H 值在 0 和 180 两端,需要做区间合并。我曾经因为这个 bug 排查了一整个下午。
5. 灰度图
灰度图,就是只有亮度信息,没有颜色信息的图像。每个像素一个值,0 是纯黑,255 是纯白。
为什么工业视觉里经常用灰度图?两个原因:
- 数据量小:一张灰度图只有彩色图的 1/3 大小
- 算法简单:很多检测算法(边缘检测、模板匹配)在灰度图上跑得更快、更稳定
灰度化的方法有好几种,最常用的是加权平均法:
import cv2
# 读取彩色图
img = cv2.imread('gear.jpg')
# 转灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 加权公式:Gray = 0.299*R + 0.587*G + 0.114*B
# 这个权重是根据人眼对绿色最敏感设计的
工业场景建议:如果检测对象颜色信息不重要(比如尺寸测量、缺陷检测),直接用灰度图。我做过一个轴承外径测量项目,灰度图处理速度比彩色图快 3 倍,精度完全一样。
6. 二值图
二值图,就是只有黑白两种颜色的图像。每个像素要么是 0(黑),要么是 255(白)。
二值化是图像分割的基石。怎么做?设定一个阈值,大于阈值的变白,小于阈值的变黑。就这么简单。
但阈值怎么选?这就讲究了。
| 二值化方法 | 原理 | 适用场景 |
|---|---|---|
| 全局阈值 | 整个图像用一个固定阈值 | 光照均匀、背景单一 |
| Otsu 阈值 | 自动计算最佳阈值 | 双峰直方图 |
| 自适应阈值 | 每个像素用局部邻域计算阈值 | 光照不均匀 |
import cv2
# 读取灰度图
gray = cv2.imread('gear.jpg', cv2.IMREAD_GRAYSCALE)
# 全局阈值
_, binary_global = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# Otsu 阈值
_, binary_otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 自适应阈值
binary_adaptive = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2
)
我曾经踩过的坑:做金属表面缺陷检测时,工件表面有反光,全局阈值死活分不开。后来换成自适应阈值,配合高斯权重,效果立竿见影。记住:工业现场的光照永远不是理想均匀的。
7. 色彩空间转换实战
最后,我们把这些知识串起来。一个典型的工业视觉预处理流程是这样的:
- 读取图像(BGR)
- 转 HSV 做颜色分割
- 转灰度图做边缘检测
- 二值化提取目标区域
- 形态学操作去噪
import cv2
import numpy as np
def preprocess_for_defect_detection(image_path):
# 1. 读取图像
img = cv2.imread(image_path)
# 2. 转 HSV,提取感兴趣颜色区域
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower = np.array([35, 50, 50]) # 绿色范围
upper = np.array([85, 255, 255])
mask_color = cv2.inRange(hsv, lower, upper)
# 3. 转灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 4. 二值化
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 5. 合并掩码
result = cv2.bitwise_and(binary, binary, mask=mask_color)
return result
嗯,这一章的内容就到这里。像素、分辨率、RGB、HSV、灰度图、二值图——这些概念看起来简单,但每一个都是工业视觉的基石。你想想看,如果连图像的基本构成都不清楚,后面的目标检测、缺陷识别又从何谈起?