基于计算机视觉和OpenCV:创建一个能够计算道路交通流量的应用

本文将介绍如何在不需要大量的深度学习算法的情况下,基于计算机视觉来计算道路交通流量。本教程只使用Python和OpenCV,在背景差分算法的帮助下,实现非常简单的运动检测方法。

本项目所需要的代码:

https://github.com/creotiv/object_detection_projects/tree/master/opencv_traffic_counting

主要内容

1.了解用于前景检测的背景差分算法的主旨 2.OpenCV图像滤波器 3.物体轮廓的检测 4.为进一步的数据操作构建处理管道

项目最终效果视频:https://youtu.be/_o5iLbRHKao

背景差分算法

背景差分有很多不同的算法,但它们的主旨非常简单。假设你有一个你房间的视频,在这个视频里没有出现人或者宠物,所以基本上这个房间是静态的,我们把它叫做背景层。为了获得在视频中移动的物体我们需要做的是:

foreground_objects = current_frame – background_layer(前景物体=当前帧 – 背景层)

但是在某些情况下,因为光线总是在改变,还有一些被人为移动或者本身可以运动的物体,我们不能得到静态帧。在这种情况下我们保存一些的帧数,并试图找出在大多数像素中,它们哪些是相同的,那么这些像素就会成为背景层的一部分。问题是,我们如何得到这个背景层和额外的滤波,从而使选择更加准确。

所以,我们将使用MOG算法来进行背景差分,在处理之后,它看起来是这样的:

原图(上),使用MOG差分前景(带有阴影检测)(下)

可以看到在前景掩模上有一些噪声,我们会用一些标准的滤波技术去除。现在代码看上去是这样的:

代码地址:

https://gist.githubusercontent.com/creotiv/f01ec1a4b7b1d88cad43c36be8fccc96/raw/f7b6ef1d4d13049e0ba28c10112e98b43f39baf0/bg_subtract.py

滤波

对于我们的情况,我们需要这些滤波器:“Threshold”, “Erode, Dilate, Opening, Closing”。请通过阅读下面链接内容查看这些滤波器是如何工作的。

http://docs.opencv.org/3.1.0/d7/d4d/tutorial_py_thresholding.html

http://docs.opencv.org/3.1.0/d9/d61/tutorial_py_morphological_ops.html

现在我们要用它们来去除前景蒙版上的一些噪声。首先,我们将使用“Closing”来移除区域的间隙,然后用“Opening”移除1–2 px点,然后用“Dilate”使物体变得bolder。

def filter_mask(img):

    kernel= cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2,2))

    # Fill any small holes
    closing= cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
    # Remove noise
    opening= cv2.morphologyEx(closing, cv2.MORPH_OPEN, kernel)

    # Dilate to merge adjacent blobs
    dilation= cv2.dilate(opening, kernel, iterations=2)

    # threshold
    th= dilation[dilation <240]= 0

    return th

然后我们的前景看上去是这样的

物体轮廓的检测

为此,我们将使用标准的带有参数的cv2.findContours方法:

cv2.CV_RETR_EXTERNAL — get only outer contours.
cv2.CV_CHAIN_APPROX_TC89_L1 - use Teh-Chin chain approximation algorithm (faster)
def
 
get_centroid(x, y, w, h):
    x1= int(w/ 2)
    y1= int(h/ 2)

    cx= x+ x1
    cy= y+ y1

    return (cx, cy)

def detect_vehicles(fg_mask, min_contour_width=35, min_contour_height=35):

    matches= []

    # finding external contours
    im, contours, hierarchy= cv2.findContours(
        fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_L1)

    # filtering by with, height
    for (i, contour)in enumerate(contours):
        (x, y, w, h)= cv2.boundingRect(contour)
        contour_valid= (w >= min_contour_width)and (
            h >= min_contour_height)

        if not contour_valid:
            continue

        # getting center of the bounding box
        centroid= get_centroid(x, y, w, h)

        matches.append(((x, y, w, h), centroid))

    return matches

构建处理管道

现在我们将构建简单的处理管道:

代码地址:

https://gist.githubusercontent.com/creotiv/6db4c523ae7c64554c3d08ff5edb8e79/raw/3b3a9f7253412dab686fe935808f66eb101057ed/pipeline.py

输入构造函数(input constructor)将会获取一个将按顺序运行的处理器列表。每个处理器都有各自的工作。因此,现在让我们来创建轮廓检测处理器。

代码地址:

https://gist.githubusercontent.com/creotiv/75a84e9a3f634c0f5c399bc495137075/raw/535b048a088e734e7bfcc1d14cb9e5e9cd434ebd/detection.py

把背景差分,滤波和检测的部分合并在一起。现在,让我们创建一个处理器,它将在不同的帧上链接检测到的对象,然后创建路径,并且还将计算出到达出口区的车辆数量。

代码地址:

https://gist.githubusercontent.com/creotiv/da7fca1b237619a756133a1fa816350e/raw/243204ca5d277b20c9addbc3559a69bcdb9d132b/counting.py

这个类有点复杂,让我们通过分部来完成。

图像上的绿色掩膜是出口区,是我们计算车辆的地方。使用掩膜是因为它比使用向量算法更有效,也更简单。只要使用“二进制”操作来检查该区域的那个点就可以了。下面是我们如何设置它的方法:

EXIT_PTS= np.array([
    [[732,720], [732,590], [1280,500], [1280,720]],
    [[0,400], [645,400], [645,0], [0,0]]
])

base= np.zeros(SHAPE+ (3,), dtype='uint8')
exit_mask= cv2.fillPoly(base, EXIT_PTS, (255,255,255))[:, :,0]

在路径上将点连接起来。

new_pathes= []

for pathin self.pathes:
    _min= 999999
    _match= None
    for pin points:
        if len(path)== 1:
            # distance from last point to current
            d= utils.distance(p[0], path[-1][0])
        else:
            # based on 2 prev points predict next point and calculate
            # distance from predicted next point to current
            xn= 2 * path[-1][0][0]- path[-2][0][0]
            yn= 2 * path[-1][0][1]- path[-2][0][1]
            d= utils.distance(
                p[0], (xn, yn),
                x_weight=self.x_weight,
                y_weight=self.y_weight
            )

        if d < _min:
            _min= d
            _match= p

    if _matchand _min <= self.max_dst:
        points.remove(_match)
        path.append(_match)
        new_pathes.append(path)

    # do not drop path if current frame has no matches
    if _matchis None:
        new_pathes.append(path)

self.pathes= new_pathes

# add new pathes
if len(points):
    for pin points:
        # do not add points that already should be counted
        if self.check_exit(p[1]):
            continue
        self.pathes.append([p])

# save only last N points in path
for i, _in enumerate(self.pathes):
    self.pathes[i]= self.pathes[i][self.path_size* -1:]

在第一帧,我们只是把所有的点都添加到新的路径中。

接下来,如果len(path)==1,那么对于缓存中的每条路径,我们将尝试从新检测到的对象中找到点(质心),这将是到路径的最后一点的最小的欧氏距离。

如果len(path) > 1,那么在路径的最后两个点上,我们就会在同一直线上预测新的点,并在它和当前点之间找到最小距离。最小距离添加到当前路径的末尾,并从列表中删除。如果在这之后留下一些点,我们将会把它们作为新的路径添加。同时我们也限制了路径上的点的个数。

# count vehicles and drop counted pathes:
 new_pathes= []
 for i, pathin enumerate(self.pathes):
     d= path[-2:]


    if (
         # need at list two points to count
         len(d) >= 2 and
         # prev point not in exit zone
         not self.check_exit(d[0][1])and
         # current point in exit zone
         self.check_exit(d[1][1])and
         # path len is bigger then min
         self.path_size <= len(path)
     ):
         self.vehicle_count+= 1
    else:
         # prevent linking with path that already in exit zone
         add= True
         for pin path:
             if self.check_exit(p[1]):
                add= False
                 break
        if add:
            new_pathes.append(path)


 self.pathes= new_pathes


 context['pathes']= self.pathes
 context['objects']= objects
 context['vehicle_count']= self.vehicle_count


 self.log.debug('#VEHICLES FOUND: %s' % self.vehicle_count)


 return context

现在我们将尝试计算进入出口区的车辆。要做到这一点,我们只需要在路径中取两个最后的点,并检查它们在出口区域中的最后一个点,以及之前没有的点,并且检查len(path)是否应该大于限制。后面的部分是防止将新点与出口区的点反向连接起来。

最后两个处理器是CSV写入器,用于创建报告CSV文件,以及用于调试和图片的可视化。

代码地址:

https://gist.github.com/creotiv/2998928abe5ab8606c07450965393261/raw/7b7633492d4ae0e8499e706029ceb452fe44ba60/output.py

CSV写入器是按时间保存数据的,因为我们需要将它进一步分析。因此,我使用这个公式向unixtimestamp添加额外的帧计时:

time = ((self.start_time + int(frame_number / self.fps)) * 100 
        + int(100.0 / self.fps) * (frame_number % self.fps))

所以在起始时间=1 000 000和fps=10时,会得到这样的结果 帧1=1 000000010 帧1=1 000000020 …

在得到完整的csv报告之后,你可以按照你的需要聚合这些数据。

这个项目的完整代码:https://github.com/creotiv/object_detection_projects/tree/master/opencv_traffic_counting

原文发布于微信公众号 - ATYUN订阅号(atyun_com)

原文发表时间:2017-09-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数据科学与人工智能

【Python环境】Python数据挖掘兵器谱

Python正渐渐成为很多人工作中的第一辅助脚本语言,在文本处理,科学计算,机器学习和数据挖掘领域,有很多很多优秀的Python工具包可供使用,所以作为Pyth...

1946
来自专栏智能算法

Python相关机器学习‘武器库’

开始学习Python,之后渐渐成为我学习工作中的第一辅助脚本语言,虽然开发语言是Java,但平时的很多文本数据处理任务都交给了Python。这些年来,接触和使...

4076
来自专栏数据小魔方

think-cell chart系列16——树状分布图

今天跟大家分享think-cell chart系列的第16篇——树状分布图。 大家不要困惑于该图表的名称——树状分布图,其实它用的技巧非常简单(就是基本图表的组...

3794
来自专栏有趣的Python

TensorFlow应用实战-17-Qlearning实现迷宫小游戏什么是Q-learning

4087
来自专栏数据小魔方

sparklines迷你图系列12——Composition(Cascade)

今天继续跟大家分享的sparklines迷你图系列12——Composition(Cascade)。 不知道为啥这个图的名字英文看起来这么怪,但是其实他就是我们...

2694
来自专栏数据小魔方

竟然是一个升级版的数据透视表,Tableau真的没有那么神秘~

前一篇跟大家详述了关于Excel在作图理念上的诸多细节,今天让我们把聚焦于多分类维度的数据呈现问题。 当然今天这篇我会把视角从Excel切换到Tableau,没...

7067
来自专栏Crossin的编程教室

【每周一坑】统计英文小说词频

Thank God It’s Friday! 又到周五啦!眼看就要忙完一周的学习和工作,又可以出去浪咯。 然而,只有我们依旧无趣地在此刻发干货文,提醒着你有没有...

2878
来自专栏编程一生

看Lucene源码必须知道的基本规则和算法

1043
来自专栏Data Analysis & Viz

Python网页爬虫&文本处理&科学计算&机器学习&数据挖掘兵器谱

周末时看到这篇不错的文章,其中介绍了诸多python第三方库和工具,与大家分享下,也算是门可罗雀的本号第一次转载文章。后续看到精彩的文章也会继续分享。

752
来自专栏数据小魔方

交叉柱形图

今天继续跟大家分享一组交叉柱形图的制作技巧! 该图表是从一本图表书中看到的,可以在同一幅图表中以两种视角展示同一组年度数据指标,步骤相对比较复杂,甚至有点炫技之...

3517

扫码关注云+社区