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 cv2

OPENCV_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:  # Escape
        break

    success, 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.03

last_measurement = None
last_prediction = None

def on_mouse_moved(event, x, y, flags, param):
    global img, kalman, last_measurement, last_prediction

    measurement = 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 = measurement
    else:
        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 = measurement

cv2.namedWindow('kalman_tracker')
cv2.setMouseCallback('kalman_tracker', on_mouse_moved)

while True:
    cv2.imshow('kalman_tracker', img)
    k = cv2.waitKey(1)
    if k == 27:  # Escape
        cv2.imwrite('kalman.png', img)
        break

输出结果:

在这里插入图片描述

运行程序并移动鼠标。如果鼠标在高速下突然转弯,你会注意到预测线(红色)会比测量线(绿色)更宽。这是因为预测跟随鼠标运动到那时的动量。

3.跟踪行人

素材:

在这里插入图片描述

应用程序将遵循以下逻辑:

(1)从视频文件中捕获帧。
(2)使用前20帧来填充背景差分器的历史记录。
(3)基于背景差分,使用第21帧识别运动的前景物体。我们将把这些当作行人对待。对于每个行人,分配一个ID和一个初始跟踪窗口,然后计算直方图。
(4)对于随后的每一帧,使用卡尔曼滤波器和MeanShift跟踪每个行人。如果这是一个实际的应用程序,可能需要存储每个行人在场景中的路径记录,以便用户稍后对其进行分析。然而,这种类型的记录保存不在本示例中探讨。此外,在实际的应用程序中,需要确保能够识别进入场景的新的行人,但是目前我们将只专注于跟踪场景中靠近视频开始处的那些物体。

代码实例:

import cv2
import numpy as np

OPENCV_MAJOR_VERSION = int(cv2.__version__.split('.')[0])

class Pedestrian():
    """A tracked pedestrian with a state including an ID, tracking
    window, histogram, and Kalman filter.
    """

    def __init__(self, id, hsv_frame, track_window):

        self.id = id

        self.track_window = track_window
        self.term_crit = \
            (cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS, 10, 1)

        # Initialize the histogram.
        x, y, w, h = track_window
        roi = 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.03
        cx = x+w/2
        cy = y+h/2
        self.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_window
        center = np.array([x+w/2, y+h/2], np.float32)

        prediction = self.kalman.predict()
        estimate = self.kalman.correct(center)
        center_offset = estimate[:,0][:2] - center
        self.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 = 20
    bg_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 = 0
    while 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 += 1
            continue

        # 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) == 0
        id = 0
        for 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:  # Escape
            break

if __name__ == "__main__":
    main()

输出结果:
在这里插入图片描述

posted @ 2023-04-22 18:05  小幽余生不加糖  阅读(232)  评论(0编辑  收藏  举报  来源