65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 TrafficCV 进行车辆检测

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2021 年 1 月 19 日

CPOL

8分钟阅读

viewsIcon

14300

在本文中,我们将深入探讨 TrafficCV 的实现细节以及用于检测车辆和计算其速度的各种对象检测模型。

引言

交通速度检测是一个大生意。世界各地的市政当局利用它来阻止超速行驶并通过超速罚单创收。但传统的测速仪,通常基于 RADAR 或 LIDAR,价格昂贵。

本系列文章将向您展示如何仅使用深度学习构建一个相当准确的交通速度检测器,并在像树莓派这样的边缘设备上运行它。

欢迎您从 TrafficCV Git 仓库 下载本系列文章的代码。我们假设您熟悉 Python,并具备 AI 和神经网络的基础知识。

上一篇文章中,我们在 Windows 10 上为我们将要在树莓派上部署和测试代码的 TrafficCV 程序设置了开发环境。在本文中,我们将探讨 TrafficCV 的整体设计、该程序要解决的问题,以及如何使用对象检测模型和对象关联跟踪器来实现车辆检测器。

TrafficCV:它是什么以及它做什么

TrafficCV——或任何 OpenCV 程序——的基本功能是逐帧分析视频,使用各种算法或神经网络模型来对对象进行分类,提取特征(如对象边界框),计算点(如对象质心),识别对象,并在视频帧中跟踪它们。通常会运行一个计时器来测量对象质心从一组像素坐标移动到另一组像素坐标所花费的时间。然后,该程序可以使用此信息,以及通过将摄像头像素距离与实际距离校准获得的常数,来估算对象的速度。

TrafficCV 需要实时解决的基本问题是:

  1. 处理视频帧,并对将在视频帧上运行的计算机视觉算法或模型执行必要的缩放、色彩空间或其他转换。
  2. 检测车辆、行人等对象,并在达到一定的置信阈值时,获取所有被识别为车辆的对象的边界框。
  3. 跟踪视频中的车辆——将当前帧中识别的对象与前一帧中识别的同一个对象进行关联。
  4. 测量车辆在视频中经过的像素距离以及花费的时间。
  5. 估算车辆的速度,使用从摄像头属性和像素/实际距离校准中获得的各种常数。
  6. 显示计算出的信息,使其易于人类理解。

第一次通过检测器

让我们看看 TrafficCV 中 (haarcascade_kraten) 模型和检测器的第一次运行。这是最容易理解的车辆速度检测器实现,它将作为使用其他类型检测器的模板。第一次通过检测器使用一个 OpenCV Haar 级联分类器 模型,该模型由 Kartike Bansal 发布,用于车辆检测,并结合了 dlib 库的对象关联跟踪器

该分类器在 CPU 上运行,每 fc 帧检测一次车辆对象的边界框,其中 fc 是一个命令行检测器参数,默认值为 10。因此,我们每 10 帧检查一次是否出现了新的车辆对象。其余帧则传递给对象关联跟踪器,该跟踪器计算每个现有车辆边界框的新位置。然后,速度估计器运行,利用前一帧和当前帧车辆边界框位置之间的距离,以及每米像素(ppm)和每秒帧数(fps)常数。这些约束必须通过物理测量和校准以及摄像头属性来获得。

然后,检测器使用 OpenCV 的绘图和文本函数以绿色绘制边界框,并以白色显示估算速度。估算的 FPS 以红色显示在顶部。

TrafficCV:在树莓派 4 上使用 Haar 级联分类器

点击上面的链接可观看在 Pi 4 上运行的第一次尝试的 YouTube 视频。要运行此模型,请下载 TrafficCV 模型文件演示视频。在您的 TrafficCV 文件夹中解压这些压缩文件,以便您拥有 modelsdemo_videos 子目录。然后在 Windows 上,您可以说:

tcv --model haarcascade_kraten --video demo_videos\cars_vertical.mp4

在 Pi 4 上,您可以说:

./tcv MYHOST:0.0 --model haarcascade_kraten \
--video demo_videos/cars_vertical.mp4

其中 MYHOST:0.0 是一个可用 X 服务器的名称。如果您登录到 Pi 的本地桌面,您可以说:

./tcv $DISPLAY --model haarcascade_kraten \
--video demo_videos/cars_vertical.mp4

让我们看看 haarcascade_kraten.py 文件中的代码,了解上面我们上面提到的 1-6 个任务是如何用 Python 实现的。首先,我们使用预训练模型文件和输入视频的视频源来初始化一个 CascadeClassifier 实例,然后读取用户指定的参数。

classifier = cv2.CascadeClassifier(model_file)
    video = cv2.VideoCapture(video_source)
    ppm = 8.8
    if 'ppm' in args:
        ppm = args['ppm']

我们还初始化了在处理视频时需要用到的变量和常数。

         VIDEO_WIDTH = 1280
    VIDEO_HEIGHT = 720
    RECT_COLOR = (0, 255, 0)
    frame_counter = 0
    fps = 0
    current_car_id = 0
    car_tracker = {}
    car_location_1 = {} # Previous car location
    car_location_2 = {} # Current car location
    speed = [None] * 1000

VIDEO_WIDTHVIDEO_HEIGHT 是分类器期望的视频帧的宽度和高度。我们需要将每个视频帧调整为分类器模型期望的尺寸。我们还需要变量来跟踪检测到的每个车辆对象的位置和速度,以及一个变量来记录每秒处理的视频帧数。在设置了所需的参数和变量后,所有操作都在一个 while 循环中进行,该循环从视频中读取一帧,复制一份,然后递增帧计数器。

    while True:
        start_time = time.time()
        _, image = video.read()
        if image is None:
            break
        image = cv2.resize(image, (VIDEO_WIDTH, VIDEO_HEIGHT))
        result = image.copy()
        frame_counter += 1 
        ...

下一步是使用当前帧更新我们的 dlib 相关跟踪器

for car_id in car_tracker.keys():
        tracking_quality = car_tracker[car_id].update(image)
        if tracking_quality < 7:
            car_ids_to_delete.append(car_id)
        for car_id in car_ids_to_delete:
            debug(f'Removing car id {car_id} + from list of tracked cars.')
            car_tracker.pop(car_id, None)
            car_location_1.pop(car_id, None)
            car_location_2.pop(car_id, None)

tracking_quality 变量实际上是 dlib 对象跟踪器在更新被跟踪对象位置时计算出的一个值,称为峰值旁瓣比。如果这个值小于 7,则对象可能消失或被另一个对象遮挡,因此我们将其从跟踪器中移除。

接下来,我们检查 frame_counter 变量是否是 fc 参数的倍数。如果是,则在当前帧上运行分类器。

if not (frame_counter % fc):
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    cars = classifier.detectMultiScale(gray, 1.1, 13, 18, (24,   24))

我们使用 OpenCV 函数将输入视频帧的颜色空间转换为 BGR 灰度,这是 Haar 分类器中最常用的颜色空间,然后调用 detectMultiScale 函数。该函数的参数指定了分类器将检测到的对象的最小边界框大小等参数。在我们的例子中,这被设置为 24 * 24 像素。对于每个检测到的车辆对象,我们计算边界框的质心。

for (_x, _y, _w, _h) in cars:
                x = int(_x)
                y = int(_y)
                w = int(_w)
                h = int(_h)
                x_bar = x + 0.5 * w
                y_bar = y + 0.5 * h

然后,我们遍历在上次分类器运行时检测到并且现在正在跟踪的所有先前车辆对象,并计算这些对象的质心。

 matched_car_id = None
            for car_id in car_tracker.keys():
                 tracked_position = car_tracker[car_id].get_position()
                 t_x = int(tracked_position.left())
                 t_y = int(tracked_position.top())
                 t_w = int(tracked_position.width())
                 t_h = int(tracked_position.height())   
                 t_x_bar = t_x + 0.5 * t_w
                 t_y_bar = t_y + 0.5 * t_h
                
                 if ((t_x <= x_bar <= (t_x + t_w)) and 
                     (t_y <= y_bar <= (t_y + t_h)) and 
                     (x <= t_x_bar <= (x + w)) and 
                     (y <= t_y_bar <= (y + h))):
                          matched_car_id = car_id

我们将每个检测到的对象的边界框和质心与我们跟踪器预测了位置的现有对象进行比较。如果跟踪对象的位置与检测到的对象的位置重叠,我们假设这个检测到的对象与现有的跟踪对象是同一个(请记住,我们已经移除了被其他对象遮挡的现有对象)。如果上述情况没有对象重叠,我们假设我们检测到了一个新对象。然后,我们为这个新对象分配一个 ID 并创建一个跟踪器。

    if matched_car_id is None:
      debug (f'Creating new car tracker with id {current_car_id}.' )
      tracker = dlib.correlation_tracker()
      tracker.start_track(image, dlib.rectangle(x, y, x + w, y + h))
      car_tracker[current_car_id] = tracker
      car_location_1[current_car_id] = [x, y, w, h]
      current_car_id += 1

因此,我们获得了当前帧中新车辆对象(使用 Haar 级联分类器)和现有对象(在我们的对象关联跟踪器中)的位置。我们可以使用 OpenCV 的 rectangle 函数在帧上为这些对象绘制绿色边界框。

    for car_id in car_tracker.keys():
        tracked_position = car_tracker[car_id].get_position()
        t_x = int(tracked_position.left())
        t_y = int(tracked_position.top())
        t_w = int(tracked_position.width())
        t_h = int(tracked_position.height())
        cv2.rectangle(result, (t_x, t_y), (t_x + t_w, t_y + t_h),   RECT_COLOR, 4)

我们还使用为每个对象计算的位置更新 car_location_2 变量。

car_location_2[car_id] = [t_x, t_y, t_w, t_h]

car_location_1 变量包含前一帧计算出的车辆对象的位置。因此,我们现在可以使用每个对象的这两个位置来计算速度。

for i in car_location_1.keys():	
    if frame_counter % 1 == 0:
        [x1, y1, w1, h1] = car_location_1[i]
        [x2, y2, w2, h2] = car_location_2[i]
        car_location_1[i] = [x2, y2, w2, h2]
        if [x1, y1, w1, h1] != [x2, y2, w2, h2]:
               # Estimate speed for a car object as it passes through a ROI.
               if (speed[i] is None) and y1 >= 275 and y1 <= 285:
               speed[i] = estimate_speed(ppm, fps, [x1, y1, w1, h1], [x2, y2, w2, h2])
                if speed[i] is not None and y1 >= 180:
                    cv2.putText(result, str(int(speed[i])) + "  
km/hr", (int(x1 + w1/2), int(y1-5)),cv2.FONT_HERSHEY_SIMPLEX, 
0.75, (255, 255, 255), 2)

对于新对象,我们仅在其通过特定区域时估算其速度。速度计算执行一次,我们将值存储在 speeds 变量中。我们仅在对象移动速度足够快时显示速度。

最后一段代码在一个窗口中显示带有所有矩形和文本注释的帧,并等待用户按下任意键以退出循环并结束程序。

        cv2.imshow('TrafficCV Haar cascade classifier speed detector.  Press q to quit.', result)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

最终结果是出现一个窗口,回放视频帧,并显示检测到的车辆对象的边界框和估算速度。

CPU 使用率考虑

当我们在 Windows 上使用演示视频第一次运行时,我们可以从视频窗口输出看到基本检测器正在工作,FPS 计数器在 18 左右。但是,CPU 使用率很高,并且随着跟踪的车辆增多而升高。在配备移动 ARM 处理器的 Pi 上,我们每秒只能处理几帧。即使如此,如果我们通过 Linux 的 htop 程序跟踪 CPU 使用率,也会发现它很高。

当通过 RDP 运行此 TrafficCV 的初始版本时,我们看到第一个 CPU 核心专用于 xrdp 服务器,而另外三个核心运行视频处理任务。然而,当我们使用在 Windows 上运行的远程 X 服务器时,CPU 使用率较低。它是单线程的,这表明有阻塞 I/O 操作在我们视频处理线程上花费了大量时间。通过网络发送 X 帧所需的时间远大于在 OpenCV 中处理视频帧所需的时间。

如果我们增加 fc 参数(增加分类器运行间隔),我们可以提高 FPS,因为关联跟踪器比分类器占用的 CPU 更少。但是,有了这个基本可行的骨架程序,我们现在可以探索更高级的 TensorFlow 速度检测模型了。

下一步

下一篇文章中,我们将专注于开发一个计算机视觉框架,该框架可以在实时和录制的车辆交通视频上运行各种机器学习和神经网络模型——例如 SSD MobileNet。敬请关注!

© . All rights reserved.