OpenCV实例(八)行人跟踪
作者:Xiou
1.目标跟踪概述
目标跟踪是对摄像头视频中的移动目标进行定位的过程,它有着广泛的应用,本章将介绍这一主题。实时目标跟踪是许多计算机视觉应用的重要任务,例如监控(surveillance)、基于感知的(perceptual)用户界面、增强现实、基于对象的视频压缩以及辅助驾驶等。
可用多种方式实现目标跟踪,而最优的跟踪技术在很大程度跟具体任务有关。
为了跟踪视频中的所有目标,首先要完成的任务是识别视频帧中那些可能包含移动目标的区域。有很多实现视频目标跟踪的方法,这些方法的目的稍微不同。例如,当跟踪所有移动目标时,帧之间的差异会变得有用;当跟踪视频中移动的手时,基于皮肤颜色的均值漂移方法是最好的解决方案;当知道跟踪对象的一方面时,模板匹配会是不错的技术。
2.基于背景差分检测运动物体
2.1 实现基本背景差分器
实现基本背景差分器为了实现基本背景差分器,我们采用以下几步:
(1)开始用摄像头捕捉帧。
(2)丢弃9帧,这样摄像头才有时间适当调整自动曝光,以适应场景中的光照条件。(3)取第10帧,将其转换为灰度图像,对其进行模糊,并把模糊图像作为背景的参考图像。
(4)对于每个后续帧,对其进行模糊,将其转换为灰度图像,再计算模糊帧和背景参考图像之间的绝对差值。对差值图像进行阈值化、平滑和轮廓检测处理。绘制并显示主要轮廓的边框。
将前面的几步展开为更小的步骤,我们可以考虑用8个连续的代码块来实现脚本:(1)首先,导入OpenCV,并定义blur、erode和dilate运算的核的大小:
(2)试着从摄像头捕捉10帧:
(3)如果无法采集到10帧,就退出。否则,将第10帧图像转换为灰度图像,并对其进行模糊:
(4)在这一阶段,我们有了背景的参考图像。现在,我们继续采集更多帧,这样就可以检测运动物体了。对每一帧的处理都从灰度转换和高斯模糊操作开始:
(5)现在,我们对当前帧的模糊、灰度版本,以及背景图像的模糊、灰度版本进行比较。具体来说,我们将使用OpenCV的cv2.absdiff函数求这两幅图像之间差值的绝对值(或大小)。然后,应用阈值来获得纯黑白图像,并通过形态学运算对阈值化图像进行平滑处理。以下是相关的代码:
(6)现在,如果技术运行良好,阈值图像应该在运动物体处包含白色斑点。我们想找到白色斑点的轮廓,并在其周围绘制边框。为了进一步过滤可能不是真实物体的微小变化,我们将应用一个基于轮廓面积的阈值。如果轮廓太小,就认为它不是真正的运动物体。(当然,“太小”的界定可能会因摄像头的分辨率和应用程序而有所不同,在某些情况下,你可能根本不希望应用此测试。)下面是检测轮廓和绘制边框的代码:
(7)显示差值图像、阈值化图像以及带有矩形边框的检测结果:
(8)继续读取帧,直到用户按下Esc键退出:
代码实例:
import cv2OPENCV_MAJOR_VERSION = int(cv2.__version__.split('.')[0])BLUR_RADIUS = 21
erode_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))cap = cv2.VideoCapture(0)# Capture several frames to allow the camera's autoexposure to adjust.
for i in range(10):success, frame = cap.read()
if not success:exit(1)gray_background = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray_background = cv2.GaussianBlur(gray_background,(BLUR_RADIUS, BLUR_RADIUS), 0)success, frame = cap.read()
while success:gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)gray_frame = cv2.GaussianBlur(gray_frame,(BLUR_RADIUS, BLUR_RADIUS), 0)diff = cv2.absdiff(gray_background, gray_frame)_, thresh = cv2.threshold(diff, 40, 255, cv2.THRESH_BINARY)cv2.erode(thresh, erode_kernel, thresh, iterations=2)cv2.dilate(thresh, dilate_kernel, thresh, iterations=2)if OPENCV_MAJOR_VERSION >= 4:# OpenCV 4 or a later version is being used.contours, hier = cv2.findContours(thresh, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)else:# OpenCV 3 or an earlier version is being used.# cv2.findContours has an extra return value.# The extra return value is the thresholded image, which is# unchanged, so we can ignore it._, contours, hier = cv2.findContours(thresh, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)for c in contours:if cv2.contourArea(c) > 4000:x, y, w, h = cv2.boundingRect(c)cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 255, 0), 2)cv2.imshow('diff', diff)cv2.imshow('thresh', thresh)cv2.imshow('detection', frame)k = cv2.waitKey(1)if k == 27: # Escapebreaksuccess, frame = cap.read()
输出结果:
2.2 使用MOG背景差分器
(1)用MOG背景差分器替换基本背景差分模型。
(2)使用视频文件(而不是摄像头)作为输入。
(3)取消高斯模糊。
(4)调整阈值、形态学和轮廓分析步骤中使用的参数。
这些修改会影响位于整个脚本中几个不同地方的几行代码。靠近脚本的顶部,我们初始化MOG背景差分器并修改形态学核的大小,如下面代码块中的粗体所示:
注意OpenCV提供了一个函数cv2.createBackgroundSubtractorMOG2来创建cv2.BackgroundSubtractorMOG2实例。该函数接受一个参数detectShadows,将其设置为True,就会标记出阴影区域,而不会标记为前景的一部分。
输出结果:
由于抛光的地板和墙壁,这个场景不仅包含阴影,也包含反射内容。当启用阴影检测时,我们可以使用一个阈值来移除掩模上的阴影和反射部分,大厅里的人周围只留下一个准确的检测矩形。可是,当禁用阴影检测时,有两个检测结果,可以说,两个检测结果都是不准确的。其中一个覆盖了男子及其影子和地板上的倒影。第二个则覆盖了该男子在墙上的倒影。可以说,这些都是不准确的检测,因为即使这个人的影子和反射是运动物体的视觉产物,但是它们并不是真正的运动物体。
2.3 使用卡尔曼滤波器寻找运动趋势
卡尔曼滤波器主要(但不完全)是由鲁道夫·卡尔曼(Rudolf Kalman)在20世纪50年代后期开发出来的一种算法。卡尔曼滤波器在许多领域都有实际应用,尤其是从核潜艇到飞机的各种交通工具的导航系统。
卡尔曼滤波器递归地对有噪声的输入数据流进行操作,以产生底层系统状态的统计最优估计。在计算机视觉背景下,卡尔曼滤波器可以对跟踪物体的位置进行平滑估计。
我们来考虑一个简单的例子。想象桌子上有一个红色小球,并想象有一个摄像头对着此场景。将球作为要跟踪的主体,然后用手指轻弹小球。球将根据运动规律开始在桌子上滚动。如果球在特定的方向上以1米/秒的速度滚动,那么很容易估计出球在1秒后的位置:它将在1米远的地方。卡尔曼滤波器应用这样的规律,根据先前收集到的帧的跟踪结果来预测当前视频帧中物体的位置。
卡尔曼滤波器本身并没有收集这些跟踪结果,但是它会根据来自另一种算法(如MeanShift)的跟踪结果更新物体的运动模型。自然,卡尔曼滤波器无法预见作用在球上的力(例如与桌上的铅笔的碰撞),但是它可以在事后根据跟踪结果更新它的运动模型。通过使用卡尔曼滤波器,我们可以获得比单独跟踪的结果更稳定、更符合运动规律的估计结果。
根据前面的描述,我们认为卡尔曼滤波器算法有两个阶段:
·预测(在第一阶段):卡尔曼滤波器使用计算到当前时间点的协方差估计物体的新位置。
·更新(在第二阶段):卡尔曼滤波器记录物体的位置,并调整协方差用于下一个计算周期。
代码实例:
import cv2
import numpy as np# Create a black image.
img = np.zeros((800, 800, 3), np.uint8)# Initialize the Kalman filter.
kalman = cv2.KalmanFilter(4, 2)
kalman.measurementMatrix = np.array([[1, 0, 0, 0],[0, 1, 0, 0]], np.float32)
kalman.transitionMatrix = np.array([[1, 0, 1, 0],[0, 1, 0, 1],[0, 0, 1, 0],[0, 0, 0, 1]], np.float32)
kalman.processNoiseCov = np.array([[1, 0, 0, 0],[0, 1, 0, 0],[0, 0, 1, 0],[0, 0, 0, 1]], np.float32) * 0.03last_measurement = None
last_prediction = Nonedef on_mouse_moved(event, x, y, flags, param):global img, kalman, last_measurement, last_predictionmeasurement = np.array([[x], [y]], np.float32)if last_measurement is None:# This is the first measurement.# Update the Kalman filter's state to match the measurement.kalman.statePre = np.array([[x], [y], [0], [0]], np.float32)kalman.statePost = np.array([[x], [y], [0], [0]], np.float32)prediction = measurementelse:kalman.correct(measurement)prediction = kalman.predict() # Gets a reference, not a copy# Trace the path of the measurement in green.cv2.line(img, (last_measurement[0], last_measurement[1]),(measurement[0], measurement[1]), (0, 255, 0))# Trace the path of the prediction in red.cv2.line(img, (last_prediction[0], last_prediction[1]),(prediction[0], prediction[1]), (0, 0, 255))last_prediction = prediction.copy()last_measurement = measurementcv2.namedWindow('kalman_tracker')
cv2.setMouseCallback('kalman_tracker', on_mouse_moved)while True:cv2.imshow('kalman_tracker', img)k = cv2.waitKey(1)if k == 27: # Escapecv2.imwrite('kalman.png', img)break
输出结果:
运行程序并移动鼠标。如果鼠标在高速下突然转弯,你会注意到预测线(红色)会比测量线(绿色)更宽。这是因为预测跟随鼠标运动到那时的动量。
3.跟踪行人
素材:
应用程序将遵循以下逻辑:
(1)从视频文件中捕获帧。
(2)使用前20帧来填充背景差分器的历史记录。
(3)基于背景差分,使用第21帧识别运动的前景物体。我们将把这些当作行人对待。对于每个行人,分配一个ID和一个初始跟踪窗口,然后计算直方图。
(4)对于随后的每一帧,使用卡尔曼滤波器和MeanShift跟踪每个行人。如果这是一个实际的应用程序,可能需要存储每个行人在场景中的路径记录,以便用户稍后对其进行分析。然而,这种类型的记录保存不在本示例中探讨。此外,在实际的应用程序中,需要确保能够识别进入场景的新的行人,但是目前我们将只专注于跟踪场景中靠近视频开始处的那些物体。
代码实例:
import cv2
import numpy as npOPENCV_MAJOR_VERSION = int(cv2.__version__.split('.')[0])class Pedestrian():"""A tracked pedestrian with a state including an ID, trackingwindow, histogram, and Kalman filter."""def __init__(self, id, hsv_frame, track_window):self.id = idself.track_window = track_windowself.term_crit = \\(cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS, 10, 1)# Initialize the histogram.x, y, w, h = track_windowroi = hsv_frame[y:y+h, x:x+w]roi_hist = cv2.calcHist([roi], [0], None, [16], [0, 180])self.roi_hist = cv2.normalize(roi_hist, roi_hist, 0, 255,cv2.NORM_MINMAX)# Initialize the Kalman filter.self.kalman = cv2.KalmanFilter(4, 2)self.kalman.measurementMatrix = np.array([[1, 0, 0, 0],[0, 1, 0, 0]], np.float32)self.kalman.transitionMatrix = np.array([[1, 0, 1, 0],[0, 1, 0, 1],[0, 0, 1, 0],[0, 0, 0, 1]], np.float32)self.kalman.processNoiseCov = np.array([[1, 0, 0, 0],[0, 1, 0, 0],[0, 0, 1, 0],[0, 0, 0, 1]], np.float32) * 0.03cx = x+w/2cy = y+h/2self.kalman.statePre = np.array([[cx], [cy], [0], [0]], np.float32)self.kalman.statePost = np.array([[cx], [cy], [0], [0]], np.float32)def update(self, frame, hsv_frame):back_proj = cv2.calcBackProject([hsv_frame], [0], self.roi_hist, [0, 180], 1)ret, self.track_window = cv2.meanShift(back_proj, self.track_window, self.term_crit)x, y, w, h = self.track_windowcenter = np.array([x+w/2, y+h/2], np.float32)prediction = self.kalman.predict()estimate = self.kalman.correct(center)center_offset = estimate[:,0][:2] - centerself.track_window = (x + int(center_offset[0]),y + int(center_offset[1]), w, h)x, y, w, h = self.track_window# Draw the predicted center position as a blue circle.cv2.circle(frame, (int(prediction[0]), int(prediction[1])),4, (255, 0, 0), -1)# Draw the corrected tracking window as a cyan rectangle.cv2.rectangle(frame, (x,y), (x+w, y+h), (255, 255, 0), 2)# Draw the ID above the rectangle in blue text.cv2.putText(frame, 'ID: %d' % self.id, (x, y-5),cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0),1, cv2.LINE_AA)def main():cap = cv2.VideoCapture('pedestrians.avi')# Create the KNN background subtractor.bg_subtractor = cv2.createBackgroundSubtractorKNN()history_length = 20bg_subtractor.setHistory(history_length)erode_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (8, 3))pedestrians = []num_history_frames_populated = 0while True:grabbed, frame = cap.read()if (grabbed is False):break# Apply the KNN background subtractor.fg_mask = bg_subtractor.apply(frame)# Let the background subtractor build up a history.if num_history_frames_populated < history_length:num_history_frames_populated += 1continue# Create the thresholded image._, thresh = cv2.threshold(fg_mask, 127, 255,cv2.THRESH_BINARY)cv2.erode(thresh, erode_kernel, thresh, iterations=2)cv2.dilate(thresh, dilate_kernel, thresh, iterations=2)# Detect contours in the thresholded image.if OPENCV_MAJOR_VERSION >= 4:# OpenCV 4 or a later version is being used.contours, hier = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)else:# OpenCV 3 or an earlier version is being used.# cv2.findContours has an extra return value.# The extra return value is the thresholded image, which# is unchanged, so we can ignore it._, contours, hier = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)# Draw green rectangles around large contours.# Also, if no pedestrians are being tracked yet, create some.should_initialize_pedestrians = len(pedestrians) == 0id = 0for c in contours:if cv2.contourArea(c) > 500:(x, y, w, h) = cv2.boundingRect(c)cv2.rectangle(frame, (x, y), (x+w, y+h),(0, 255, 0), 1)if should_initialize_pedestrians:pedestrians.append(Pedestrian(id, hsv_frame,(x, y, w, h)))id += 1# Update the tracking of each pedestrian.for pedestrian in pedestrians:pedestrian.update(frame, hsv_frame)cv2.imshow('Pedestrians Tracked', frame)k = cv2.waitKey(110)if k == 27: # Escapebreakif __name__ == "__main__":main()
输出结果: