死磕YOLO系列,不会 AI没关系,用OpenCV 调用YOLO 做目标检测

2020-07-14 10:24:59 浏览数 (1)

如果你要完成下图的目标检测功能,你会怎么做?

对于视觉工程师而言,这当然是个小问题。可术业有专攻,不一定每个程序员都懂 AI 算法,那肯定就有一种方法把算法当成一个黑盒子,处理好输入输出就好了。

所以,这篇文章完全是零基础告诉你如何完成上面的目标。

首先,进行输入与输出的定义。

  1. 输入是一张图片
  2. 经过 AI 算法的处理
  3. AI 算法输出结果

能搞定目标检测的算法有很多,当前 OpenCV 都支持这些算法的调用,本文讲解 Yolov3,其它算法其实也是大同小异。

本文不分析 Yolo 算法的原理,对原理有兴趣的可以到文章末尾查看链接。

本文只讲如何利用 OpenCV 来调用 Yolo 进行目标检测。

YOLO 是一种目标检测的算法,就是算法接收一张图片,识别图片中物体的类别和位置。

OpenCV 是一个开源的机器视觉库,借助它我们可以很方便处理图片及一些机器视觉操作。

本文介绍如何利用 OpenCV 做目标检测。

安装 OpenCV

为了演示方便,本文是在 Ubuntu 16.04 系统上操作。

OpenCV 版本选择 Python 版直接安装。

代码语言:javascript复制
sudo pip3 install opencv-python

我选择了用 Python3 去安装 opencv-python,这样可以保证 opencv 的版本是 3.4 以上。

如果用 python2 去安装 Opencv-python ,那么版本很可能就是 2.4,那么就无法用 DNN 模块。

如果上面的 pip3 安装时速度过慢,可以尝试用国内镜像。

代码语言:javascript复制
sudo pip3 install opencv-python -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com

下载 YOLO 配置文件和权重文件

代码语言:javascript复制
wget https://pjreddie.com/media/files/yolov3.weight

当然,下载速度过慢的话,可以去网上找网盘的地址。

yolov3.weight 是 yolo 第 3 个版本的训练好的神经网络权重。

光有权重还不够,还需要有神经网络的配置文件 yolov3.cfg.

我本文的示例源码中将会直接提供。

利用 OpenCV DNN 模块调用 YOLOv3

OpenCV 中 DNN 是专门用来处理神经网络的模块,可以加载主流的 AI 框架产生的权重文件,如 Caffe、Pytorch。并且从 3.4.3 版本开始也支持 Darknet。

Darknet 是 YOLO 作者自己编写的神经网络框架。

OpenCV 支持 Darknet 说明它也支持 YOLO 做目标检测。

下面开始写 python 代码示例。

代码语言:javascript复制
def main():

    # 读取图片
    img = cv2.imread("yolotest.jpg")

    # 目标检测
    img = detect(img)

    # 绘制图片
    cv2.imshow("test",img)
    cv2.waitKey()
    cv2.imwrite("yoloresult.jpg",img)
    cv2.destroyAllWindows()
    
    pass

if __name__ == "__main__":
    main()

main() 是整个程序的入口,过程非常简单。

  1. 通过 OpenCV 的 API 读取图片
  2. 将图片进行目标检测
  3. 将进行了检测后的图片进行显示和保存

最终效果如下:

我们很容易知道,detect 就是最核心的函数,所以本文章重点分析它。

核心检测代码
代码语言:javascript复制
def detect(img):

    # 1. 图像缩放到规整的 608*608 分辨率
    img = cv2.resize(img,(608,608))
    w = img.shape[1]
    h = img.shape[0]

    # 2. 从图像处创建 blob 对象
    blob = dnn.blobFromImage(img,1/255.0)

    # 3. 将图像输入给神经网络
    net.setInput(blob)

    layername = getOutputsNames(net)
  
    # 4. 神经网络进行前向推断预测
    detections = net.forward(layername)

    # 5. 推断的结果进行后处理优化
    img = postprocess(img,detections)


    return img 

代码也非常简单,但需要做一些前提说明。

net 代表神经网络,在调用 detect 之前,其实我们应该做一些初始化的动作。

神经网络的加载
代码语言:javascript复制
import cv2
from cv2 import dnn

net = dnn.readNetFromDarknet("yolov3.cfg","yolov3.weights")

首先,我们需要从 cv2 模块中引入 dnn 模块。

因为 dnn 模块支持很多 AI 模型,不同的 AI 模型有不同的导入 API。

因为我们需要导入 Darknet 版本的 Yolo,所以调用的方法是 readNetFromDarknet 。

意思就是通过读取配置文件和权重文件构建神经网络。

yolov3.cfg 描述了神经网络的结构。 yolov3.weights 描述了神经网络训练后保存下来的权重参数。

还有一点需要注意的是,读取的图片是要经过处理才能输入到神经网络的。

并且,神经网络可以接受多张图片作为输入,也可以接受一张图片。

dnn.blobfromimage()接受一张图像,并对这些对象对一些预处理,包括减均值、归一化、缩放尺寸的操作。

一般地,深度学习神经网络会涉及到 2 个概念,训练和推理。

当前,我们调用的模型是别人已经训练好的,因此,我们不需要再对它进行训练,我们只用它来做推理。

所以,调用下面的方法就好了。

代码语言:javascript复制
 detections = net.forward(layername)
forward 方法细节

为什么 forward 方面要加 layername 这个参数呢?

因为神经网络推理时,最后一层就是结果,所以我们需要知道最后一层的名字,其它的网络模型一般如下代码这样就可以求出来了。

代码语言:javascript复制
 detections = net.forward("detection_out")

但偏偏 Yolov3 不行,它的网络是分支结构的,它有 3 个分支,所以要求 3 个分支的最后一层。

代码语言:javascript复制
def getOutputsNames(net):
    # 获取 net 中所有的层的名字
    layersNames = net.getLayerNames()

    print("layersNames:",layersNames)
    # 获取没有向后连接的层的名字,最后一层就是 unconnectedoutlayers
    return [layersNames[i[0] - 1] for i in net.getUnconnectedOutLayers()]


layername = getOutputsNames(net)
    print("layername-->",layername)
  
# 4. 神经网络进行前向推断预测
detections = net.forward(layername)

其实,代码运行时,我也有打印最后输出层的名字。结果如下:

代码语言:javascript复制
layername--> ['yolo_82', 'yolo_94', 'yolo_106']

我这里通过了一个开源工具netron 查看了 Yolov3.cfg 的结构。

层数还挺多的,放大后,仔细看它的末端。

3 个分支,对应的确实是 yolo 82,yolo 94,yolo 106

后处理

经过推导后得到的数据,不是最终的数据,这个只是神经网络预测的数据,我们要经过一些基本的验证,置信度低的结果直接忽视,所以我们要经过一些后处理,这个通过 postprocess()完成。

代码语言:javascript复制
def postprocess(frame, outs):
    frameHeight = frame.shape[0]
    frameWidth = frame.shape[1]
 
    classIds = []
    confidences = []
    boxes = []
    
    classIds = []
    confidences = []
    boxes = []
    for out in outs:
        print("out size",out.shape)
        for detection in out:
            # 不同的数据集训练下的 label 数量不一样,yolov3 是在 coco 数据集上训练的,所以支持 80 种类别,输出层代表多个 box 的信息
            scores = detection[5:]
            classId = np.argmax(scores)
            confidence = scores[classId]
            if confidence > confThreshold:
               # x,y,width,height 都是相对于输入图片的比例,所以需要乘以相应的宽高进行复原
                center_x = int(detection[0] * frameWidth)
                center_y = int(detection[1] * frameHeight)
                width = int(detection[2] * frameWidth)
                height = int(detection[3] * frameHeight)
                left = int(center_x - width / 2)
                top = int(center_y - height / 2)
                classIds.append(classId)
                confidences.append(float(confidence))
                boxes.append([left, top, width, height])
                
 
    # 利用 NMS 算法消除多余的框,有些框会叠加在一块,留下置信度最高的框
    indices = cv2.dnn.NMSBoxes(boxes, confidences, confThreshold, 0.5)

    for i in indices:
        i = i[0]
        box = boxes[i]
        left = box[0]
        top = box[1]
        width = box[2]
        height = box[3]
        print(box)
        cv2.rectangle(frame,(left,top),(left width,top height),(0,0,255))
        return frame

我们注意到,第一个 for 循环中有这么一段。

代码语言:javascript复制
for out in outs:
    print("out size",out.shape)
    for detection in out:
       
        scores = detection[5:]
        classId = np.argmax(scores)
        confidence = scores[classId]
        if confidence > confThreshold:
            # x,y,width,height 都是相对于输入图片的比例,所以需要乘以相应的宽高进行复原
            center_x = int(detection[0] * frameWidth)
            center_y = int(detection[1] * frameHeight)
            width = int(detection[2] * frameWidth)
            height = int(detection[3] * frameHeight)
            left = int(center_x - width / 2)
            top = int(center_y - height / 2)
            classIds.append(classId)
            confidences.append(float(confidence))
            boxes.append([left, top, width, height])

很多同学可能会疑惑 detection[5:]是什么操作?

不同的数据集训练下的 label 数量不一样,yolov3 是在 coco 数据集上训练的,所以支持 80 种类别,输出层代表多个 box 的信息,是一个 Tensor,尺寸是 N*85。

N 代表经过前向推断,产生的 bbox 数量,85 分成两部分。

代码语言:javascript复制
[x,y,w,h,conf,score1,socre2,...score80]

前面代表 bbox 的位置尺寸置信度,后面 80 个参数分别代表这个 bbox 中类别的概率。

我们肯定选概率最高的那一个类别,作为这个 bbox 的类别。

但如果某个 bbox 的类别概率,也就是类别置信度太低时,它就被直接 pass 掉了,如下代码所以。

代码语言:javascript复制
    scores = detection[5:]
    classId = np.argmax(scores)
    confidence = scores[classId]
    if confidence > confThreshold:
        ...

再把经过筛选的 bbox 进行专业的 NMS 去重,就得到了最终的结果。

然后,就画出来好了。

最终运行结果,如下图所示。

运行 Yolov3 进行视频处理

对视频进行处理和对图片进行处理原理是一样的,不同的地方就是可以把视频分拆成一张张图片分别处理。

把 main 方法简单改造一下就好了

代码语言:javascript复制
def main():

    # 加载视频
    cap = cv2.VideoCapture("bottle_test.mp4")
    # 加载默认的摄像头视频流
    #cap = cv2.VideoCapture(0)

    while cap.isOpened():


        ret,img = cap.read()

        img = detect(img)

        if img is not None:

            cv2.imshow("test",img)

        # 按 ESC 键结束
        if cv2.waitKey(1) == 27:
            break
        
    cap.release()
    cv2.destroyAllWindows()

这样就可以运行了,但你可能发现视频会很卡顿。

这是因为,OpenCV 目前只支持 CPU 版本的 Yolov3,所以没有办法达到实时。不过,我们可以尝试一下 yolov3-tiny 版本,它更积极更小,速度也更快。当然,精确率也更低。

所以,你可以先下载 yolov3-tiny.cfg 和 yolov3-tiny.weights

代码语言:javascript复制
wget https://pjreddie.com/media/files/yolov3-tiny.weights

然后,在代码中加载它就好了。

代码语言:javascript复制
net = dnn.readNetFromDarknet("yolov3-tiny.cfg","yolov3-tiny.weights")

如果,你又想快又想准,你可以这样做。

  1. 用原生的 Darknet 配合 GPU 使用,或者用 Pytorch、Tensorflow 运行相应版本的 yolov3
  2. 自己去训练 Yolov3 的神经网络权重,让它符合你的期望
  3. 利用目标跟踪技术

当然,如果你之前对此一无所知,就不需要太在意这件细节,你直接去网络上搜别人训练好的比较高精度的模型,你直接加载就好了。如果,你因此兴趣更浓了,建议系统化学习一下深度学习,学习一下目标检测,你要意识到你一脚准备踏向更专业的 AI 领域。

文章源码:https://github.com/frank909zhao/LeaningYolos

0 人点赞