ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

OpenCV 光流全解析:Lucas-Kanade 稀疏光流与 Farneback 稠密光流实战指南

OpenCV 光流全解析:Lucas-Kanade 稀疏光流与 Farneback 稠密光流实战指南 OpenCV 光流全解析Lucas-Kanade 稀疏光流与 Farneback 稠密光流实战指南【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv本篇文章围绕 OpenCV 官方教程 optical_flow.markdown 展开系统讲解光流Optical Flow的数学原理、两大假设与求解思路并基于本仓库的官方示例与 video 模块源码 完整演示如何用cv.calcOpticalFlowPyrLK()追踪视频中的 Shi-Tomasi 角点、如何用cv.calcOpticalFlowFarneback()生成全图稠密运动场并做 HSV 可视化。读完你将能够独立在 C / Python / Java 工程中实现稀疏特征点追踪与稠密运动估计并理解每个关键参数背后的物理与算法含义。什么是光流光流描述的是相邻两帧之间图像对象表现出的表观运动模式apparent motion它既可以由物体自身的运动引起也可以由相机的运动引起。从数学上看光流是一个二维矢量场其中每个矢量表示某个像素点从第一帧到第二帧的位移(dx, dy)。上面这幅示意图里一个小球连续出现在 5 帧画面中箭头即表示其位移矢量。正因为光流直接给出相邻帧间每个可见点的运动信息它拥有广泛的应用场景例如运动恢复结构Structure from Motion视频压缩Video Compression视频稳像Video Stabilization目标跟踪、动作识别、自动驾驶场景感知等光流的两大基本假设光流求解之所以可行建立在两个核心假设之上亮度恒定物体上的像素在两帧之间灰度值不发生改变邻域运动一致相邻像素具有相似的运动即空间平滑性。以第一帧中的某个像素I(x, y, t)为例注意这里多了一个时间维度t因为光流处理的是视频序列而非静态图像经过时间dt后它移动到下一帧的位置(xdx, ydy)。由于是同一个点且亮度不变有$$I(x, y, t) I(xdx, ydy, tdt)$$对右端做一阶泰勒级数展开、消去公共项并除以dt就得到光流方程$$f_x u f_y v f_t 0$$其中$f_x \frac{\partial f}{\partial x}$、$f_y \frac{\partial f}{\partial y}$ 是图像空间梯度$f_t$ 是沿时间轴的梯度$u \frac{dx}{dt}$、$v \frac{dy}{dt}$ 是待求的光流速度分量。问题在于方程中fx、fy、ft都可以从图像中计算出来但(u, v)是两个未知量——一个方程解两个未知数是欠定问题。这正是后续各种光流算法要解决的核心矛盾Lucas-Kanade 就是其中最具代表性的解法。Lucas-Kanade 方法用邻域约束求解回顾前面“邻域运动一致”的假设一个 3×3 窗口内的 9 个像素应具有相同的运动(u, v)。于是可以对窗口内每个像素分别建立光流方程得到 9 个方程、2 个未知量——超定方程组通过最小二乘拟合得到最优解。最终的解析结果为$$ \begin{bmatrix} u \ v \end{bmatrix} \begin{bmatrix} \sum_{i}{f_{x_i}}^2 \sum_{i}{f_{x_i} f_{y_i} } \ \sum_{i}{f_{x_i} f_{y_i}} \sum_{i}{f_{y_i}}^2 \end{bmatrix}^{-1} \begin{bmatrix} - \sum_{i}{f_{x_i} f_{t_i}} \ - \sum_{i}{f_{y_i} f_{t_i}} \end{bmatrix} $$值得注意上式中的 2×2 矩阵与 Harris 角点检测器中的自相关矩阵形式非常相似。这意味着角点特征明显的点才是适合被光流追踪的候选点——这正是后面的示例代码先用cv.goodFeaturesToTrack()检测 Shi-Tomasi 角点、再对其追踪的原因。金字塔解决大运动问题上述推导建立在“小位移”的隐式假设上当目标运动幅度较大时直接求解会失败。OpenCV 的解决方案是图像金字塔逐层向上缩放图像在高层原来的小运动被滤除、大运动被压缩成“小运动”再在这一层上应用 Lucas-Kanade即可在获得光流的同时得到运动尺度信息。这就是calcOpticalFlowPyrLK中 PyrPyramid的由来。Lucas-Kanade 稀疏光流实战追踪视频中的角点OpenCV 将上述全部流程封装进单个函数cv.calcOpticalFlowPyrLK()。其官方声明位于 modules/video/include/opencv2/video/tracking.hpp#L186-L191CV_EXPORTS_W void calcOpticalFlowPyrLK( InputArray prevImg, InputArray nextImg, InputArray prevPts, InputOutputArray nextPts, OutputArray status, OutputArray err, Size winSize Size(21,21), int maxLevel 3, TermCriteria criteria TermCriteria(TermCriteria::COUNTTermCriteria::EPS, 30, 0.01), int flags 0, double minEigThreshold 1e-4 );其底层实现在 modules/video/src/lkpyramid.cpp#L1099 的SparsePyrLKOpticalFlowImpl::calc()中算法本身对应 Bouguet 的经典论文Pyramidal Implementation of the Lucas Kanade Feature Tracker。参数详解参数说明示例值 / 默认值prevImg/nextImg前一帧与后一帧的 8 位单通道灰度图或由buildOpticalFlowPyramid预构建的金字塔—prevPts需要追踪的二维点集单精度浮点坐标即特征点角点坐标p0nextPts输出特征点在后一帧中的新位置p1status输出状态数组元素为 1 表示该点光流成功求得为 0 表示失败uchar向量err输出误差向量误差度量类型由flags决定失败点的误差未定义float向量winSize每个金字塔层的搜索窗口大小示例用(15, 15)默认(21, 21)maxLevel金字塔最大层数0 起算。为 0 表示不使用金字塔单层为 1 表示两层依此类推示例用2默认3criteria迭代搜索的终止条件达到最大迭代次数maxCount或搜索窗口移动小于epsilon(COUNTEPS, 10, 0.03)flagsOPTFLOW_USE_INITIAL_FLOW把nextPts当作初始估计OPTFLOW_LK_GET_MIN_EIGENVALS用最小特征值作为误差度量配合minEigThreshold否则用窗口内 L1 距离均值默认0minEigThreshold算法计算 2×2 光流方程矩阵除以窗口像素数的最小特征值若小于该阈值则该特征点被过滤、不参与求解可剔除坏点并提升性能默认1e-4完整示例代码C本仓库对应的官方示例位于 samples/cpp/tutorial_code/video/optical_flow/optical_flow.cpp完整流程为读取首帧 →goodFeaturesToTrack检测角点 → 循环中逐帧调用calcOpticalFlowPyrLK递推追踪 → 按status筛选有效点并绘制轨迹#include iostream #include opencv2/core.hpp #include opencv2/highgui.hpp #include opencv2/imgproc.hpp #include opencv2/features.hpp #include opencv2/videoio.hpp #include opencv2/video.hpp using namespace cv; using namespace std; int main(int argc, char **argv) { const string about This sample demonstrates Lucas-Kanade Optical Flow calculation.\n The example file can be downloaded from: slow_traffic_small.mp4; const string keys { h help | | print this help message } { image | vtest.avi | path to image file }; CommandLineParser parser(argc, argv, keys); parser.about(about); if (parser.has(help)) { parser.printMessage(); return 0; } string filename samples::findFile(parser.getstring(image)); if (!parser.check()) { parser.printErrors(); return 0; } VideoCapture capture(filename); if (!capture.isOpened()) { cerr Unable to open file! endl; return 0; } // Create some random colors vectorScalar colors; RNG rng; for(int i 0; i 100; i) { int r rng.uniform(0, 256), g rng.uniform(0, 256), b rng.uniform(0, 256); colors.emplace_back(r, g, b); } Mat old_frame, old_gray; vectorPoint2f p0, p1; // Take first frame and find corners in it capture old_frame; cvtColor(old_frame, old_gray, COLOR_BGR2GRAY); goodFeaturesToTrack(old_gray, p0, 100, 0.3, 7, Mat(), 7, false, 0.04); // Create a mask image for drawing purposes Mat mask Mat::zeros(old_frame.size(), old_frame.type()); while(true){ Mat frame, frame_gray; capture frame; if (frame.empty()) break; cvtColor(frame, frame_gray, COLOR_BGR2GRAY); // calculate optical flow vectoruchar status; vectorfloat err; TermCriteria criteria TermCriteria((TermCriteria::COUNT) (TermCriteria::EPS), 10, 0.03); calcOpticalFlowPyrLK(old_gray, frame_gray, p0, p1, status, err, Size(15,15), 2, criteria); vectorPoint2f good_new; for(uint i 0; i p0.size(); i) { // Select good points if(status[i] 1) { good_new.push_back(p1[i]); // draw the tracks line(mask, p1[i], p0[i], colors[i], 2); circle(frame, p1[i], 5, colors[i], -1); } } Mat img; add(frame, mask, img); imshow(Frame, img); int keyboard waitKey(30); if (keyboard q || keyboard 27) break; // Now update the previous frame and previous points old_gray frame_gray.clone(); p0 good_new; } }完整示例代码PythonPython 版本见 samples/python/tutorial_code/video/optical_flow/optical_flow.py逻辑与 C 一一对应使用dict集中管理两组参数便于阅读与调参import numpy as np import cv2 as cv import argparse parser argparse.ArgumentParser(descriptionThis sample demonstrates Lucas-Kanade Optical Flow calculation.) parser.add_argument(image, typestr, helppath to image file) args parser.parse_args() cap cv.VideoCapture(args.image) # params for ShiTomasi corner detection feature_params dict( maxCorners 100, qualityLevel 0.3, minDistance 7, blockSize 7 ) # Parameters for lucas kanade optical flow lk_params dict( winSize (15, 15), maxLevel 2, criteria (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03)) # Create some random colors color np.random.randint(0, 255, (100, 3)) # Take first frame and find corners in it ret, old_frame cap.read() old_gray cv.cvtColor(old_frame, cv.COLOR_BGR2GRAY) p0 cv.goodFeaturesToTrack(old_gray, mask None, **feature_params) # Create a mask image for drawing purposes mask np.zeros_like(old_frame) while(1): ret, frame cap.read() if not ret: print(No frames grabbed!) break frame_gray cv.cvtColor(frame, cv.COLOR_BGR2GRAY) # calculate optical flow p1, st, err cv.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params) # Select good points if p1 is not None: good_new p1[st1] good_old p0[st1] # draw the tracks for i, (new, old) in enumerate(zip(good_new, good_old)): a, b new.ravel() c, d old.ravel() mask cv.line(mask, (int(a), int(b)), (int(c), int(d)), color[i].tolist(), 2) frame cv.circle(frame, (int(a), int(b)), 5, color[i].tolist(), -1) img cv.add(frame, mask) cv.imshow(frame, img) k cv.waitKey(30) 0xff if k 27: break # Now update the previous frame and previous points old_gray frame_gray.copy() p0 good_new.reshape(-1, 1, 2) cv.destroyAllWindows()Java 对应版本可参考 samples/java/tutorial_code/video/optical_flow/OpticalFlowDemo.java。关键实现细节说明运行方式与输入C 示例通过CommandLineParser支持-h帮助与image参数默认加载vtest.avi该测试视频位于本仓库 samples/data 目录也可换用其他交通监控类视频演示教程演示的slow_traffic_small.mp4即为此类素材。特征点初始化goodFeaturesToTrack对应 Shi-Tomasi 角点检测参数maxCorners100最多 100 个点、qualityLevel0.3最小质量水平、minDistance7点间最小距离、blockSize7邻域尺寸。PyTorch 层面对应于追踪窗口(15,15)与金字塔层数2。筛选与绘制status[i]1的点才是可信点用line在累加mask上画轨迹、用实心circle标记当前帧位置最后add(frame, mask, img)将轨迹叠加回画面。递推更新每帧处理完把当前frame_gray克隆为old_gray、把good_new作为下一轮的p0实现逐帧连续追踪。运行得到的效果如图所示彩色轨迹清晰地勾勒出车辆的运动路径使追踪更鲁棒定时重检角点 反向校验教程原文特别提醒上述示例没有校验追踪到的关键点是否正确——即使某特征点在画面中消失光流也可能“就近”给出一个看起来相近的错误点。因此真正稳健的追踪应该每隔固定帧数重新检测角点而不是只初始化一次做一次反向光流校验backward-check把前向追踪得到的点p1作为输入、在(next, prev)方向再做一次calcOpticalFlowPyrLK得到p0r若p0与p0r的距离足够小如 1 像素才认为该点追踪可信只保留这些“好点”。OpenCV 官方提供了这种更完备的示例即 samples/python/snippets/lk_track.py它以detect_interval 5每 5 帧重新执行一次goodFeaturesToTrack并把已有轨迹终点附近的区域在 mask 中排除避免重复检测通过反向追踪残差abs(p0-p0r)过滤野点并用长度为track_len 10的滑动轨迹记录每个点的历史位置后绘制折线。C 侧同类演示见 samples/cpp/lkdemo.cpp若存在。这套“前向追踪 反向校验 周期重检”的组合是工程化稀疏追踪的标准范式。Farneback 稠密光流为全图每个像素计算运动Lucas-Kanade 只针对稀疏特征点集示例中为 Shi-Tomasi 角点计算光流。当我们需要画面中每个像素的运动时可以用 OpenCV 提供的稠密光流算法cv.calcOpticalFlowFarneback()。它基于 Gunnar Farneback 2003 年的论文Two-Frame Motion Estimation Based on Polynomial Expansion其核心思想是对每个像素邻域做局部多项式展开来近似图像再通过比较两帧的多项式系数来估计位移场。函数声明位于 modules/video/include/opencv2/video/tracking.hpp#L235-L238底层实现位于 modules/video/src/optflowgf.cpp#L1098CV_EXPORTS_W void calcOpticalFlowFarneback( InputArray prev, InputArray next, InputOutputArray flow, double pyr_scale, int levels, int winsize, int iterations, int poly_n, double poly_sigma, int flags );参数详解参数说明示例值 / 建议prev/next前后两帧的 8 位单通道输入图像—flow输出光流场尺寸与输入相同、类型为CV_32FC2两通道分别存放(u, v)—pyr_scale金字塔每层的缩放系数1。0.5即经典金字塔每层尺寸减半0.5levels金字塔层数含原始图像。1表示不建金字塔只用原图3winsize平均窗口大小。越大对噪声越鲁棒、越能捕捉快速运动但运动场会更模糊15iterations每个金字塔层的迭代次数3poly_n计算多项式展开所用的像素邻域大小。越大表面拟合越平滑、越鲁棒但场越模糊典型取 5 或 75poly_sigma平滑多项式展开所用高斯的标准差。poly_n5时建议1.1poly_n7时建议1.51.2flagsOPTFLOW_USE_INITIAL_FLOW把输入 flow 作为初始估计、OPTFLOW_FARNEBACK_GAUSSIAN用高斯窗替代同尺寸 box 窗通常更精确但更慢需配合更大winsize0输出满足prev(y,x) ≈ next(y flow(y,x)[1], x flow(y,x)[0])即flow两个通道分别给出了x与y方向OpenCL 加速版本见 modules/video/src/opencl/optical_flow_farneback.cl。完整示例代码C示例位于 samples/cpp/tutorial_code/video/optical_flow/optical_flow_dense.cpp。因为得到的是包含(u, v)两分量的 2 通道浮点数组代码先用cartToPolar把直角坐标换成幅度magnitude与方向角angle再把 HSV 色彩空间映射到光流场Hue 通道对应运动方向、Value 通道对应运动幅度从而把不可见的矢量场变成直观的彩色图#include iostream #include opencv2/core.hpp #include opencv2/highgui.hpp #include opencv2/imgproc.hpp #include opencv2/videoio.hpp #include opencv2/video.hpp using namespace cv; using namespace std; int main() { VideoCapture capture(samples::findFile(vtest.avi)); if (!capture.isOpened()) { cerr Unable to open file! endl; return 0; } Mat frame1, prvs; capture frame1; cvtColor(frame1, prvs, COLOR_BGR2GRAY); while(true){ Mat frame2, next; capture frame2; if (frame2.empty()) break; cvtColor(frame2, next, COLOR_BGR2GRAY); Mat flow(prvs.size(), CV_32FC2); calcOpticalFlowFarneback(prvs, next, flow, 0.5, 3, 15, 3, 5, 1.2, 0); // visualization Mat flow_parts[2]; split(flow, flow_parts); Mat magnitude, angle, magn_norm; cartToPolar(flow_parts[0], flow_parts[1], magnitude, angle, true); normalize(magnitude, magn_norm, 0.0f, 1.0f, NORM_MINMAX); angle * ((1.f / 360.f) * (180.f / 255.f)); //build hsv image Mat _hsv[3], hsv, hsv8, bgr; _hsv[0] angle; _hsv[1] Mat::ones(angle.size(), CV_32F); _hsv[2] magn_norm; merge(_hsv, 3, hsv); hsv.convertTo(hsv8, CV_8U, 255.0); cvtColor(hsv8, bgr, COLOR_HSV2BGR); imshow(frame2, bgr); int keyboard waitKey(30); if (keyboard q || keyboard 27) break; prvs next; } }完整示例代码PythonPython 版本见 samples/python/tutorial_code/video/optical_flow/optical_flow_dense.py。与 C 不同的是它把整张 HSV 图像预先置为全饱和hsv[..., 1] 255每帧只更新 Hue 与 Value 平面此外按下s键会把原始帧与 HSV 可视化结果分别保存为opticalfb.png与opticalhsv.pngimport numpy as np import cv2 as cv cap cv.VideoCapture(cv.samples.findFile(vtest.avi)) ret, frame1 cap.read() prvs cv.cvtColor(frame1, cv.COLOR_BGR2GRAY) hsv np.zeros_like(frame1) hsv[..., 1] 255 while(1): ret, frame2 cap.read() if not ret: print(No frames grabbed!) break next cv.cvtColor(frame2, cv.COLOR_BGR2GRAY) flow cv.calcOpticalFlowFarneback(prvs, next, None, 0.5, 3, 15, 3, 5, 1.2, 0) mag, ang cv.cartToPolar(flow[..., 0], flow[..., 1]) hsv[..., 0] ang*180/np.pi/2 hsv[..., 2] cv.normalize(mag, None, 0, 255, cv.NORM_MINMAX) bgr cv.cvtColor(hsv, cv.COLOR_HSV2BGR) cv.imshow(frame2, bgr) k cv.waitKey(30) 0xff if k 27: break elif k ord(s): cv.imwrite(opticalfb.png, frame2) cv.imwrite(opticalhsv.png, bgr) prvs next cv.destroyAllWindows()Java 对应版本见 samples/java/tutorial_code/video/optical_flow/OpticalFlowDenseDemo.java。运行效果图如下——不同颜色代表不同的运动方向颜色的亮度Value反映运动速度的大小画面中背景静止区域呈暗色运动车辆则被鲜明地“染色”进阶以类封装使用光流、光流文件读写除了函数式接口本仓库还在 modules/video/include/opencv2/video/tracking.hpp 中提供了面向对象的封装供追求可配置性与扩展性的工程使用SparsePyrLKOpticalFlow声明于 tracking.hpp#L882-L902带金字塔的 LK 稀疏光流通过create()构造可setWinSize/setMaxLevel配置窗口与金字塔层数等价于calcOpticalFlowPyrLKFarnebackOpticalFlowtracking.hpp#L653与DISOpticalFlowtracking.hpp#L768实现见 modules/video/src/dis_flow.cpp均派生自DenseOpticalFlowcalc()方法接收两帧、输出稠密光流场其中 DISDense Inverse Search以更快的速度换取接近 Farneback 的质量适合实时场景光流场文件读写cv.readOpticalFlow()/cv.writeOpticalFlow()tracking.hpp#L591-L601支持.flo格式Float Flow的存取实现见 modules/video/src/optical_flow_io.cpp便于把稠密光流结果保存下来做离线分析或数据集交换。小结本文以官方教程 optical_flow.markdown 为脉络覆盖了从理论到实践的完整链路原理层光流基于亮度恒定与邻域运动一致两大假设由光流方程 $f_x u f_y v f_t 0$ 出发用局部最小二乘Lucas-Kanade解决欠定问题用金字塔化解大位移稀疏追踪goodFeaturesToTrack检测 Shi-Tomasi 角点 calcOpticalFlowPyrLK逐帧递推配套status筛选、误差输出以及“周期重检 反向校验”的鲁棒化改造参考 samples/python/snippets/lk_track.py稠密估计calcOpticalFlowFarneback为全图像素输出 2 通道CV_32FC2位移场用“Hue方向、Value幅度”的 HSV 映射完成可视化。若想进一步深入源码建议通读 lkpyramid.cpp金字塔构建与迭代求解、optflowgf.cpp多项式展开与全局匹配以及各自的 OpenCL 内核 optical_flow_farneback.cl并把 optical_flow.cpp 与 optical_flow_dense.cpp 作为上手调试的起点。【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表