当摄像头运动时,背景本身就在全局变化,背景差分法就不再适用了。这时就需要光流法。
简单来说,光流就是像素点在图像中的瞬时运动速度。当摄像头移动时,几乎所有像素都在运动,但运动物体产生的光流,其方向和大小会与背景的全局光流明显不同。
核心原理:稀疏光流与稠密光流
实现光流法主要有两种思路:
- 稀疏光流:只追踪图像中一些“好追踪”的特征点(比如角点)。计算快,但只能得到稀疏的运动信息。
- 稠密光流:计算图像中每一个像素点的光流。信息丰富,但计算量巨大。
对于“动静检测”这个需求,稀疏光流法(典型代表是Lucas-Kanade 算法)是更主流和高效的选择。
实现流程:三步走
第一步:找到“好”的特征点
不是所有点都适合追踪,算法会自动选择那些纹理变化明显的点(如角点、边缘交汇处)。在OpenCV中,通常使用cv2.goodFeaturesToTrack来完成。
第二步:追踪这些特征点
使用Lucas-Kanade 光流算法,在下一帧图像中找出上一帧特征点的新位置。算法会假设特征点在一个小邻域内所有像素的运动是一致的,然后通过解方程找到最佳匹配。
第三步:通过运动矢量做判断
这一步是关键。得到两帧之间所有特征点的运动矢量后,我们就可以从中分离出“动”(运动物体)与“静”(背景)。
一个典型的逻辑是:
- 估计背景运动:摄像头运动时,绝大部分特征点的运动属于背景。可以通过计算所有运动矢量的中位数或众数,来得到一个代表背景的全局运动矢量。
- 识别异常运动:找出那些方向或大小与全局运动矢量差异显著的特征点。这些“离群点”就附着在运动的物体上。
代码实战:用稀疏光流检测摄像头运动下的移动物体
下面的代码展示了完整的处理流程,从读取视频到找出运动物体。
importcv2importnumpyasnp# 参数配置LK_PARAMS=dict(winSize=(15,15),maxLevel=2,criteria=(cv2.TERM_CRITERIA_EPS|cv2.TERM_CRITERIA_COUNT,10,0.03))# 特征点追踪的颜色COLOR=(0,255,0)defdraw_flow_and_mask(frame,good_old,good_new,status):"""绘制追踪点和运动矢量"""mask=np.zeros_like(frame)fori,(new,old)inenumerate(zip(good_new,good_old)):ifstatus[i]:# 只绘制成功追踪的点a,b=new.ravel().astype(int),old.ravel().astype(int)mask=cv2.line(mask,tuple(a),tuple(b),COLOR,1)mask=cv2.circle(mask,tuple(a),2,COLOR,-1)returncv2.add(frame,mask)# 1. 打开视频(或摄像头)cap=cv2.VideoCapture('path/to/your/video.mp4')# 替换为你的视频路径# 读取第一帧,并寻找特征点ret,old_frame=cap.read()old_gray=cv2.cvtColor(old_frame,cv2.COLOR_BGR2GRAY)# Shi-Tomasi 角点检测,找出最适合追踪的点p0=cv2.goodFeaturesToTrack(old_gray,mask=None,maxCorners=100,qualityLevel=0.3,minDistance=7,blockSize=7)whileTrue:ret,frame=cap.read()ifnotret:breakframe_gray=cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)# 2. 核心:计算稀疏光流(追踪前一帧的特征点在当前帧的新位置)# p1: 新位置, st: 状态(1=成功追踪, 0=失败), err: 误差p1,st,err=cv2.calcOpticalFlowPyrLK(old_gray,frame_gray,p0,None,**LK_PARAMS)# 筛选成功追踪的点good_new=p1[st==1]good_old=p0[st==1]# 3. 分析运动矢量,判断动静# --- 方法一:仅凭肉眼观察(可视化)---# 将所有追踪点和运动矢量画出来,用肉眼判断哪些点的运动“格格不入”visual_frame=draw_flow_and_mask(frame,good_old,good_new,st[st==1])# --- 方法二:用算法自动判断 ---# 计算所有成功追踪点的运动矢量(位移)movements=good_new-good_oldiflen(movements)>0:# 计算运动矢量的中位数,作为背景运动的估计median_movement=np.median(movements,axis=0)# 设置一个阈值,判断哪些点的运动与背景估计差异过大THRESHOLD=3.0# 可调节的敏感度参数motion_diff=np.linalg.norm(movements-median_movement,axis=1)# 找出运动“异常”的点,这些点很可能属于运动物体motion_points=good_new[motion_diff>THRESHOLD]# 在原图上高亮显示这些“动点”forptinmotion_points:cv2.circle(visual_frame,tuple(pt.astype(int)),4,(0,0,255),-1)# 红色大圆点# 显示结果cv2.imshow('Sparse Optical Flow',visual_frame)# 更新前一帧的数据,为下一轮循环做准备old_gray=frame_gray.copy()p0=good_new.reshape(-1,1,2)# 将追踪点更新为当前帧找到的新位置ifcv2.waitKey(30)&0xFF==ord('q'):breakcap.release()cv2.destroyAllWindows()如何进一步优化?
- 加速运算:每处理N帧才重新检测一次特征点,中间帧直接沿用上次的点进行追踪。
- 提高鲁棒性:用RANSAC算法来剔除误匹配的“野点”,使背景运动估计更准确。
- 获取物体轮廓:稀疏光流只给出一堆“动点”。如果想得到物体完整的运动区域,需要结合稠密光流(如
cv2.calcOpticalFlowFarneback),然后用聚类或分割算法将这些“动点”聚成团。
总的来说,稀疏光流法是处理摄像头运动时检测动静的利器。它不像背景减法那样需要一个静态的背景假设,而是通过分析所有像素的运动模式,找出那些“不随大流”的异常点,从而锁定运动的物体。