【OpenCV学习】(九)目标识别之车辆检测与计数

【OpenCV学习】(九)目标识别之车辆检测及计数

背景

本篇将具体介绍一个实际应用项目——车辆检测及计数,在交通安全中是很重要的一项计数;当然,本次完全采用OpenCV进行实现,和目前落地的采用深度学习的算法并不相同,但原理是一致的;本篇将从基础开始介绍,一步步完成车辆检测计数的项目;

一、图像轮廓

本质:具有相同颜色强度连续点的曲线;

作用:

1、可用于图形分析;

2、应用于物体的识别与检测;

注意点:

1、为了检测的准确性,需要先对图像进行二值化或Canny操作;

2、画轮廓的时候回修改输入的图像,需要先深拷贝原图;

轮廓查找的函数原型:

findContours(img,mode,ApproximationMode…)

  • mode

    RETR_EXTERNAL=0,表示只检测外轮廓;

    RETR_LIST=1,检测的轮廓不建立等级关系;(常用)

    RETR_CCOMP=2,每层最多两级;

    RETR_TREE=3,按树形结构存储轮廓,从右到左,从大到小;(常用)

  • ApproximationMode

    CHAIN_APPROX_BOBE:保存轮廓上所有的点;

    CHAIN_APPROX_SIMPLE:只保存轮廓的角点;

  • 代码实战:

    img = cv2.imread('./contours1.jpeg')
    # 转变成单通道
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化
    ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    

    contours输出结果:

    (array([[[  0,   0]],
    
           [[  0, 435]],
    
           [[345, 435]],
    
           [[345,   0]]], dtype=int32),)
    

    可以看出,我们找最外层轮廓,找出了一个矩形轮廓的四个点;

    当然,我们不需要通过画形状来绘制轮廓,可以通过一个内置函数来绘制轮廓;

    绘制轮廓函数原型

    drawContours(img,contours,contoursIdx,color,thickness,…)

  • contours:表示保存轮廓的数组;
  • contoursIdx:表示绘制第几个轮廓,-1表示所有轮廓;
  • 代码案例:

    img = cv2.imread('./contours1.jpeg')
    img2 = img.copy()
    # 转变成单通道
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化
    ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
    cv2.drawContours(img, contours, -1, (0, 0, 255), 1)
    cv2.drawContours(img2, contours, -1, (0, 0, 255), -1)
    
    cv2.imshow('org', img)
    cv2.imshow('org2', img2)
    cv2.waitKey(0)
    

    如上图所示,左图是线宽设置为1,右图为线宽设置为-1,也就是填充的效果;

    当然,OpenCV还提供了计算轮廓周长和面积的方法;

    轮廓面积函数原型:

    contourArea(contour)

    轮廓周长函数原型:

    arcLength(curve,closed)

  • curve:表示轮廓;
  • closed:是否是闭合的轮廓;
  • 上述两个函数比较简单,在这就不做代码演示了;

    二、多边形逼近与凸包

    多边形逼近函数原型:

    approxPolyDP(curve,epsilon,closed)

  • epslion:精度;
  • 凸包的函数原型:

    convexHull(points,clockwise,…)

  • points:轮廓;
  • clockwise:绘制方向,顺时针或逆时针;(不重要)
  • 首先我们看一下基于轮廓查找输出的轮廓形状:

    可以看出轮廓点十分密集,接下来看一下基于多变形逼近和凸包的效果:

    代码案例:

    img = cv2.imread('./hand.png')
    img2 = img.copy()
    # 转变成单通道
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化
    ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # cv2.drawContours(img, contours, -1, (0, 0, 255), 1)
    e = 20
    approx = cv2.approxPolyDP(contours[0], e, True)    # 多边形逼近
    approx = (approx, )
    cv2.drawContours(img, approx, 0, (0, 0, 255), 3)
    
    hull = cv2.convexHull(contours[0])
    hull = (hull, )
    cv2.drawContours(img2, hull, 0, (0, 0, 255), 3)    # 凸包
    
    cv2.imshow('org', img)
    cv2.imshow('org2', img2)
    cv2.waitKey(0)
    

    这里需要注意一点,绘制轮廓的函数对于轮廓的传入需要为元组,需要将得到的数组放到一个元组中!

    当然,多边形逼近这里设置的精度为20,所以比较粗糙,设置小一些可以达到更好的效果;

    三、外接矩形

    外接矩阵分为最大外接矩阵和最小外接矩阵,如下图所示:

    最小外接矩阵还有一个功能,就是计算旋转角度,从上图的绿框应该可以很明显看出;

    最小外接矩阵函数原型:

    minAreaRect(points)

    返回值:起始点(x,y)、宽高(w,h)、角度(angle)

    最大外接矩形函数原型:

    boundingRect(array)

    返回值:起始点(x,y)、宽高(w,h)

    代码案例:

    img = cv2.imread('./hello.jpeg')
    img2 = img.copy()
    # 转变成单通道
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化
    ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
    # 获取最小外接矩形
    r = cv2.minAreaRect(contours[1])
    box = cv2.boxPoints(r)                     # 提取其中的点
    box = np.int0(box)                         # 将浮点型转换为整型
    cv2.drawContours(img, (box, ), 0, (0, 0, 255), 2)
    
    # 获取最大外接矩形
    x, y, w, h = cv2.boundingRect(contours[1])
    cv2.rectangle(img2, (x, y), (x+w, y+h), (0, 0, 255), 2)
    
    cv2.imshow('org', img)
    cv2.imshow('org2', img2)
    cv2.waitKey(0)
    

    四、车辆统计实战

    涉及的知识点:

  • 窗口展示
  • 图像、视频的加载
  • 基本图形的绘制
  • 基本图像运算与处理
  • 形态学
  • 轮廓查找
  • 实现流程:

    加载视频 —— 通过形态学识别车辆 —— 对车辆进行统计 —— 显示统计信息

    1、加载视频

    这里就是一个简单加载视频的实现:

    cap = cv2.VideoCapture('video.mp4')
    while True:
        ret, frame = cap.read()
        
        if(ret == True):
            cv2.imshow('video', frame)
        
        key = cv2.waitKey(1)
        if(key == 27):                  # Esc退出
            break
    
    cap.release()
    cv2.destroyAllWindows()
    

    2、去除背景

    函数原型:

    createBackgroundSubtractorMOG()

  • history:缓冲,表示多少毫秒,可不指定参数,用默认的即可;
  • 具体实现原理比较复杂,用到了一些视频序列关联信息,把像素值不变的认为是背景;

    注意:在opencv中已经不支持该函数,而是用createBackgroundSubtractorMOG2()替代;如果需要使用可以安装opencv_contrib模块,在其中的bgsegm中保留了该函数;

    代码实现:

    cap = cv2.VideoCapture('video.mp4')
    
    bgsubmog = cv2.bgsegm.createBackgroundSubtractorMOG()
    
    while True:
        ret, frame = cap.read()
        
        if(ret == True):
            # 灰度处理
            cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            # 高斯去噪
            blur = cv2.GaussianBlur(frame, (3, 3), 5)
            mask = bgsubmog.apply(blur)
            cv2.imshow('video', mask)
            
        key = cv2.waitKey(1)
        if(key == 27):                  # Esc退出
            break
    
    cap.release()
    cv2.destroyAllWindows()
    

    这里尽量采用旧版的MOG函数,新版的MOG2函数比较精细,会将树叶等信息输出,去除效果没那么好;

    3、形态处理

    这里主要是为了处理一些小的噪声点以及目标中的黑色块;

    代码实现:

    cap = cv2.VideoCapture('video.mp4')
    
    bgsubmog = cv2.bgsegm.createBackgroundSubtractorMOG()
    
    # 形态学kernel
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    
    while True:
        ret, frame = cap.read()
        
        if(ret == True):
            # 灰度处理
            cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            # 高斯去噪
            blur = cv2.GaussianBlur(frame, (3, 3), 5)
            mask = bgsubmog.apply(blur)
            # 腐蚀
            erode = cv2.erode(mask, kernel)
            # 膨胀
            dilate = cv2.dilate(erode, kernel, 3)
            # 闭操作
            close = cv2.morphologyEx(dilate, cv2.MORPH_CLOSE, kernel)
            close = cv2.morphologyEx(close, cv2.MORPH_CLOSE, kernel)
            
            contours, h = cv2.findContours(close, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE,)
            
            for (i, c) in enumerate(contours):
                (x, y, w, h) = cv2.boundingRect(c)
                cv2.rectangle(frame, (x, y), (x+w, y+h), (0,0,255), 2)
                
            cv2.imshow('video', frame)
            
        
        key = cv2.waitKey(1)
        if(key == 27):                  # Esc退出
            break
    
    cap.release()
    cv2.destroyAllWindows()
    

    从图中效果来看,还是会有很多小的检测框,接下来就是处理重合检测框以及去掉一些多余的检测框,类似于NMS去重,当然原理还不太一样;

    4、车辆统计

    首先需要过滤一些小的矩形,已经检测框的长和宽,设定一些阈值即可;

    代码实现:

    cap = cv2.VideoCapture('video.mp4')
    
    bgsubmog = cv2.bgsegm.createBackgroundSubtractorMOG()
    
    # 保存车辆中心点信息
    cars = []
    # 统计车的数量
    car_n = 0
    
    # 形态学kernel
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    
    while True:
        ret, frame = cap.read()
        
        if(ret == True):
            # 灰度处理
            cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            # 高斯去噪
            blur = cv2.GaussianBlur(frame, (3, 3), 5)
            mask = bgsubmog.apply(blur)
            # 腐蚀
            erode = cv2.erode(mask, kernel)
            # 膨胀
            dilate = cv2.dilate(erode, kernel, 3)
            # 闭操作
            close = cv2.morphologyEx(dilate, cv2.MORPH_CLOSE, kernel)
            close = cv2.morphologyEx(close, cv2.MORPH_CLOSE, kernel)
            
            contours, h = cv2.findContours(close, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE,)
            
            # 画一条线
            cv2.line(frame, (10, 550), (1200, 550), (0, 255, 255), 3)
            for (i, c) in enumerate(contours):
                (x, y, w, h) = cv2.boundingRect(c)
                
                # 过滤小的检测框
                isshow = (w >= 90) and (h >= 90)
                if(not isshow):
                    continue
                    
                # 保存中心点信息
                cv2.rectangle(frame, (x, y), (x+w, y+h), (0,0,255), 2)
                centre_p = (x + int(w/2), y + int(h/2))
                cars.append(centre_p)
                cv2.circle(frame, (centre_p), 5, (0,0,255), -1)
                for (x, y) in cars:
                    if(593 < y < 607):
                        car_n += 1 
                        cars.remove((x, y))             
            cv2.putText(frame, "Cars Count:" + str(car_n), (500, 60), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 5)    
            cv2.imshow('video', frame)
            
        
        key = cv2.waitKey(1)
        if(key == 27):                  # Esc退出
            break
    
    cap.release()
    cv2.destroyAllWindows()
    

    简单的效果已经出来了,对于大部分车辆都能够很好的检测并且计数了;

    存在问题:

    由于是用中心点与线的距离来判断,车速过慢可能会在两帧内重复计数,车速过快可能会计数不到;这就是传统算法存在的一个问题,基于深度学习的方法可以很好解决这些问题,可关注目标跟踪实战的那一篇文章!

    总结

    项目到这里就介绍了,通过该项目主要是将所学的知识点进行串联,重点在于形态学的运用!当然这个效果可能达不到实际应用的标准,这也是传统算法的一个弊端;有能力的可以采用深度学习的方法进行实现,也可以关注我后续的目标跟踪是实现车辆计数,效果会远比这个好。

    来源:一个热爱学习的深度渣渣

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【OpenCV学习】(九)目标识别之车辆检测与计数

    发表评论