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

各位同学,欢迎来到实战课的第二讲。

今天我们要聊的,是OpenCV里最基础、也最常用的一套操作。说白了,就是怎么把一张图片“请”进来,怎么看看它长什么样,处理完了怎么存回去,以及怎么在图像里“切”出我们感兴趣的区域。

这些操作,我几乎每天都在用。你想想看,不管多复杂的缺陷检测算法,第一步永远是读图。这一步要是出了问题,后面全是白搭。我记得刚入行那会儿,就因为没注意色彩空间的转换,在一条产线上折腾了整整两天,最后发现是BGR和RGB搞混了。嗯,今天咱们就把这些坑一个个填平。

2.1 图像读取:imread() 的“潜规则”

读取图像,用的是 cv2.imread()。这个函数看着简单,但有几个细节,我建议你记牢。

import cv2

# 最常用的方式:读取彩色图
img_color = cv2.imread('defect_sample.jpg', cv2.IMREAD_COLOR)

# 读取灰度图
img_gray = cv2.imread('defect_sample.jpg', cv2.IMREAD_GRAYSCALE)

# 读取包含透明通道的图
img_unchanged = cv2.imread('defect_sample.jpg', cv2.IMREAD_UNCHANGED)

第一个坑:路径问题。 我建议你永远使用绝对路径,或者用 os.path.join() 拼接路径。直接用相对路径,一旦换了电脑或者改了目录结构,代码就崩了。我在项目里吃过这个亏,调试了半天,结果就是图片没找到。

第二个坑:BGR 顺序。 OpenCV 默认读取的彩色图像是 BGR 格式,不是我们常见的 RGB。这一点极其重要。如果你用 matplotlib 显示,颜色会完全错乱。为什么会这样?历史原因,OpenCV 早期就是这么设计的。我们只能接受它,然后在需要的时候转换。

⚠️ 避坑指南: 我曾经在给客户演示的时候,因为直接用 matplotlib 显示了 OpenCV 读进来的图,结果一张蓝色的工件变成了橙色。场面一度非常尴尬。从那以后,我每次显示前都会确认色彩空间。

2.2 图像显示:imshow() 与窗口管理

读进来之后,总得看一眼吧?cv2.imshow() 就是干这个的。

cv2.imshow('Defect Image', img_color)
cv2.waitKey(0)  # 等待按键,0表示无限等待
cv2.destroyAllWindows()

这里有个关键点:waitKey() 必须要有。没有它,窗口会一闪而过。我个人习惯用 cv2.waitKey(0) 来暂停,按任意键继续。如果你想让窗口自动关闭,可以设置一个时间,比如 cv2.waitKey(1000) 表示等待1000毫秒。

另外,destroyAllWindows() 是清理窗口的好习惯。尤其是在循环处理多张图片时,不清理的话,内存占用会越来越高。

💡 小技巧: 在调试时,我经常用 cv2.namedWindow('window', cv2.WINDOW_NORMAL) 来创建可调整大小的窗口,方便查看大图的细节。

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

处理完了,要存盘。cv2.imwrite() 可以保存为多种格式,但有几个参数值得注意。

# 保存为 PNG(无损)
cv2.imwrite('result.png', img_processed)

# 保存为 JPEG,并指定质量(95%)
cv2.imwrite('result.jpg', img_processed, [cv2.IMWRITE_JPEG_QUALITY, 95])

# 保存为 PNG,并指定压缩级别(3,0-9,越大压缩率越高但越慢)
cv2.imwrite('result.png', img_processed, [cv2.IMWRITE_PNG_COMPRESSION, 3])

JPEG 质量参数: 范围是0到100,默认是95。在工业缺陷检测中,我建议不要低于90。因为 JPEG 是有损压缩,质量太低会引入压缩伪影,这些伪影可能被误判为缺陷。我曾经遇到过因为 JPEG 压缩导致边缘出现锯齿,被算法误报为划痕的情况。

PNG 压缩参数: 范围是0到9。0表示不压缩,速度最快;9表示最大压缩,文件最小但保存最慢。我个人习惯用3,平衡速度和大小。

2.4 色彩空间转换:BGR、灰度、HSV

这是图像处理的核心之一。不同的色彩空间,适合不同的任务。

2.4.1 BGR 转灰度

灰度图只有亮度信息,没有颜色。在缺陷检测中,很多算法(比如边缘检测、阈值分割)都是在灰度图上操作的。

gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)

为什么转灰度?说白了,就是降维。把三个通道变成一个通道,计算量大大减少,而且很多纹理信息在灰度图上依然保留得很好。

2.4.2 BGR 转 HSV

HSV(色调、饱和度、明度)在颜色分割方面非常强大。比如你要检测一个红色区域的缺陷,在 BGR 空间里很难设定阈值,因为红色在 BGR 中可能是 (0,0,255) 到 (0,0,200) 的范围,受光照影响很大。但在 HSV 空间里,你只需要设定色调的范围。

hsv = cv2.cvtColor(img_color, cv2.COLOR_BGR2HSV)

# 定义红色的 HSV 范围
lower_red = np.array([0, 50, 50])
upper_red = np.array([10, 255, 255])

# 创建掩码
mask = cv2.inRange(hsv, lower_red, upper_red)
🔑 关键点: HSV 的 H 通道范围是 0-179,不是 0-255。这是 OpenCV 的特殊设计。很多新手在这里翻车,设定了一个 0-255 的范围,结果什么都检测不到。

我在项目中用 HSV 做过一个案例:检测电路板上的焊锡缺陷。焊锡在特定光照下会呈现特定的色调,通过 HSV 分割,可以快速定位焊点区域,然后在这个区域内做更精细的检测。效果比在 BGR 空间里好得多。

2.5 像素操作:访问与修改

图像本质上就是一个多维数组。在 OpenCV 中,你可以像操作 NumPy 数组一样操作像素。

# 获取某个像素的值(BGR)
(b, g, r) = img_color[100, 200]
print(f"像素 (100,200) 的 BGR 值: ({b}, {g}, {r})")

# 修改某个像素的值
img_color[100, 200] = (255, 0, 0)  # 变成蓝色

# 获取某个区域的值
roi = img_color[50:150, 100:200]  # 行范围 50-150,列范围 100-200

# 修改某个区域的值
img_color[50:150, 100:200] = (0, 255, 0)  # 整个区域变成绿色

注意: 像素坐标是 (行, 列),也就是 (y, x)。这和数学中的 (x, y) 是反的。我刚开始也经常搞混,后来养成了习惯:先写行,再写列。

⚠️ 性能提示: 不要用 for 循环逐像素操作!在 Python 中,逐像素循环非常慢。应该尽量使用 NumPy 的向量化操作。比如要把所有像素的红色通道加倍,应该用 img[:, :, 2] = img[:, :, 2] * 2,而不是用 for 循环。

2.6 ROI 提取:从图像中“切”出感兴趣的区域

ROI(Region of Interest)是缺陷检测中最常用的操作之一。一张大图上,缺陷可能只占一小块。我们先把这一小块切出来,再进行处理,效率会高很多。

# 假设我们已知缺陷区域在 (x, y) = (100, 50),宽高为 (w, h) = (200, 150)
x, y, w, h = 100, 50, 200, 150
roi = img_color[y:y+h, x:x+w]

# 在 ROI 上做处理(比如转灰度)
roi_gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)

# 处理完后,可以放回原图
img_color[y:y+h, x:x+w] = cv2.cvtColor(roi_gray, cv2.COLOR_GRAY2BGR)

ROI 的常见应用场景:

  • 定位后裁剪: 先用模板匹配或特征检测找到目标位置,然后切出 ROI 做精细检测。
  • 多区域检测: 一张图上有多个检测区域,分别提取 ROI,用不同的参数处理。
  • 减少计算量: 只处理 ROI,不处理背景区域,速度能提升好几倍。

我记得有一次做手机屏幕的划痕检测。整张屏幕图片有 4000x3000 像素,直接处理非常慢。后来我通过定位屏幕的四个角,只提取屏幕区域作为 ROI,处理速度从 2 秒降到了 0.3 秒。这就是 ROI 的威力。

2.7 综合示例:一个完整的流程

最后,我们把这些操作串起来,写一个完整的示例。假设我们要检测一张电路板图片上的焊点缺陷。

import cv2
import numpy as np

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

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

# 3. 转 HSV,用于颜色分割
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# 4. 定义焊点的颜色范围(假设是金色)
lower_gold = np.array([15, 50, 50])
upper_gold = np.array([30, 255, 255])
mask = cv2.inRange(hsv, lower_gold, upper_gold)

# 5. 用掩码提取 ROI(焊点区域)
roi = cv2.bitwise_and(img, img, mask=mask)

# 6. 在 ROI 上做缺陷检测(这里简单用边缘检测示意)
roi_gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(roi_gray, 50, 150)

# 7. 显示结果
cv2.imshow('Mask', mask)
cv2.imshow('ROI', roi)
cv2.imshow('Edges', edges)

# 8. 保存结果
cv2.imwrite('result_edges.jpg', edges)

cv2.waitKey(0)
cv2.destroyAllWindows()

这个例子虽然简单,但涵盖了今天讲的所有核心操作:读取、显示、色彩空间转换、像素操作、ROI 提取、保存。在实际项目中,你只需要根据具体的缺陷特征,调整颜色范围和处理算法即可。

好了,这一章的内容就到这里。下一章我们会深入图像预处理,聊聊滤波、阈值和形态学操作。这些都是缺陷检测的“基本功”,练好了,后面才能玩出花来。