图像处理基础(上):图像读取与显示、色彩空间转换、几何变换

各位同学,欢迎来到第二章。上一章我们搭好了开发环境,今天咱们来点真家伙——图像处理的基础操作。

说实话,图像处理是整个交通标志识别系统的地基。你想想看,摄像头拍回来的原始图像,噪点多、光照乱、角度歪,直接扔给模型去识别,那结果肯定惨不忍睹。我当年第一次做车牌识别项目时,就吃了这个亏——图像预处理没做好,模型准确率死活上不去。

好,咱们开始。今天要讲三个核心模块:图像怎么读进来、怎么在不同色彩空间里切换、怎么对图像做几何变换。

一、图像读取与显示

在OpenCV里,读取图像用 cv2.imread(),显示用 cv2.imshow()。听起来简单吧?但这里有个坑,我踩过。

注意:OpenCV默认读取的图像是BGR格式,不是RGB!很多初学者直接拿OpenCV读图,然后用matplotlib显示,结果颜色全乱了。我刚开始也犯过这个错,调试了半天才发现是通道顺序的问题。

来看代码:

import cv2
import matplotlib.pyplot as plt

# 读取图像
img = cv2.imread('traffic_sign.jpg')

# 检查是否读取成功
if img is None:
    print("图像读取失败,检查路径")
    exit()

# 显示图像(OpenCV方式)
cv2.imshow('Traffic Sign', img)
cv2.waitKey(0)  # 等待按键
cv2.destroyAllWindows()

# 如果用matplotlib显示,需要转换通道
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
plt.title('交通标志')
plt.show()

这里有个小技巧:cv2.waitKey(0) 会让窗口一直显示,直到你按下任意键。如果你想让窗口自动关闭,可以设置一个延时,比如 cv2.waitKey(3000) 表示3秒后自动关闭。

个人经验:我习惯在读取图像后立即检查返回值是否为None。因为路径写错、文件损坏这些情况太常见了。加个判断,能省去很多调试时间。

二、色彩空间转换

色彩空间转换,说白了就是把图像从一种颜色表示方式变成另一种。最常用的三种:RGB、HSV、灰度图。

2.1 RGB与灰度图

灰度图只有亮度信息,没有颜色。为什么要转灰度?因为很多算法(比如边缘检测、特征提取)在灰度图上跑得更快,而且不受颜色干扰。

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

# 显示灰度图
cv2.imshow('Gray Image', gray)
cv2.waitKey(0)

灰度化的原理其实很简单:对RGB三个通道加权平均。OpenCV用的权重是:Gray = 0.299*R + 0.587*G + 0.114*B。为什么绿色权重最高?因为人眼对绿色最敏感。

2.2 RGB与HSV

HSV色彩空间把颜色分解成色调(Hue)、饱和度(Saturation)、明度(Value)。这在做颜色分割时特别好用。

举个例子:你想从图像里提取红色交通标志。在RGB空间里,红色可能是 (200, 30, 30),也可能是 (180, 20, 20),阈值很难定。但在HSV空间里,红色的色调值集中在0°附近,你只需要设定一个H的范围就行。

# BGR转HSV
hsv = cv2.cvtColor(img, 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)

# 提取红色区域
red_region = cv2.bitwise_and(img, img, mask=mask)

cv2.imshow('Red Region', red_region)
cv2.waitKey(0)
关键点:HSV的H范围是0-180(OpenCV里),不是0-360。S和V的范围是0-255。这个细节很多人会搞错,我当年也在这上面栽过跟头。

为什么要用 cv2.inRange()?它会生成一个二值掩码,在指定范围内的像素设为255(白色),其他设为0(黑色)。然后用 bitwise_and 把原图和掩码做与运算,就能提取出目标区域。

2.3 色彩空间转换的应用场景

色彩空间 适用场景 我的建议
RGB 通用显示、深度学习输入 注意OpenCV是BGR顺序
HSV 颜色分割、目标检测 做交通标志识别首选
灰度 边缘检测、特征提取 能提速,但会丢失颜色信息

三、图像几何变换

几何变换,就是改变图像的形状、大小、位置。交通标志在摄像头里可能是歪的、小的、斜的,我们需要把它们校正到标准状态。

3.1 缩放

缩放用 cv2.resize()。这里有个选择:用哪种插值方法?

# 缩小图像(用最近邻插值)
small = cv2.resize(img, (100, 100), interpolation=cv2.INTER_NEAREST)

# 放大图像(用双线性插值)
large = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_LINEAR)

# 高质量放大(用三次插值)
high_quality = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
避坑指南:我曾经在放大图像时用了最近邻插值,结果图像出现了明显的锯齿。后来改成双线性插值,效果就好多了。一般来说,缩小用INTER_AREA,放大用INTER_LINEAR或INTER_CUBIC。

3.2 平移

平移就是让图像在x和y方向上移动。需要构造一个仿射变换矩阵。

import numpy as np

# 定义平移矩阵
M = np.float32([[1, 0, 50],  # x方向移动50像素
                [0, 1, 30]]) # y方向移动30像素

# 执行平移
shifted = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))

cv2.imshow('Shifted Image', shifted)
cv2.waitKey(0)

注意 warpAffine 的第三个参数是输出图像的尺寸。如果移动后部分图像超出边界,会被裁剪掉。

3.3 旋转

旋转也是用仿射变换。OpenCV提供了 cv2.getRotationMatrix2D() 来生成旋转矩阵。

# 获取图像中心
h, w = img.shape[:2]
center = (w // 2, h // 2)

# 生成旋转矩阵(旋转45度,缩放1.0)
M = cv2.getRotationMatrix2D(center, 45, 1.0)

# 执行旋转
rotated = cv2.warpAffine(img, M, (w, h))

cv2.imshow('Rotated Image', rotated)
cv2.waitKey(0)

这里有个问题:旋转后图像四个角会被裁剪掉。如果想保留完整图像,需要调整输出尺寸。

# 计算旋转后的新尺寸
cos = abs(M[0, 0])
sin = abs(M[0, 1])
new_w = int(h * sin + w * cos)
new_h = int(h * cos + w * sin)

# 调整平移量
M[0, 2] += new_w // 2 - center[0]
M[1, 2] += new_h // 2 - center[1]

rotated_full = cv2.warpAffine(img, M, (new_w, new_h))

3.4 仿射变换与透视变换

仿射变换是线性变换加平移,保持平行线平行。透视变换则更灵活,可以把矩形变成任意四边形。

在交通标志识别中,透视变换特别有用。比如你拍到的标志是倾斜的,可以用透视变换把它校正成正面视角。

# 原图中的四个点(标志的四个角)
pts1 = np.float32([[50, 50], [200, 50], [50, 200], [200, 200]])

# 目标图中的四个点(校正后的位置)
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])

# 计算透视变换矩阵
M = cv2.getPerspectiveTransform(pts1, pts2)

# 执行透视变换
warped = cv2.warpPerspective(img, M, (300, 300))

cv2.imshow('Warped Image', warped)
cv2.waitKey(0)
核心要点:仿射变换需要3个点对,透视变换需要4个点对。点对的选择很关键,选错了校正效果会很差。我一般会先用边缘检测找到标志的轮廓,然后提取四个角点。

四、实战小练习

好了,理论讲完了。咱们来个小练习,把今天学的串起来。

任务:读取一张交通标志图像,先转HSV提取红色区域,然后对提取到的区域做透视校正,最后缩放到统一尺寸。

import cv2
import numpy as np

# 1. 读取图像
img = cv2.imread('stop_sign.jpg')
if img is None:
    print("读取失败")
    exit()

# 2. 转HSV提取红色
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower_red = np.array([0, 50, 50])
upper_red = np.array([10, 255, 255])
mask = cv2.inRange(hsv, lower_red, upper_red)

# 3. 找到轮廓
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0:
    print("未找到红色区域")
    exit()

# 取最大轮廓
cnt = max(contours, key=cv2.contourArea)

# 4. 获取四个角点(用近似多边形)
epsilon = 0.02 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)

if len(approx) == 4:
    pts1 = np.float32(approx)
    pts2 = np.float32([[0, 0], [200, 0], [0, 200], [200, 200]])
    M = cv2.getPerspectiveTransform(pts1, pts2)
    warped = cv2.warpPerspective(img, M, (200, 200))
    
    # 5. 缩放到标准尺寸
    final = cv2.resize(warped, (100, 100))
    
    cv2.imshow('Result', final)
    cv2.waitKey(0)
else:
    print("未找到四边形轮廓")

这个练习涵盖了今天讲的三个核心内容。你可以在自己的数据集上试试,看看效果如何。

好,今天就到这里。下一章我们会讲图像滤波、边缘检测和形态学操作,这些都是图像预处理的重头戏。记得动手敲代码,光看是学不会的。