8.3 光流法
光流法是计算机视觉中一种用于估计图像中像素运动的技术,它基于一系列图像帧之间的亮度信息变化,通过跟踪同一场景中的特征点,计算这些特征点在时间上的运动轨迹。光流法在很多应用中都有重要的作用,例如目标跟踪、运动分析、视觉里程计等。光流法的基本假设是,场景中相邻的图像点在连续帧之间的亮度值保持不变。光流可以用向量场表示,其中每个向量代表了一个点在两帧之间的位移。光流的计算可以通过不同的算法实现,其中一种常用的是Lucas-Kanade方法。
8.3.1 Lucas-Kanade算法
Lucas-Kanade(LK)光流算法是一种用于计算图像中局部运动的经典光流算法,该算法基于亮度恒定假设,即场景中相邻像素的亮度在短时间内保持不变。Lucas-Kanade算法主要用于跟踪图像中的特征点,估计这些特征点在连续帧之间的运动。
Lucas-Kanade算法的基本思想是在每个特征点周围采用局部的二阶泰勒展开,然后通过最小二乘法求解一个由光流场引起的误差最小的位移向量。实现Lucas-Kanade算法的具体步骤如下所示。
(1)选择特征点:在图像中选择需要跟踪的特征点。
(2)建立局部窗口:对每个特征点周围建立一个局部窗口,窗口的大小可以根据具体情况而定。
(3)计算空间梯度:在窗口内计算每个像素点的空间梯度,通常使用Sobel算子等方法。
(4)计算时间梯度:在时间维度上计算特征点在图像序列中的亮度变化。
(5)构建雅可比矩阵:利用空间梯度和时间梯度,构建雅可比矩阵,描述光流场对亮度变化的影响。
(6)求解最小二乘问题:利用最小二乘法,求解一个由光流场引起的误差最小的位移向量。
Lucas-Kanade算法的优点在于其简单性和计算效率,适用于近似局部恒定运动的场景。然而,它对于较大的光流和非恒定亮度变化的情况可能表现不佳。在实际应用中,可以结合金字塔法等技术来提高算法的稳健性和适用性。请看下面的例子,演示了使用Lucas-Kanade算法进行光流估计的过程,并在移动的车辆图像中标记运动的区域。
实例5-8:使用Lucas-Kanade算法进行光流估计(codes/5/lk.py)
实例文件lk.py的具体实现代码如下所示。
# 读取图像
img = cv2.imread('car.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 选择特征点
p0 = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.01, minDistance=10)
# Lucas-Kanade参数
lk_params = dict(winSize=(15, 15), maxLevel=2, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
# 光流法计算
p1, status, err = cv2.calcOpticalFlowPyrLK(gray, gray, p0, None, **lk_params)
# 筛选跟踪失败的点
good_new = p1[status == 1]
good_old = p0[status == 1]
# 在图像上绘制运动区域
for i, (new, old) in enumerate(zip(good_new, good_old)):
a, b = np.int32(new.ravel()) # 将坐标转换为整数类型
c, d = np.int32(old.ravel())
cv2.line(img, (a, b), (c, d), (0, 255, 0), 2)
cv2.circle(img, (a, b), 5, (0, 255, 0), -1)
# 显示结果
cv2.imshow('Lucas-Kanade for Motion Detection', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
上述代码的实现流程如下所示:
(1)首先,通过cv2.goodFeaturesToTrack选择图像中的一些特征点,这些特征点将被用于后续的Lucas-Kanade算法。
(2)然后,定义Lucas-Kanade算法的参数,包括窗口大小、金字塔层数和终止条件。
(3)接着,利用cv2.calcOpticalFlowPyrLK计算特征点在同一图像上的运动,得到新的特征点位置(p1)、状态(status)和误差(err)。在接下来的步骤中,筛选出成功跟踪的特征点,得到good_new和good_old。
(4)最后,在图像上绘制运动区域。通过遍历成功跟踪的特征点,使用cv2.line绘制运动轨迹,同时使用cv2.circle标记每个特征点的当前位置。这样,能够在图像中观察到运动区域的估计效果。
8.3.2 Farneback光流算法
Farneback光流算法是一种用于计算光流场的密集光流方法,与Lucas-Kanade算法不同,Farneback光流算法通过对整个图像中的每个像素点进行处理,得到一个密集的光流场,表示图像中每个位置的运动信息。该算法由Gunnar Farneback在2003年提出,是一种基于多项式展开的方法。
实现Farneback光流算法的主要步骤如下所示。
(1)构建图像金字塔:对输入图像构建一系列图像金字塔,通过多次降采样获得不同分辨率的图像。
(2)计算光流场:在金字塔的每一层上,通过多项式展开来逼近局部的像素运动。通过对这些展开系数的计算,得到每个像素点的光流矢量。
(3)合并光流场:将不同层次的光流场进行合并,得到最终的密集光流场。
Farneback适用于一定程度的图像变化和非线性的运动,在处理一些复杂场景下能够提供比较鲁棒的光流估计。请看下面的例子,使用Farneback光流算法估计光流场,并在图像中标记光流场的运动区域。
实例5-8:使用Farneback光流算法在图像中标记运动区域(codes/5/fb.py)
实例文件fb.py的具体实现代码如下所示。
# 读取图像
img = cv2.imread('car.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 计算光流场
flow = cv2.calcOpticalFlowFarneback(prev=gray, next=gray, flow=None, pyr_scale=0.5, levels=5, winsize=11, iterations=5, poly_n=5, poly_sigma=1.1, flags=0)
# 计算光流场的幅值和角度
magnitude, angle = cv2.cartToPolar(flow[..., 0], flow[..., 1])
# 通过降低阈值来筛选运动区域
threshold = 0.1
motion_mask = magnitude > threshold
# 在图像上标记运动区域
motion_image = np.zeros_like(img)
motion_image[motion_mask] = [0, 0, 255] # 在运动区域绘制红色
# 获取运动区域的坐标
motion_coords = np.column_stack((np.where(motion_mask)))
# 在图像上标记运动方向
for coord in motion_coords:
x, y = coord
if 0 <= x < img.shape[1] and 0 <= y < img.shape[0]: # 确保坐标在图像内
dx, dy = flow[y, x]
cv2.arrowedLine(img, (x, y), (int(x + dx), int(y + dy)), (0, 255, 0), 2)
# 合并原始图像和标记的运动区域
result = cv2.addWeighted(img, 1, motion_image, 0.5, 0)
# 显示结果
cv2.imshow('Enhanced Motion Detection using Farneback Optical Flow', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
上述代码的实现流程如下所示:
- 首先,读取汽车行驶图像并将其转换为灰度图像,然后利用Farneback光流法计算光流场。
- 接着,通过计算光流场的幅值,筛选出运动区域。这里使用了一个阈值,只保留幅值大于阈值的部分,形成二值掩码。
- 然后,从运动区域的二值掩码中获取坐标信息,表示为(motion_coords)。
- 接下来,在原始图像上标记运动区域,将运动区域绘制为红色。
- 最后,遍历运动区域的坐标,获取对应位置的光流矢量,并使用箭头标记运动方向。在这一步中,代码确保坐标不会超出图像边界,避免数组越界错误。
- 最终,将标记运动区域和箭头标记的图像与原始图像进行合并,形成最终的结果图像,并显示在屏幕上。