第三讲:图像处理基础(下)——加减乘除、几何变换与ROI提取

各位同学,欢迎来到第三讲。上一讲我们聊了像素、颜色空间这些基础概念。今天要讲的东西,说白了就是怎么对图像做「数学题」和「变形记」。这些操作在车道线检测里几乎天天用,比如矫正摄像头畸变、提取感兴趣区域、增强图像对比度等等。

我个人习惯把图像看作一个巨大的数字矩阵。你想想看,既然它是数字,那加减乘除自然就能往上招呼。嗯,咱们就从最基本的运算开始。

3.1 图像的基本算术运算

图像的基本运算,其实就是对每个像素点做数学计算。OpenCV里封装好了函数,咱们直接调就行。但我建议你理解背后的原理,这样遇到奇怪的结果时才知道怎么排查。

3.1.1 加法与减法

图像加法最常用的场景是调整亮度。每个像素值加上一个常数,图像就变亮;减去一个常数,就变暗。但要注意,像素值范围是0-255,超出这个范围会被截断或取模。

⚠️ 我曾经踩过的坑: 直接用NumPy做加法,结果溢出后像素值变成了很小的数,图像反而变暗了。OpenCV的cv2.add()会自动做饱和截断,而NumPy的+操作是取模运算。两者结果完全不同!

import cv2
import numpy as np

img = cv2.imread('road.jpg')
# OpenCV加法(饱和截断)
bright_img = cv2.add(img, 50)  # 每个像素+50,超过255就变成255
# NumPy加法(取模)
wrong_img = img + 50  # 超过255会绕回0,结果诡异

减法常用于图像差分,比如检测运动物体。在车道线检测里,我偶尔会用减法来去除背景光照不均的影响。

3.1.2 乘法与除法

乘除法主要用于调整对比度。乘以一个大于1的系数,对比度增强;乘以小于1的系数,对比度降低。但同样要注意溢出问题。

💡 我的小技巧: 做对比度调整时,我习惯先归一化到0-1的浮点数,做完乘法再映射回0-255。这样精度更高,也不容易溢出。

# 对比度调整
img_float = img.astype(np.float32) / 255.0
contrast_img = cv2.multiply(img_float, 1.5)  # 对比度增强1.5倍
contrast_img = np.clip(contrast_img * 255, 0, 255).astype(np.uint8)

3.1.3 图像混合(加权加法)

这个在车道线检测里特别有用。比如你想把检测到的车道线叠加到原图上,就需要用到图像混合。

# 图像混合:alpha * img1 + beta * img2 + gamma
overlay = cv2.addWeighted(img1, 0.7, img2, 0.3, 0)

说白了,就是给两张图各分配一个权重,加起来得到新图。权重之和通常为1,这样亮度不会整体偏移。

3.2 几何变换:让图像动起来

几何变换解决的是「图像怎么摆」的问题。摄像头装在车上,角度、位置都固定,但路面是变化的。我们需要通过几何变换来模拟不同视角,或者矫正镜头畸变。

3.2.1 图像缩放

缩放是最简单的几何变换。但这里有个坑:直接缩小再放大,图像会变模糊。因为缩放过程中丢失了高频信息。

我记得有一次做项目,为了加快处理速度把图像缩小了4倍,结果车道线检测的精度掉了一大截。后来发现是缩放时用了最近邻插值,换成双线性插值就好多了。

# 缩放
resized = cv2.resize(img, (640, 480))  # 指定尺寸
resized = cv2.resize(img, None, fx=0.5, fy=0.5)  # 按比例缩放

# 插值方法选择
# cv2.INTER_NEAREST - 最近邻(速度快,质量差)
# cv2.INTER_LINEAR - 双线性(默认,平衡)
# cv2.INTER_CUBIC - 双三次(速度慢,质量好)

3.2.2 图像旋转

旋转需要指定旋转中心、旋转角度和缩放比例。OpenCV会生成一个仿射变换矩阵,然后应用变换。

# 旋转图像
h, w = img.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, 45, 1.0)  # 旋转45度,不缩放
rotated = cv2.warpAffine(img, M, (w, h))

旋转后图像边缘会被裁剪掉。如果你不想丢失信息,可以调整输出尺寸,或者用cv2.BORDER_REPLICATE填充边缘。

3.2.3 仿射变换

仿射变换是车道线检测里的核心操作。它保持直线和平行关系,但可以完成平移、旋转、缩放、错切等操作。

我举个实际例子:车载摄像头拍到的车道线是透视变形的——近处宽、远处窄。通过仿射变换,我们可以把路面投影成俯视图,这样车道线就变成平行的了,检测起来简单很多。

# 仿射变换:需要3个对应点
src_points = np.float32([[50, 50], [200, 50], [50, 200]])
dst_points = np.float32([[10, 100], [200, 50], [100, 250]])
M = cv2.getAffineTransform(src_points, dst_points)
affined = cv2.warpAffine(img, M, (w, h))

🔑 关键理解: 仿射变换矩阵M是一个2x3的矩阵,前两列控制旋转和缩放,最后一列控制平移。你想想看,6个参数决定了图像怎么变,是不是很灵活?

3.2.4 透视变换

透视变换比仿射变换更强大。它不保持平行关系,可以模拟「近大远小」的效果。在车道线检测中,我们常用透视变换来做「鸟瞰图」转换。

# 透视变换:需要4个对应点
src_points = np.float32([[0, 0], [w, 0], [0, h], [w, h]])
dst_points = np.float32([[100, 0], [w-100, 0], [0, h], [w, h]])
M = cv2.getPerspectiveTransform(src_points, dst_points)
warped = cv2.warpPerspective(img, M, (w, h))

嗯,这里要注意:透视变换需要4个点,而仿射变换只需要3个。因为透视变换有8个自由度(矩阵是3x3的),比仿射变换多2个。

3.3 图像裁剪与ROI提取

ROI(Region of Interest)提取,说白了就是「我只关心图像中的某一块」。在车道线检测中,天空、路边的树木、对面来车,这些都不是我们关心的。我们只关注路面上的车道线区域。

3.3.1 直接裁剪

最简单的ROI提取就是数组切片。图像在NumPy里是数组,直接切出你想要的区域就行。

# 裁剪:y1:y2, x1:x2
roi = img[200:600, 100:500]  # 从第200行到600行,第100列到500列

但直接裁剪有个问题:ROI形状必须是矩形。而车道线区域往往是梯形或更复杂的形状。

3.3.2 掩膜提取

对于不规则形状的ROI,我们需要用掩膜(mask)。先创建一个全黑的图像,然后在ROI区域画上白色,最后用按位与操作提取。

# 创建掩膜
mask = np.zeros(img.shape[:2], dtype=np.uint8)
# 定义多边形顶点(车道线区域通常是梯形)
pts = np.array([[100, 600], [300, 400], [500, 400], [700, 600]], np.int32)
cv2.fillPoly(mask, [pts], 255)
# 提取ROI
roi = cv2.bitwise_and(img, img, mask=mask)

💡 避坑指南: 我曾经在提取ROI时忘了把掩膜转换成单通道,结果按位与操作报错。记住:掩膜必须是单通道的uint8类型,值只有0和255。

3.4 知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速索引。

图像处理基础(下) 基本算术运算 加法/减法 → 亮度调整 乘法/除法 → 对比度调整 加权加法 → 图像混合 几何变换 缩放 → 插值方法选择 旋转 → 中心/角度/填充 仿射变换 → 3点对应 透视变换 → 4点对应 ROI提取 直接裁剪 → 矩形区域 掩膜提取 → 任意形状 车道线检测核心应用 透视变换 → 鸟瞰图 掩膜提取 → 路面区域

3.5 实战要点总结

好了,这一讲的内容不少。我帮你梳理一下核心要点:

  • 算术运算:注意OpenCV和NumPy的差异,加法用cv2.add(),乘法用cv2.multiply(),避免溢出问题。
  • 几何变换:仿射变换用3个点,透视变换用4个点。车道线检测中透视变换做鸟瞰图是标配操作。
  • ROI提取:矩形区域用切片,不规则区域用掩膜+按位与。掩膜必须是单通道uint8。
  • 插值选择:缩放时双线性插值最常用,缩小用区域插值效果更好。

🎯 一句话总结: 图像处理就是「算」和「变」——算像素值来调整图像质量,变几何结构来改变观察视角。这两个基本功打牢了,后面做车道线检测就顺风顺水。

我个人建议你动手跑一遍上面的代码。把一张道路图片拿来,试试不同的加减乘除、旋转角度、透视变换参数。你会发现,同样的图片在不同参数下差别巨大。嗯,这就是图像处理的魅力——你手里握着的是像素,但你能创造的是无数种视觉可能性。

可可精品资料库