【OpenCV无人机光流速度估计】基于Farneback稠密光流方法的无人机速度估计

前言

  • 最近有个无人机项目,使用光流传感器对无人机速度进行估计,用于飞控进行速度闭环。但是在测试期间出现了光流传感器在纯色地板上速度估计失效的现象。
  • 本文将从光流的原理出发,使用opencv-python对测试视频进行模拟光流传感器的速度估计算法,并对结果进行可视化,效果如下:

1 光流

1-1 什么是光流
  • 光流(Optical Flow)就是用来描述"图像中像素在连续两帧之间的运动变化"的方法。
  • 通常我们可以用一堆箭头来表示光流的方向

1-2 光流能做什么?
  • 光流 = 用图像变化来推断"世界在怎么动"或"相机在怎么动"
  • 那么当然就可以运用到很多领域
应用 光流作用
无人机 估计速度 / 悬停
机器人 避障
监控 运动检测
视觉跟踪 目标追踪
AR/VR 稳定画面

1-3 光流有两大类
1-3-1 稀疏光流(Sparse)
  • 只跟踪"特征点",比如:
    • 角点
    • IR点阵
  • 代表方法:
    • Lucas-Kanade
  • 比如说下图的车灯

1-3-2 稠密光流(Dense)
  • 每个像素都算运动
  • 代表方法:
    • Farneback
    • DeepFlow
  • 将会对每一个像素都进行估计

2 Farneback原理

2-1 介绍
  • Farneback本质是稠密光流估计的一种算法。
  • 其核心就是用"局部多项式拟合"来估计整张图像运动。

2-2 核心原理
  • Farneback 光流 = 用局部多项式拟合图像结构,再比较连续帧的偏移,从而得到"整张图的运动场"。
  1. 其中局部用"二次多项式表示图像":意思是在一个小窗口内:I(x,y)≈ax2+by2+cxy+dx+ey+f I(x,y)\approx ax^2 + by^2 + cxy + dx + ey + fI(x,y)≈ax2+by2+cxy+dx+ey+f也就是说图像的每个局部区域都用一个"平滑曲面"表示。(因为真实图像在小范围内"变化是连续的",可以用一个简单函数近似它)
  2. 然后比较两帧"曲面如何移动",找到dxdxdx与dydydy使曲面f(x,y)f(x,y)f(x,y)与f(x+dx,y+dy)f(x + dx, y + dy)f(x+dx,y+dy)两者最接近
  3. 最终算法将输出flowy,x=(dx,dy)flowy, x = (dx, dy)flowy,x=(dx,dy)也就是每个像素的运动向量。

2-3 Farneback算法流程
  1. 图像预处理(构建金字塔):输入两帧图像,然后构建多尺度图像金字塔,目的是为了从低分辨率到高分辨率逐层估计运动
  2. 局部多项式建模:在每个像素邻域内,假设图像可以表示为:I(x,y)≈ax2+by2+cxy+dx+ey+f I(x,y)\approx ax^2 + by^2 + cxy + dx + ey + fI(x,y)≈ax2+by2+cxy+dx+ey+f也就是用一个二次曲面拟合局部灰度变化
  3. 帧间模型位移假设:假设第二帧是第一帧的平移版本It+1(x,y)=It(x+dx,y+dy)I_{t+1}(x,y)=I_t(x+dx,y+dy)It+1(x,y)=It(x+dx,y+dy)求每个位置的(dx,dy)(dx, dy)(dx,dy)
  4. 多项式对齐(核心):对比两帧局部模型,通过数学优化,找到 dxdxdx, dydydy,使两个局部多项式误差最小∣∣Pt(x,y)−Pt+1(x+dx,y+dy)∣∣|| P_t(x,y) - P_{t+1}(x+dx,y+dy) ||∣∣Pt(x,y)−Pt+1(x+dx,y+dy)∣∣其中 dxdxdx, dydydy即为每个像素的运动向量
  5. 金字塔迭代,先估计大位移,上采样结果,在高分辨率修正
  6. 最终算法会输出一个向量场flow(x,y)=(dx,dy)flow(x,y) = (dx, dy)flow(x,y)=(dx,dy)即每个像素都有运动方向

2-4 为什么Farneback只关注灰度信息?
  • 光流只需要"亮度变化",不需要"颜色信息"。
  • 光流的基本假设是:

同一个点在短时间内亮度不变

  • 也就是:I(x,y,t)=I(x+dx,y+dy,t+Δt)I(x,y,t)=I(x+dx,y+dy,t+\Delta t)I(x,y,t)=I(x+dx,y+dy,t+Δt)
  • 关键变量只有一个:
    • I = 亮度(Intensity)
  • 而且光流关心的是边缘、纹理、明暗变化这些东东, 灰度刚好保留这些

2-5 OpenCV的Farneback实现
  • OpenCV中内置实现了Farneback算法
python 复制代码
cv2.calcOpticalFlowFarneback(  
	prev, next, flow,  
	pyr_scale,  
	levels,  
	winsize,  
	iterations,  
	poly_n,  
	poly_sigma,  
	flags  
)
  • 算法输入:(注意必须是同尺寸灰度图
    • prev:上一帧
    • next:当前帧
  • 算法输出:flow,即:每个像素的运动向量flowy,x=(dx,dy)flowy, x = (dx, dy)flowy,x=(dx,dy)

2-5-1 核心参数:
  • pyr_scale:金字塔缩放比例, 每一层图像缩小多少,用来处理"大位移"
    • 默认为0.5,数值越小越稳定但是越慢,数值越大越快但是越不准
  • levels:金字塔层数,就是图像要缩几层
  • winsize:窗口大小,就是每次看多大区域来判断运动
    • 越小细节越多但更容易抖
  • iterations:迭代次数,每一层算几次优化
  • poly_n:多项式窗口大小,用多大邻域来拟合"曲面",就是上面那个I(x,y)≈ax2+by2+cxy+dx+ey+f I(x,y)\approx ax^2 + by^2 + cxy + dx + ey + fI(x,y)≈ax2+by2+cxy+dx+ey+f
  • poly_sigma:平滑程度,曲面拟合的"平滑程度"
    • 越小更敏感,但噪声多。越大 更平滑,但细节少
  • flags:模式开关
    • flags = 0:使用高斯权重(更平滑)

3 代码实现

3-1 核心目标
  • 我们要做的事情非常简单,读取一段视频,借助OpenCV内置实现的Farneback算法,计算出连续视频帧的像素向量场,并将其可视化为箭头的同时计算整体平均速度。、
3-2 完整代码
  • 老规矩先给出全部代码,再细致分析
python 复制代码
import cv2  
import numpy as np  
  
VIDEO_PATH = "test02.mp4"  
  
cap = cv2.VideoCapture(VIDEO_PATH)  
  
if not cap.isOpened():  
    raise RuntimeError("视频打开失败")  
  
ret, frame = cap.read()  
if not ret:  
    raise RuntimeError("读取失败")  
  
prev_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  
  
  
scale = 0.5  # 降采样提升稳定性  
  
while True:  
    ret, frame = cap.read()  
    if not ret:  
        break  
  
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  
  
    # 可选降采样  
    gray_s = cv2.resize(gray, None, fx=scale, fy=scale)  
  
    prev_s = cv2.resize(prev_gray, None, fx=scale, fy=scale)  
  
  
    flow = cv2.calcOpticalFlowFarneback(  
        prev_s,  
        gray_s,  
        None,  
        pyr_scale=0.5,  
        levels=3,  
        winsize=15,  
        iterations=3,  
        poly_n=5,  
        poly_sigma=1.2,  
        flags=0  
    )  
  
    # flow[...,0] = dx, flow[...,1] = dy  
    fx = flow[..., 0]  
    fy = flow[..., 1]  
  
    # 计算平均运动(核心)  
    mean_fx = np.mean(fx)  
    mean_fy = np.mean(fy)  
  
  
    vis = frame.copy()  
  
    h, w = vis.shape[:2]  
  
    step = 20  
    for y in range(0, h, step):  
        for x in range(0, w, step):  
            dx = fx[int(y * scale), int(x * scale)]  
            dy = fy[int(y * scale), int(x * scale)]  
  
            cv2.arrowedLine(  
                vis,  
                (x, y),  
                (int(x + dx * 5), int(y + dy * 5)),  
                (0, 255, 0),  
                1,  
                tipLength=0.3  
            )  
  
  
    text = f"Flow X: {mean_fx:.3f}  Y: {mean_fy:.3f}"  
    cv2.putText(vis, text, (20, 30),  
                cv2.FONT_HERSHEY_SIMPLEX, 0.8,  
                (0, 0, 255), 2)  
  
    print(text)  
  
    cv2.imshow("PX4FLOW-like Optical Flow", vis)  
  
  
    prev_gray = gray.copy()  
  
    key = cv2.waitKey(1)  
    if key == 27:  
        break  
  
cap.release()  
cv2.destroyAllWindows()

3-3 读取视频
python 复制代码
VIDEO_PATH = "test02.mp4"
cap = cv2.VideoCapture(VIDEO_PATH)
ret, frame = cap.read()
prev_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
  • 打开视频流并转灰度图

3-4 降采样
python 复制代码
scale = 0.5
gray_s = cv2.resize(gray, None, fx=scale, fy=scale)
prev_s = cv2.resize(prev_gray, None, fx=scale, fy=scale)
  • 这里我们把图缩小一半,加速计算而且可以一定程度上降噪提高稳定性。

3-5 核心:计算光流
python 复制代码
flow = cv2.calcOpticalFlowFarneback(  
    prev_s,  
    gray_s,  
    None,  
    pyr_scale=0.5,  
    levels=3,  
    winsize=15,  
    iterations=3,  
    poly_n=5,  
    poly_sigma=1.2,  
    flags=0  
)
  • 请见2-5 OpenCV的Farneback实现查看具体参数信息
  • 算法输出二维向量场:每个像素从上一帧到当前帧移动了多少
python 复制代码
flow[y, x] = (dx, dy)

3-6 拆出 x/y 方向
python 复制代码
fx = flow[..., 0]  
fy = flow[..., 1]
  • fx = 水平运动(左右)
  • fy = 垂直运动(上下)

3-7 平均速度估计
python 复制代码
mean_fx = np.mean(fx)
mean_fy = np.mean(fy)
  • 直接取平均值

3-8 可视化向量场
python 复制代码
step = 20
for y in range(0, h, step):
	for x in range(0, w, step):
		dx = fx[int(y * scale), int(x * scale)]
		dy = fy[int(y * scale), int(x * scale)]

		cv2.arrowedLine(
			vis,
			(x, y),
			(int(x + dx * 5), int(y + dy * 5)),
			(0, 255, 0),
			1,
			tipLength=0.3
		)
  • 为了保证可视化,我们不把每一个像素都画箭头,我们每隔20像素取一个点,取画图
  • 注意因为我们缩放了图像,所以要乘 scale
  • 箭头的大小表示这个位置的运动速度有多大(位移有多强)

3-9 更新帧
python 复制代码
prev_gray = gray.copy()
  • 当前帧变上一帧,方便下一轮循环迭代

4 测试:

4-1 普通场景测试

4-2 纯色地板测试

4-3 小结
  • 可以看到,纯色地板压根就没有什么箭头
  • 这是因为纯色地板图像灰度几乎恒定,缺乏可用于匹配的特征结构,局部多项式拟合退化。
  • 我们回顾以下光流依赖的基本假设:
    • 图像亮度在短时间内保持不变,并且空间上具有可区分结构
  • 那么在纯色区域
    • 亮度不变(满足)
    • 但空间结构不存在(关键失败点)

总结

  • 本文基于Optical flow原理与Farneback算法实现,验证了光流在存在纹理时可稳定估计运动,但在低纹理(纯色)场景下因空间结构缺失导致估计退化甚至失效。
  • 如有错误!欢迎指出!
  • 感谢观看!
相关推荐
聆风吟º1 小时前
【Python编程日志】Python基础语法:常量 | 表达式 | 变量
开发语言·python·变量·常量·表达式
jiayong231 小时前
ZeroClaw 项目总览与架构分析
人工智能·架构·智能体·zeroclaw
QiLinkOS1 小时前
发明人与专利价值共生逻辑
c语言·数据结构·c++·人工智能·单片机·嵌入式硬件·算法
阳明山水1 小时前
销量预测模型评估进阶:从 MAPE 到库存周转率与缺货损失金额
人工智能·机器学习·微信·微信公众平台·微信开放平台
weixin_468466851 小时前
Airtable 零基础快速上手与实战指南
数据库·人工智能·python·深度学习·ai·大模型
sulikey1 小时前
大模型插件
人工智能
2301_818527781 小时前
冲锋衣可持续发展之路——AI助力绿色制造
人工智能
mit6.8241 小时前
Prompt caching is all we need | Ralph
人工智能
AI服务老曹1 小时前
统一视界:基于 Docker+GB28181+RTSP 的边缘计算 AI 视频管理平台协议兼容架构解析(附源码交付)
人工智能·docker·边缘计算