光流法:从传统方法到深度学习方法

1 光流法简介

  光流(Optical Flow)是指图像中像素灰度值随时间的变化而产生的运动场。 简单来说,它描述了图像中每个像素点的运动速度和方向。 光流法是一种通过分析图像序列中像素灰度值来计算光流的方法。对于图像数据计算出来的光流是一个二维向量场。比如Opencv官方给的示例图,该图中显示了一个连续运动的光点,箭头的方向表示光流的方向,箭头的大小表示光流的大小。有了光流场,我们就可以利用光流场来跟踪图像中的目标物体,就可以进行运动跟踪,目标检测,视频压缩,运动分割,三维重建等。

  光流法基于以下基本假设:

  • 亮度恒定:就是同一点随着时间的变化,其亮度不会发生改变。
  • 小运动假设(Small Motion Assumption): 相邻帧之间的运动是微小的,足以进行线性近似。

  假设每两帧画面之间的时间间隔为 Δ t \Delta t Δt,且运动很小,通过泰勒级数展开,有:
I ( x + Δ x , y + Δ y , t + Δ t ) = I ( x , y , t ) + ∂ I ∂ x Δ x + ∂ I ∂ y Δ y + ∂ I ∂ t Δ t + R ( x , y , t ) \begin{aligned} {\displaystyle I(x+\Delta x,y+\Delta y,t+\Delta t)=I(x,y,t)+{\frac {\partial I}{\partial x}}\Delta x+{\frac {\partial I}{\partial y}}\Delta y+{\frac {\partial I}{\partial t}}\Delta t+} R(x,y,t) \end{aligned} I(x+Δx,y+Δy,t+Δt)=I(x,y,t)+xIΔx+yIΔy+tIΔt+R(x,y,t)

  其中, I ( x , y , t ) I(x, y, t) I(x,y,t) 表示图像在点 ( x , y ) (x, y) (x,y) 处的灰度值, t t t 表示时间, Δ x \Delta x Δx Δ y \Delta y Δy Δ t \Delta t Δt 分别表示水平、垂直和时间上的位移, R ( x , y , t ) R(x, y, t) R(x,y,t) 表示泰勒级数展开的高阶项。根据光流法的基本假设,有:
I ( x + Δ x , y + Δ y , t + Δ t ) = I ( x , y , t ) I(x+\Delta x,y+\Delta y,t+\Delta t)=I(x,y,t) I(x+Δx,y+Δy,t+Δt)=I(x,y,t)

  由于泰勒高阶项趋近于0,直接忽略,则有:
∂ I ∂ x Δ x + ∂ I ∂ y Δ y + ∂ I ∂ t Δ t = 0 {\displaystyle {\frac {\partial I}{\partial x}}\Delta x+{\frac {\partial I}{\partial y}}\Delta y+{\frac {\partial I}{\partial t}}\Delta t=0} xIΔx+yIΔy+tIΔt=0
  等价为:
∂ I ∂ x Δ x Δ t + ∂ I ∂ y Δ y Δ t + ∂ I ∂ t Δ t Δ t = 0 {\displaystyle {\frac {\partial I}{\partial x}}{\frac {\Delta x}{\Delta t}}+{\frac {\partial I}{\partial y}}{\frac {\Delta y}{\Delta t}}+{\frac {\partial I}{\partial t}}{\frac {\Delta t}{\Delta t}}=0} xIΔtΔx+yIΔtΔy+tIΔtΔt=0
  其中, Δ t \Delta t Δt 表示时间间隔, Δ x \Delta x Δx Δ y \Delta y Δy 分别表示水平和垂直上的位移。则:
∂ I ∂ x V x + ∂ I ∂ y V y + ∂ I ∂ t = 0 {\displaystyle {\frac {\partial I}{\partial x}}V_{x}+{\frac {\partial I}{\partial y}}V_{y}+{\frac {\partial I}{\partial t}}=0} xIVx+yIVy+tI=0
  其中, V x V_x Vx V y V_y Vy 分别表示光流在水平和垂直方向上的分量。则我们需要求解的问题为:
I x V x + I y V y = − I t {\displaystyle I_{x}V_{x}+I_{y}V_{y}=-I_{t}} IxVx+IyVy=It
  简化为:
∇ I T ⋅ V ⃗ = − I t {\displaystyle \nabla I^{T}\cdot {\vec {V}}=-I_{t}} ITV =It

2 传统算法

2.1.1 Lucas-Kanade光流法

  Lucas-Kanade 方法是一种广泛使用的光流估计微分方法,由 Bruce D. Lucas 和Takeo Kanade开发。该方法假设光流在所考虑像素的局部邻域中基本恒定,并根据最小二乘准则求解该邻域内所有像素的基本光流方程。
  LK光流法基于以下假设:

  • 小运动假设(Small Motion Assumption): 相邻帧之间的运动是微小的,足以进行线性近似。
  • 亮度恒定:就是同一点随着时间的变化,其亮度不会发生改变。
  • 空间一致性:场景中相同表面的相邻点具有相似的运动,并且其投影到图像平面上的距离也比较近。一个场景上邻近的点投影到图像上也是邻近点,且邻近点速度一致。

  前两个假设是基于泰勒展开的,是所有传统光流法的基础。根据前两个假设,前面已经推导过需要求解:
∇ I T ⋅ V ⃗ = − I t {\displaystyle \nabla I^{T}\cdot {\vec {V}}=-I_{t}} ITV =It

空间一致性假设(邻域光流相似假设)
  针对第三个假设,我们可以假设在一个大小为 w × w w \times w w×w 的窗口内,所有像素点的光流都相同。则有:
I x ( q 1 ) V x + I y ( q 1 ) V y = − I t ( q 1 ) I x ( q 2 ) V x + I y ( q 2 ) V y = − I t ( q 2 ) ⋮ I x ( q n ) V x + I y ( q n ) V y = − I t ( q n ) {\displaystyle {\begin{aligned}I_{x}(q_{1})V_{x}+I_{y}(q_{1})V_{y}&=-I_{t}(q_{1})\\I_{x}(q_{2})V_{x}+I_{y}(q_{2})V_{y}&=-I_{t}(q_{2})\\&\;\ \vdots \\I_{x}(q_{n})V_{x}+I_{y}(q_{n})V_{y}&=-I_{t}(q_{n})\end{aligned}}} Ix(q1)Vx+Iy(q1)VyIx(q2)Vx+Iy(q2)VyIx(qn)Vx+Iy(qn)Vy=It(q1)=It(q2) =It(qn)

  其中, q 1 , q 2 , ⋯ , q n q_{1}, q_{2}, \cdots, q_{n} q1,q2,,qn 表示窗口内的像素点, I x ( q ) I_{x}(q) Ix(q) I y ( q ) I_{y}(q) Iy(q) 表示窗口内像素点的梯度, I t ( q ) I_{t}(q) It(q) 表示窗口内像素点灰度梯度。将该公式表示为矩阵形式为 A v = b {\displaystyle Av=b} Av=b
A = [ I x ( q 1 ) I y ( q 1 ) I x ( q 2 ) I y ( q 2 ) ⋮ ⋮ I x ( q n ) I y ( q n ) ] v = [ V x V y ] b = [ − I t ( q 1 ) − I t ( q 2 ) ⋮ − I t ( q n ) ] {\displaystyle A={\begin{bmatrix}I_{x}(q_{1})&I_{y}(q_{1})\\[10pt]I_{x}(q_{2})&I_{y}(q_{2})\\[10pt]\vdots &\vdots \\[10pt]I_{x}(q_{n})&I_{y}(q_{n})\end{bmatrix}}\quad \quad \quad v={\begin{bmatrix}V_{x}\\[10pt]V_{y}\end{bmatrix}}\quad \quad \quad b={\begin{bmatrix}-I_{t}(q_{1})\\[10pt]-I_{t}(q_{2})\\[10pt]\vdots \\[10pt]-I_{t}(q_{n})\end{bmatrix}}} A= Ix(q1)Ix(q2)Ix(qn)Iy(q1)Iy(q2)Iy(qn) v= VxVy b= It(q1)It(q2)It(qn)

  该系统方程组的数量多于未知数的数量,无法直接求解。因此,我们可以使用最小二乘来求解数值解,得到:
v = ( A T A ) − 1 A T b {\displaystyle v=\left(A^{T}A\right)^{-1}A^{T}b} v=(ATA)1ATb
  其矩阵形式为:
[ V x V y ] = [ ∑ i I x ( q i ) 2 ∑ i I x ( q i ) I y ( q i ) ∑ i I y ( q i ) I x ( q i ) ∑ i I y ( q i ) 2 ] − 1 [ − ∑ i I x ( q i ) I t ( q i ) − ∑ i I y ( q i ) I t ( q i ) ] {\displaystyle {\begin{bmatrix}V_{x}\\[10pt]V_{y}\end{bmatrix}}={\begin{bmatrix}\sum _{i}I_{x}(q_{i})^{2}&\sum _{i}I_{x}(q_{i})I_{y}(q_{i})\\[10pt]\sum _{i}I_{y}(q_{i})I_{x}(q_{i})&\sum _{i}I_{y}(q_{i})^{2}\end{bmatrix}}^{-1}{\begin{bmatrix}-\sum _{i}I_{x}(q_{i})I_{t}(q_{i})\\[10pt]-\sum _{i}I_{y}(q_{i})I_{t}(q_{i})\end{bmatrix}}} VxVy = iIx(qi)2iIy(qi)Ix(qi)iIx(qi)Iy(qi)iIy(qi)2 1 iIx(qi)It(qi)iIy(qi)It(qi)

  为了方程 A T A v = A T b {\displaystyle A^{T}Av=A^{T}b} ATAv=ATb可解, A T A {\displaystyle A^{T}A} ATA应该是可逆的,或者 A T A {\displaystyle A^{T}A} ATA的特征值满足 λ 1 ≥ λ 2 > 0 {\displaystyle \lambda _{1}\geq \lambda _{2}>0} λ1λ2>0。为了避免噪音问题,通常 λ 2 {\displaystyle \lambda _{2}} λ2不能太小。另外,如果 λ 1 / λ 2 {\displaystyle \lambda _{1}/\lambda _{2}} λ1/λ2太大,这意味着点
p {\displaystyle p} p位于边缘,容易导致多孔径问题(即不同方向的光流观察到的结果一致,而无法确认光流的方向)。因此,为了使该方法正常工作,条件是 λ 1 {\displaystyle \lambda _{1}} λ1 λ 2 {\displaystyle \lambda _{2}} λ2足够大且具有相似的量级。这个条件也是角点检测的条件。这项观察表明,通过检查单个图像,可以轻松判断哪个像素适合使用 Lucas-Kanade 方法。
算法改进
  LK光流假设相同点在相邻帧画面中移动租足够小,但是在实际场景中很多时候光流是很大的,因此需要对LK光流法进行改进。其中一种改进方法就是引入图像金字塔的多尺度处理。使用图像金字塔改进 LK 光流算法的主要思想是:

  • 解决大位移问题: 传统的 LK 光流算法基于小运动假设,当图像中存在大位移运动时,算法的精度会下降。 通过使用图像金字塔,可以将大位移运动分解为多个小位移运动,从而提高算法的精度。
  • 由粗到精的策略: 首先在金字塔的顶层(低分辨率图像)估计光流,然后在金字塔的下一层(较高分辨率图像)对光流进行细化,以此类推,直到金字塔的底层(原始图像)。 这种由粗到精的策略可以有效地减少计算量,并提高算法的鲁棒性。

  基本步骤为:

  1. 构建图像金字塔: 对连续的两帧图像分别构建图像金字塔。
  2. 初始化光流: 在金字塔的顶层,将光流初始化为零。
  3. 由粗到精的光流估计: 从金字塔的顶层开始,逐层向下进行光流估计和细化:
    • 光流估计: 在当前层,使用 LK 光流算法估计光流。
    • 光流上采样: 将当前层的光流上采样到下一层,作为下一层光流的初始值。
    • 图像扭曲 (Warping): 使用当前层的光流对下一帧图像进行扭曲,使得两帧图像更加对齐。
    • 光流细化: 在下一层,使用 LK 光流算法对光流进行细化。
  4. 重复步骤 3,直到金字塔的底层。

实践

import cv2
import numpy as npdef visualize_lk_optical_flow(video_path):"""使用金字塔 LK 光流算法预览视频的光流场。Args:video_path (str): 视频文件的路径。"""cap = cv2.VideoCapture(video_path)if not cap.isOpened():print(f"无法打开视频文件: {video_path}")returnret, old_frame = cap.read()if not ret:print("无法读取视频的第一帧。")returnold_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)# Shi-Tomasi 角点检测参数feature_params = dict(maxCorners=100,qualityLevel=0.3,minDistance=7,blockSize=7)# LK 光流参数lk_params = dict(winSize=(15, 15),maxLevel=2,criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))# 创建随机颜色用于绘制轨迹color = np.random.randint(0, 255, (100, 3))# 检测 Shi-Tomasi 角点p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)# 确保 p0 的数据类型和形状正确if p0 is not None:p0 = np.float32(p0)# 创建一个 mask 用于绘制光流轨迹mask = np.zeros_like(old_frame)while True:ret, frame = cap.read()if not ret:breakframe_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)# 计算光流if p0 is not None:  # 确保有角点可以跟踪p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)# 选择好的跟踪点if p1 is not None:good_new = p1[st == 1]good_old = p0[st == 1]# 绘制轨迹for i, (new, old) in enumerate(zip(good_new, good_old)):a, b = new.ravel()c, d = old.ravel()mask = cv2.line(mask, (int(a), int(b)), (int(c), int(d)), color[i].tolist(), 2)frame = cv2.circle(frame, (int(a), int(b)), 5, color[i].tolist(), -1)img = cv2.add(frame, mask)cv2.imshow('LK 光流', img)k = cv2.waitKey(30) & 0xffif k == 27:break# 更新前一帧和跟踪点old_gray = frame_gray.copy()p0 = good_new.reshape(-1, 1, 2)else:# 如果没有跟踪点,重新检测角点p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)if p0 is not None:p0 = np.float32(p0)mask = np.zeros_like(old_frame)  # 清空 maskcap.release()cv2.destroyAllWindows()if __name__ == "__main__":video_path = "B:/Code/TestForLearn/Src/PyTest/ImageProcess/videos/3.mp4"  # 替换为你的视频文件路径visualize_lk_optical_flow(video_path)

2.1.2 Farneback光流法

  Farneback光流法不同于LK光流法,其是基于特征点的稠密光流法,其基于图像金字塔和多尺度处理。 稠密光流意味着它为图像中的每个像素都计算一个运动矢量。 Farneback 算法特别之处在于它使用多项式展开来近似图像的局部邻域,从而估计运动。

  Farneback 算法的核心思想是使用二次多项式来近似每个像素的局部邻域的亮度分布。 对于图像中的每个像素 (x, y),其邻域的亮度值 I(x, y) 可以用以下二次多项式表示:
I ( x , y ) ≈ x T ∗ A ∗ x + b T ∗ x + c I(x, y) ≈ x^T * A * x + b^T * x + c I(x,y)xTAx+bTx+c

  • x = [ x , y ] T x = [x, y]^T x=[x,y]T是像素坐标的向量。
  • A A A 是一个 2x2 的矩阵,表示二次项系数。
  • b b b 是一个 2x1 的向量,表示一次项系数。
  • c c c 是一个标量,表示常数项。

  算法通过最小二乘法,在每个像素的局部邻域内,拟合这个二次多项式。 这就得到了每个像素的 A A A b b b c c c。假设在时间 t t t t + d t t + dt t+dt 的两个相邻帧中,同一个像素的亮度保持不变(亮度恒定假设)。 那么,对于时间 t t t 的像素 ( x , y ) (x, y) (x,y) 和时间 t + d t t + dt t+dt 的像素 ( x + d x , y + d y ) (x + dx, y + dy) (x+dx,y+dy),有:
I ( x , y , t ) = I ( x + d x , y + d y , t + d t ) I(x, y, t) = I(x + dx, y + dy, t + dt) I(x,y,t)=I(x+dx,y+dy,t+dt)

  将这个假设应用到多项式展开中,并进行一系列数学推导(涉及泰勒展开和线性化),可以得到一个线性方程组,用于求解像素的运动矢量 ( d x , d y ) (dx, dy) (dx,dy)。 这个线性方程组可以表示为:
[ A 1 + A 2 ] ∗ d = − 0.5 ∗ ( b 1 − b 2 ) [A_1 + A_2] * d = -0.5 * (b_1 - b_2) [A1+A2]d=0.5(b1b2)
其中:

  • A 1 A_1 A1 b 1 b_1 b1 是时间 t t t 的帧中像素 ( x , y ) (x, y) (x,y) 的多项式系数。
  • A 2 A_2 A2 b 2 b_2 b2 是时间 t + d t t + dt t+dt 的帧中像素 ( x , y ) (x, y) (x,y) 的多项式系数。
  • d = [ d x , d y ] T d = [dx, dy]^T d=[dx,dy]T 是运动矢量。

  通过求解这个线性方程组,可以得到每个像素的运动矢量 ( d x , d y ) (dx, dy) (dx,dy),即光流。

实践

import cv2
import numpy as npdef visualize_farneback_optical_flow_to_mp4(video_path, output_mp4_path):"""使用 Farneback 光流算法预览视频的光流场,并将结果保存为 MP4 视频文件。Args:video_path (str): 视频文件的路径。output_mp4_path (str): 输出 MP4 文件的路径。"""cap = cv2.VideoCapture(video_path)if not cap.isOpened():print(f"无法打开视频文件: {video_path}")returnret, frame = cap.read()if not ret:print("无法读取视频的第一帧。")returnheight, width = frame.shape[:2]fps = cap.get(cv2.CAP_PROP_FPS)# 定义 VideoWriter 对象fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # 或使用 'XVID'out = cv2.VideoWriter(output_mp4_path, fourcc, fps, (width, height))prvs = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)hsv = np.zeros_like(frame)hsv[..., 1] = 255while True:ret, frame = cap.read()if not ret:breaknext = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)flow = cv2.calcOpticalFlowFarneback(prvs, next, None, 0.5, 3, 15, 3, 5, 1.2, 0)mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])hsv[..., 0] = ang * 180 / np.pi / 2hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)out.write(bgr) # 写入帧到视频文件cv2.imshow('Farneback 光流', bgr)k = cv2.waitKey(30) & 0xffif k == 27:breakelif k == ord('s'):cv2.imwrite('opticalfb.png', frame)cv2.imwrite('opticalhsv.png', bgr)prvs = nextcap.release()out.release()cv2.destroyAllWindows()print(f"光流 MP4 已保存到: {output_mp4_path}")if __name__ == "__main__":video_path = "Src/PyTest/ImageProcess/videos/3.mp4"  # 替换为你的视频文件路径output_mp4_path = "Src/PyTest/ImageProcess/videos/farneback_optical_flow.mp4"  # 替换为你想保存 MP4 的路径visualize_farneback_optical_flow_to_mp4(video_path, output_mp4_path)

2.1.3 Horn光流法

  Horn-Schunck 算法是一种全局光流估计方法,它试图在整个图像范围内最小化一个能量函数,以获得平滑且符合亮度恒定假设的光流场。 与局部方法(如 Lucas-Kanade)不同,Horn-Schunck 算法考虑了整个图像的全局信息,因此对噪声和光照变化具有更强的鲁棒性。
  Horn算法假设整个图像的光流都是平滑的。因此,它会尽量减少光流的扭曲,并优先选择那些更平滑的解。该流被表述为一个全局能量函数,然后寻求最小化。对于二维图像流,该函数如下:
E = ∬ [ ( I x u + I y v + I t ) 2 + α 2 ( ∥ ∇ u ∥ 2 + ∥ ∇ v ∥ 2 ) ] d x d y {\displaystyle E=\iint \left[(I_{x}u+I_{y}v+I_{t})^{2}+\alpha ^{2}(\lVert \nabla u\rVert ^{2}+\lVert \nabla v\rVert ^{2})\right]{{\rm {d}}x{\rm {d}}y}} E=[(Ixu+Iyv+It)2+α2(∥u2+v2)]dxdy

  • I x I_x Ix I y I_y Iy是图像的梯度。
  • I t I_t It是时间导数。
  • u u u v v v是光流场的分量。
  • α \alpha α是平滑项的权重。

  该函数可以通过求解相关的多维欧拉-拉格朗日方程来最小化。这些方程是:
∂ E ∂ u − ∂ ∂ x ∂ E ∂ u x − ∂ ∂ y ∂ E ∂ u y = 0 ∂ E ∂ v − ∂ ∂ x ∂ E ∂ v x − ∂ ∂ y ∂ E ∂ v y = 0 \begin{aligned} {\displaystyle {\frac {\partial E}{\partial u}}-{\frac {\partial }{\partial x}}{\frac {\partial E}{\partial u_{x}}}-{\frac {\partial }{\partial y}}{\frac {\partial E}{\partial u_{y}}}=0} \\ {\displaystyle {\frac {\partial E}{\partial v}}-{\frac {\partial }{\partial x}}{\frac {\partial E}{\partial v_{x}}}-{\frac {\partial }{\partial y}}{\frac {\partial E}{\partial v_{y}}}=0} \end{aligned} uExuxEyuyE=0vExvxEyvyE=0

   L {\displaystyle L} L是能量表达式的被积函数,得出
I x ( I x u + I y v + I t ) − α 2 Δ u = 0 I y ( I x u + I y v + I t ) − α 2 Δ v = 0 \begin{aligned} {\displaystyle I_{x}(I_{x}u+I_{y}v+I_{t})-\alpha ^{2}\Delta u=0}\\ {\displaystyle I_{y}(I_{x}u+I_{y}v+I_{t})-\alpha ^{2}\Delta v=0} \end{aligned} Ix(Ixu+Iyv+It)α2Δu=0Iy(Ixu+Iyv+It)α2Δv=0

  在实践中,拉普拉斯算子可以用有限差分进行数值近似,可以写成 Δ u ( x , y ) = ( u ‾ ( x , y ) − u ( x , y ) ) {\displaystyle \Delta u(x,y)=({\overline {u}}(x,y)-u(x,y))} Δu(x,y)=(u(x,y)u(x,y)) u ‾ ( x , y ) {\displaystyle {\overline {u}}(x,y)} u(x,y)是加权平均值 u {\displaystyle u} u在$位置 ( x , y ) (x,y) (x,y) 处像素周围的邻域内计算。使用这个符号,上述方程组可以写成

( I x 2 + α 2 ) u + I x I y v = α 2 u ‾ − I x I t I x I y u + ( I y 2 + α 2 ) v = α 2 v ‾ − I y I t \begin{aligned} {\displaystyle (I_{x}^{2}+\alpha ^{2})u+I_{x}I_{y}v=\alpha ^{2}{\overline {u}}-I_{x}I_{t}}\\ {\displaystyle I_{x}I_{y}u+(I_{y}^{2}+\alpha ^{2})v=\alpha ^{2}{\overline {v}}-I_{y}I_{t}} \end{aligned} (Ix2+α2)u+IxIyv=α2uIxItIxIyu+(Iy2+α2)v=α2vIyIt

  这是线性的 u {\displaystyle u} u v {\displaystyle v} v并且可以针对图像中的每个像素进行求解。然而,由于解依赖于流场的相邻值,因此在更新相邻值后必须重复求解。以下迭代方案是利用克莱姆规则推导出来的:
u k + 1 = u ‾ k − I x ( I x u ‾ k + I y v ‾ k + I t ) α 2 + I x 2 + I y 2 v k + 1 = v ‾ k − I y ( I x u ‾ k + I y v ‾ k + I t ) α 2 + I x 2 + I y 2 \begin{aligned} {\displaystyle u^{k+1}={\overline {u}}^{k}-{\frac {I_{x}(I_{x}{\overline {u}}^{k}+I_{y}{\overline {v}}^{k}+I_{t})}{\alpha ^{2}+I_{x}^{2}+I_{y}^{2}}}}\\ {\displaystyle v^{k+1}={\overline {v}}^{k}-{\frac {I_{y}(I_{x}{\overline {u}}^{k}+I_{y}{\overline {v}}^{k}+I_{t})}{\alpha ^{2}+I_{x}^{2}+I_{y}^{2}}}} \end{aligned} uk+1=ukα2+Ix2+Iy2Ix(Ixuk+Iyvk+It)vk+1=vkα2+Ix2+Iy2Iy(Ixuk+Iyvk+It)
   u ‾ k {\displaystyle {\overline {u}}^{k}} uk v ‾ k {\displaystyle {\overline {v}}^{k}} vk u {\displaystyle u} u v {\displaystyle v} v的平均值。

实践

import cv2
import numpy as np
from scipy.ndimage.filters import convolve as filter2
import os
from argparse import ArgumentParser"""
see readme for running instructions
"""def show_image(name, image):if image is None:returncv2.imshow(name, image)cv2.waitKey(0)cv2.destroyAllWindows()#compute magnitude in each 8 pixels. return magnitude average
def get_magnitude(u, v):scale = 3sum = 0.0counter = 0.0for i in range(0, u.shape[0], 8):for j in range(0, u.shape[1],8):counter += 1dy = v[i,j] * scaledx = u[i,j] * scalemagnitude = (dx**2 + dy**2)**0.5sum += magnitudemag_avg = sum / counterreturn mag_avgdef visualize_optical_flow(u, v, img):h, w = img.shape[:2]hsv = np.zeros((h, w, 3), dtype=np.uint8)magnitude, angle = cv2.cartToPolar(u, v)hsv[..., 0] = angle * 180 / np.pi / 2hsv[..., 1] = 255hsv[..., 2] = cv2.normalize(magnitude, None, 0, 255, cv2.NORM_MINMAX)bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)return bgr#compute derivatives of the image intensity values along the x, y, time
def get_derivatives(img1, img2):#derivative masksx_kernel = np.array([[-1, 1], [-1, 1]]) * 0.25y_kernel = np.array([[-1, -1], [1, 1]]) * 0.25t_kernel = np.ones((2, 2)) * 0.25fx = filter2(img1,x_kernel) + filter2(img2,x_kernel)fy = filter2(img1, y_kernel) + filter2(img2, y_kernel)ft = filter2(img1, -t_kernel) + filter2(img2, t_kernel)return [fx,fy, ft]#input: images name, smoothing parameter, tolerance
#output: images variations (flow vectors u, v)
#calculates u,v vectors and draw quiver
def computeHS(beforeImg, afterImg, alpha, delta):beforeImg = beforeImg.astype(float)afterImg = afterImg.astype(float)#removing noisebeforeImg  = cv2.GaussianBlur(beforeImg, (5, 5), 0)afterImg = cv2.GaussianBlur(afterImg, (5, 5), 0)# set up initial valuesu = np.zeros((beforeImg.shape[0], beforeImg.shape[1]))v = np.zeros((beforeImg.shape[0], beforeImg.shape[1]))fx, fy, ft = get_derivatives(beforeImg, afterImg)avg_kernel = np.array([[1 / 12, 1 / 6, 1 / 12],[1 / 6, 0, 1 / 6],[1 / 12, 1 / 6, 1 / 12]], float)iter_counter = 0while True:iter_counter += 1u_avg = filter2(u, avg_kernel)v_avg = filter2(v, avg_kernel)p = fx * u_avg + fy * v_avg + ftd = 4 * alpha**2 + fx**2 + fy**2prev = uu = u_avg - fx * (p / d)v = v_avg - fy * (p / d)diff = np.linalg.norm(u - prev, 2)#converges check (at most 300 iterations)if  diff < delta or iter_counter > 300:# print("iteration number: ", iter_counter)breakreturn [u, v]if __name__ == '__main__':# Input and output video pathsinput_video_path = "Src/PyTest/ImageProcess/videos/3.mp4"  # 替换为你的视频文件路径output_video_path = "Src/PyTest/ImageProcess/videos/horn_schunck.mp4"  # 替换为你想保存 MP4 的路径# Read the input videocap = cv2.VideoCapture(input_video_path)if not cap.isOpened():print(f"Error opening video stream or file: {input_video_path}")exit()# Get video propertiesframe_width = int(cap.get(3))frame_height = int(cap.get(4))frame_rate = cap.get(cv2.CAP_PROP_FPS)# Define the codec and create VideoWriter objectfourcc = cv2.VideoWriter_fourcc(*'mp4v')out = cv2.VideoWriter(output_video_path, fourcc, frame_rate, (frame_width, frame_height))# Read the first frameret, frame1 = cap.read()if not ret:print("Failed to read the first frame!")exit()prev_gray = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)# Process the video frame by framewhile(True):ret, frame2 = cap.read()if not ret:break# Convert to grayscalegray = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)# Compute Horn-Schunck optical flowalpha = 15delta = 10**-1u, v = computeHS(prev_gray, gray, alpha, delta)# Visualize the optical flowflow_vis = visualize_optical_flow(u, v, prev_gray)# Write the frame to the output videoout.write(flow_vis)# Display the resulting frame#cv2.imshow('Horn-Schunck Optical Flow', flow_vis)#if cv2.waitKey(1) & 0xFF == ord('q'):#    break# Update the previous frameprev_gray = gray# Release video capture and writer objectscap.release()out.release()# Destroy all the windowscv2.destroyAllWindows()print(f"Video saved to {output_video_path}")

3 深度学习算法

  深度学习相关的光流算法有很多,这里只简单介绍FlowNet。

3.1 FlowNet

  FlowNet 是一个开创性的工作,它首次展示了使用卷积神经网络 (CNN) 直接学习光流估计的可行性。 在此之前,光流估计主要依赖于传统的优化方法,这些方法计算量大,速度慢。 FlowNet 的出现为光流估计领域带来了新的思路,并为后续的深度学习光流算法奠定了基础。

  FlowNet 的核心思想是利用 CNN 强大的特征提取和学习能力,直接从图像数据中学习光流的模式。 通过大量的训练数据,CNN 可以学习到图像特征与光流之间的映射关系,从而实现快速且相对准确的光流估计。FlowNet 包含两个主要的变体:FlowNetS (Simple) 和 FlowNetC (Correlation)。

  • FlowNetS (Simple):
    • 输入: 将两张连续的图像 (I1, I2) 简单地堆叠在一起,形成一个 6 通道的输入。
    • 网络结构: 一个标准的 CNN 结构,包含卷积层、池化层和全连接层。
    • 输出: 光流场 (u, v),其中 u 和 v 分别表示水平和垂直方向上的运动矢量。
    • 特点: 结构简单,易于实现,但精度相对较低。
  • FlowNetC (Correlation):
    • 输入: 两张连续的图像 (I1, I2) 分别经过一个 CNN 提取特征。
    • 相关层 (Correlation Layer): 将两组特征进行相关性计算,得到一个相关性图 (correlation map)。 相关性图表示了 I1 中每个像素与 I2 中所有像素之间的相似度。
    • 网络结构: 将相关性图与 I1 的特征图连接在一起,输入到一个 CNN 中。
    • 输出: 光流场 (u, v)。
    • 特点: 通过相关性计算,可以更好地匹配对应点,从而提高精度。

  FlowNet 使用监督学习的方式进行训练,目标是最小化预测的光流场与真实光流场之间的误差。 常用的损失函数包括 L1 损失、L2 损失以及 EPE (End-Point Error)。EPE 是光流估计中最常用的评估指标之一,它衡量的是预测光流矢量与真实光流矢量之间的欧氏距离。 对于每个像素 (x, y),EPE 定义为:
E P E ( x , y ) = ∣ ∣ ( u p r e d ( x , y ) , v p r e d ( x , y ) ) − ( u g t ( x , y ) , v g t ( x , y ) ) ∣ ∣ 2 EPE(x, y) = || (u_{pred}(x, y), v_{pred}(x, y)) - (u_{gt}(x, y), v_{gt}(x, y)) ||_2 EPE(x,y)=∣∣(upred(x,y),vpred(x,y))(ugt(x,y),vgt(x,y))2
  其中, u p r e d ( x , y ) u_{pred}(x, y) upred(x,y) v p r e d ( x , y ) v_{pred}(x, y) vpred(x,y) 是预测的光流场, u g t ( x , y ) u_{gt}(x, y) ugt(x,y) v g t ( x , y ) v_{gt}(x, y) vgt(x,y) 是真实的光流场。

  L1 损失是 EPE 的一种变体,它使用 L1 范数来衡量光流矢量之间的差异:
L 1 ( x , y ) = ∣ ∣ ( u p r e d ( x , y ) , v p r e d ( x , y ) ) − ( u g t ( x , y ) , v g t ( x , y ) ) ∣ ∣ 1 L1(x, y) = || (u_{pred}(x, y), v_{pred}(x, y)) - (u_{gt}(x, y), v_{gt}(x, y)) ||_1 L1(x,y)=∣∣(upred(x,y),vpred(x,y))(ugt(x,y),vgt(x,y))1
  L2 损失是 EPE 的另一种变体,它使用 L2 范数来衡量光流矢量之间的差异:
L 2 ( x , y ) = ∣ ∣ ( u p r e d ( x , y ) , v p r e d ( x , y ) ) − ( u g t ( x , y ) , v g t ( x , y ) ) ∣ ∣ 2 L2(x, y) = || (u_{pred}(x, y), v_{pred}(x, y)) - (u_{gt}(x, y), v_{gt}(x, y)) ||_2 L2(x,y)=∣∣(upred(x,y),vpred(x,y))(ugt(x,y),vgt(x,y))2

  FlowNet的优缺点:

  • 优点:
    • 速度快: 相对于传统的优化方法,FlowNet 的速度非常快,可以实现实时的光流估计。
    • 端到端学习: FlowNet 可以直接从图像数据中学习光流,无需手动设计特征。
    • 开创性: FlowNet 开创了深度学习光流估计的先河,为后续的研究奠定了基础。
  • 缺点:
    • 精度较低: 相对于传统的优化方法,FlowNet 的精度较低。
    • 依赖于训练数据: FlowNet 的性能 сильно зависит от качества и количества обучающих данных。
    • 泛化能力有限: FlowNet 在训练数据之外的场景中表现可能不佳。

4 总结

算法原理优点缺点适用场景
LK局部恒定性,小运动假设计算简单,速度快,易于实现对噪声敏感,对大位移估计差,容易受光照变化影响,依赖梯度信息小运动场景,光照变化不大的场景,需要快速计算的场景
Farnebäck多项式展开对噪声具有一定的鲁棒性,可以处理一定程度的大位移计算复杂度较高,对参数比较敏感运动幅度较大的场景,需要较高精度的场景
Horn-Schunck全局平滑性约束可以生成平滑的光流场,对噪声具有一定的鲁棒性计算复杂度高,速度慢,容易过度平滑,对光照变化敏感需要平滑光流场的场景,对精度要求较高的场景
深度学习算法CNN 学习图像特征与光流之间的映射关系速度快,精度高,可以处理复杂的场景 (遮挡、光照变化、运动模糊)需要大量的训练数据,对训练数据的质量要求高,泛化能力可能有限,计算资源消耗较大需要高精度和高速度的场景,复杂的场景,例如自动驾驶、机器人和视频分析

5 参考文献

  • Wiki-光流法
  • 光流法–Lucas Kanade算法
  • 计算机视觉的基本原理——光流法
  • 光流法(optical flow methods)
  • Wiki-Lucas-Kanade
  • vSLAMNet(三)-光流法-LK光流法
  • 经典光流算法 Farneback 原理与源码解析
  • Horn–Schunck Optical Flow with a Multi-Scale Strategy
  • 【回归本源】光流 - 传统算法
  • FlowNet: Learning Optical Flow with Convolutional Networks

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/903341.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解决ssh拉取服务器数据,要多次输入密码的问题

问题在于&#xff0c;每次循环调用 rsync 都是新开一个连接&#xff0c;所以每次都需要输入一次密码。为了只输入一次密码&#xff0c;有以下几种方式可以解决&#xff1a; ✅ 推荐方案&#xff1a;设置 SSH 免密登录 最稳最安全的方式是&#xff1a;配置 SSH 免密登录&#x…

web技术与Nginx网站服务

目录 一. web基础 1. 域名概念 2. Hosts 文件 3. DNS 4. 域名注册 5. 网页与 HTML 二. 网页概述 1. HTML 概述 2. HTML 基本标签 3. 网站和主页 三. 静态网页与动态网页 1. 静态网页 2. 动态网页 3. 动态网页语言 四. HTTP 协议 1. HTTP 协议概述 2. HTTP …

信创系统资产清单采集脚本:主机名+IP+MAC 一键生成 CSV

原文链接&#xff1a;信创系统资产清单采集脚本&#xff1a;主机名IPMAC 一键生成 CSV Hello&#xff0c;大家好啊&#xff01;今天给大家带来一篇在信创终端操作系统上自动批量采集主机名、IP 和 MAC 并导出为 CSV 表格的实战文章&#xff01;本方案使用 sshpass 和 Bash 脚本…

【dify+docker安装教程】

目录 一、dify安装包下载 二、运行环境配置 1、下载docker 2、安装 2.1 新建文件夹 2.2 安装 2.3 命令安装 3.下载完成后需要重启电脑&#xff0c;注意保存文档&#xff01;&#xff01;注意保存&#xff01;&#xff01;注意&#xff01;&#xff01;&#xff08;血的教…

HTML 地理定位(Geolocation)教程

HTML 地理定位(Geolocation)教程 简介 HTML5 的 Geolocation API 允许网页应用获取用户的地理位置信息。这个功能可用于提供基于位置的服务&#xff0c;如导航、本地搜索、天气预报等。本教程将详细介绍如何在网页中实现地理定位功能。 工作原理 浏览器可以通过多种方式确定…

协作开发攻略:Git全面使用指南 — 引言

协作开发攻略&#xff1a;Git全面使用指南 — 引言 Git 是一种分布式版本控制系统&#xff0c;用于跟踪文件和目录的变更。它能帮助开发者有效管理代码版本&#xff0c;支持多人协作开发&#xff0c;方便代码合并与冲突解决&#xff0c;广泛应用于软件开发领域。 文中内容仅限技…

毕业设计-基于预训练语言模型与深度神经网络的Web入侵检测系统

项目技术说明 基于预训练语言模型与深度神经网络的Web入侵检测系统&#xff0c;通过预训练模型CodeBert分词&#xff0c;将分词输入给BiGRU的深度学习模型训练。通过sniff函数实时捕获http流量信息&#xff0c;将流量信息输入给模型进行检测&#xff0c;模型可以检测的类别有S…

[计算机科学#4]:二进制如何塑造数字世界(0和1的力量)

【核知坊】&#xff1a;释放青春想象&#xff0c;码动全新视野。 我们希望使用精简的信息传达知识的骨架&#xff0c;启发创造者开启创造之路&#xff01;&#xff01;&#xff01; 内容摘要&#xff1a; 二进制是计算机世界的基石&#xff0c;数学是世界的…

JUC中各种锁机制的应用和原理及死锁问题定位

JUC中各种锁机制的应用和原理及死锁问题定位 在互联网大厂Java求职者的面试中&#xff0c;经常会被问到关于JUC&#xff08;Java Util Concurrency&#xff09;中的各种锁机制及其应用和原理的问题。本文通过一个故事场景来展示这些问题的实际解决方案。 第一轮提问 面试官&…

配置Ubuntu18.04中的Qt Creator为中文(图文详解)

配置Qt Creator为中文 1、前言2、先设置Ubuntu系统语言为中文3、配置Qt Creator中文环境2.1 IBus输入法&#xff08;方法一&#xff09;2.2、测试IBus输入法2.21IBus输入法终端中测试2.2.2IBus输入法Qt Creator中测试 2.3、Fcitx输入法&#xff08;方法二&#xff09;2.3.1安装…

高性能服务器配置经验指南3——安装服务器可能遇到的问题及解决方法

文章目录 1、重装系统后VScode远程连接失败问题2、XRDP连接黑屏问题1. 打开文件2. 添加配置3. 重启xrdp服务 3、VScode远程免密连接问题4、Vim编辑文件时出现不同用户冲突编辑的问题 在完成 服务器基本配置和 深度学习环境准备后&#xff0c;大家应该就可以正常使用服务器了&…

PyQt6基础_QThread

目录 前置 代码&#xff1a; 运行 正常运行 QThread运行报错 视频 前置 1 PySide6.QtCore.QThread - Qt for Python QThread官方文档 2 长时间任务可以放到QThread中执行&#xff0c;避免占用主线程导致界面卡顿无法操作 代码&#xff1a; import traceback,sys fro…

Spring Boot 应用运行指南

&#x1f680; Spring Boot 应用运行指南 ⚙️ 使用 Maven &#x1f527; 运行命令 $ mvn spring-boot:run✨ 启动效果 . ____ _ __ _ _/\\ / ____ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | _ | _| | _ \/ _ | \ \ \ \\\/ ___)| |_)| | | | | || (_…

jeecgboot 3.8.0 集成knife4j问题一文解决

问题描述: ​ 在cloud环境下,若应用系统配置了context-path,则无法通过网关进入后台接口管理系统 原因分析: ​ 查看请求信息发现少拼接了系统的context-path,导致无法正确请求到数据。直接使用正确的地址可以正常通过网关访问。故此确定为集成knife4j的问题。 解决办法…

【Flutter】Flutter + Unity 插件结构与通信接口封装

关联文档&#xff1a;【方案分享】Flutter Unity 跨平台三维渲染架构设计全解&#xff1a;插件封装、通信机制与热更新机制—— 支持 Android/iOS/Web 的 3D 内容嵌入与远程资源管理&#xff0c;助力 XR 项目落地 —— 支持 Android/iOS/Web 的 3D 内容嵌入与远程资源管理&…

推荐 1 款 9.3k stars 的全景式开源数据分析与可视化工具

Orama 是一个开源的数据分析与可视化项目&#xff0c;由askorama团队开发和维护。该项目旨在为用户提供一套强大而易用的工具集&#xff0c;帮助用户轻松处理和理解大规模数据&#xff0c;通过创建交互式且引人入胜的数据可视化图表&#xff0c;揭示隐藏在数据背后的深层次洞察…

关于windows API 的键鼠可控可测

相关函数解释 GetAsyncKeyState 是 Windows API 中的一个函数&#xff0c;用于判断某个虚拟键是否被按下。GetAsyncKeyState(VK_ESCAPE) 专门用于检测 Esc 键的状态。下面为你详细介绍其用法&#xff1a; 函数原型 cpp SHORT GetAsyncKeyState( int vKey ); 参数 vKey&a…

vs 安装完番茄助手visual assist 后 菜单栏不显示

vs 安装完番茄助手visual assist 后 菜单栏不显示 出现原因解决办法&#xff1a; 出现原因 vs安装完番茄助手后&#xff0c;不显示。主要原因是之前安装过&#xff0c;但是试用过期了&#xff0c;卸载后重新安装然后替换破解版的dll 导致的。 解决办法&#xff1a; 关闭vs软件…

论文导读 - 基于边缘计算、集成学习与传感器集群的便携式电子鼻系统

基于边缘计算、集成学习与传感器集群的便携式电子鼻系统 原论文地址&#xff1a;https://www.sciencedirect.com/science/article/abs/pii/S0925400522015684 引用此论文&#xff08;GB/T 7714-2015&#xff09;&#xff1a; WANG T, WU Y, ZHANG Y, et al. Portable electr…

EasyCVR视频汇聚平台助力大型生产监控项目摄像机选型与应用

一、方案背景 在300路大型生产监控项目中&#xff0c;由于生产环境复杂多样&#xff0c;涵盖室外厂区、大型车间、室内办公区域等不同场景&#xff0c;单一类型的摄像机难以满足全方位、精细化的监控需求。EasyCVR作为一款功能强大的视频融合管理平台&#xff0c;具备灵活的视…