第一次接触监控视频分析时,我盯着屏幕上闪烁的画面发愁:怎么让计算机自动识别画面里走动的人?这就是运动目标检测要解决的问题。简单来说,就是让计算机在连续的视频帧中找到"会动的东西"。
你可能见过商场里的人流统计摄像头,或是公路上抓拍违章的电子眼,它们背后都依赖这项技术。在实际项目中,我发现没有哪种算法能通吃所有场景。就像厨师做菜要选对刀具一样,我们需要根据场景特点选择算法组合:
举个例子,停车场车辆检测用帧差法就够用,但要是分析十字路口行人过街的轨迹,就需要光流法来追踪运动方向。我在某商场项目里就吃过亏——开始只用背景减除法,结果玻璃反光导致误报一堆"幽灵目标",后来结合帧差法才解决问题。
记得第一次实现帧差法时,我用了不到20行Python代码就做出了运动检测效果:
python复制import cv2
cap = cv2.VideoCapture('walking.mp4')
_, prev = cap.read()
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
while True:
_, curr = cap.read()
curr_gray = cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY)
diff = cv2.absdiff(curr_gray, prev_gray)
_, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
cv2.imshow('Motion', thresh)
prev_gray = curr_gray.copy()
if cv2.waitKey(30) == 27:
break
这段代码的核心就是计算当前帧与前帧的灰度差,然后二值化处理。但实际使用时我发现几个坑:
python复制kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
在检测快速移动的车辆时,我发现两帧差法会出现"拖尾"现象。这时三帧差法就派上用场了:
python复制frame1 = cv2.cvtColor(cap.read()[1], cv2.COLOR_BGR2GRAY)
frame2 = cv2.cvtColor(cap.read()[1], cv2.COLOR_BGR2GRAY)
frame3 = cv2.cvtColor(cap.read()[1], cv2.COLOR_BGR2GRAY)
diff1 = cv2.absdiff(frame1, frame2)
diff2 = cv2.absdiff(frame2, frame3)
motion = cv2.bitwise_and(diff1, diff2)
三帧差法通过两次差分再求与运算,能有效减少重影问题。实测在检测时速60km以上的车辆时,目标完整性比两帧法提升约40%。
在需要分析运动方向的场景,比如统计人流走向时,光流法就是利器。OpenCV提供的稀疏光流实现非常高效:
python复制# 初始化
feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7)
lk_params = dict(winSize=(15,15), maxLevel=2)
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
# 计算光流
new_gray = cv2.cvtColor(new_frame, cv2.COLOR_BGR2GRAY)
p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, new_gray, p0, None, **lk_params)
# 绘制轨迹
for i,(new,old) in enumerate(zip(p1,p0)):
a,b = new.ravel()
c,d = old.ravel()
mask = cv2.line(mask, (a,b),(c,d), color=(0,255,0), thickness=2)
这个案例中我设置了几个关键参数:
maxCorners控制跟踪点数量,人少时设50-100,密集场景可到300qualityLevel决定特征点质量,值越小检测点越多winSize是搜索窗口,目标移动快时要适当增大在分析液体流动或人群密集场景时,我改用Farneback稠密光流:
python复制flow = cv2.calcOpticalFlowFarneback(prev_gray, curr_gray, None,
pyr_scale=0.5, levels=3, winsize=15,
iterations=3, poly_n=5, poly_sigma=1.2,
flags=0)
这里有个调参技巧:poly_n和poly_sigma控制算法平滑度,处理烟雾、水流等模糊运动时,建议设为7和1.5。我曾用这个方法成功分析过化工厂的废气排放扩散模式。
背景减除法的核心是建立背景模型。最常用的是MOG2算法:
python复制fgbg = cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=16,
detectShadows=True)
fgmask = fgbg.apply(frame)
几个关键参数经验值:
history:室内场景建议500,室外动态场景设200足够varThreshold:光照稳定时16-25,阴雨天气可降到10detectShadows:行人检测建议开启,车辆检测可关闭在某商场项目中,我发现MOG2对突然的光照变化(比如云层移动)特别敏感。后来通过设置varThreshold=36并配合形态学闭运算才稳定下来。
当场景中有摇曳的树木或飘动的旗帜时,KNN背景减除器表现更好:
python复制fgbg = cv2.createBackgroundSubtractorKNN(history=500, dist2Threshold=400,
detectShadows=False)
这里的dist2Threshold控制灵敏度,值越大越不容易检测到运动。在公园场景测试中,设为600能有效过滤树叶晃动,同时保留行人检测能力。
在24小时监控系统中,我设计了一套组合策略:
python复制def detect_motion(frame, is_night):
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if is_night: # 夜间模式
fgmask = knn_bgs.apply(gray)
_, thresh = cv2.threshold(fgmask, 244, 255, cv2.THRESH_BINARY)
else: # 白天模式
diff = cv2.absdiff(gray, prev_gray)
_, thresh = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)
# 通用后处理
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
cleaned = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
return cleaned
这个方案在某个小区项目中使误报率降低了65%,关键点在于:
分析十字路口车流时,我采用了三级检测策略:
python复制# 第一级检测
mog_mask = mog.apply(frame)
contours, _ = cv2.findContours(mog_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
if cv2.contourArea(cnt) > 500: # 过滤小区域
x,y,w,h = cv2.boundingRect(cnt)
# 第二级确认
roi = frame[y:y+h, x:x+w]
diff = cv2.absdiff(cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY), prev_roi)
if np.mean(diff) > 15:
# 第三级分析
tracks = optical_flow(roi)
update_traffic_stats(tracks)
这种组合方案在实测中达到92%的车辆轨迹识别准确率,比单一算法提升约30%。