本文共 10854 字,大约阅读时间需要 36 分钟。
导言
在这篇文章中,我们将用Java构建一个实时视频对象检测应用程序,用于对汽车进行检测,这是自动驾驶系统的关键组成部分。在之前的文章中,我们构建了一个图像分类器,本文我们要检测物体(如汽车、行人),并用矩形标记它们。
对象分类
首先,我们存在对象分类的问题,假如有一个图像,我们想知道这个图像是否包含任何特定的类别,即包含汽车或者不包含。
我们在之前的文章中看到如何使用VGG-16等现有架构和转移学习构建图像图像分类器。
对象本地化
现在我们能够很自信的说出图像是否具有特定对象,通常使用矩形对对象进行标记。
除了图像分类,还需要指定图像中对象的位置,这是通过定义边界框完成的。边界框通常由中心(b x,b y),矩形高度(b h)和矩形宽度(b w)表示。
现在,我们需要在训练数据中为图像中的每个对象定义这四个变量。此外,网络不仅会输出图像类别编号的概率(即20%cat [1],70%dog [2],10%tiger [3]),还会输出上面定义边界框的四个变量。
只需要提供边界框(中心、高度、宽度),我们的模型通过提供更多详细的图像内容视图来输出/预测更多信息。
不难想象在图像训练数据中加入更多的点可以让我们更深入的了解图像。例如,将点放在人脸上(如眼睛、嘴巴)可以告诉我们这个人正在微笑、哭泣等。
物体检测
我们甚至可以更进一步,不仅定位一个对象,还可以定位图像中的多个对象。这就需要我们进行物体检测。
虽然结构没有很大的改变,但是这个问题有点难度,因为需要准备大量的数据。原则上,我们只是将图像划分为较小的矩形,对于每个矩形,有相同的另外五个变量:P c,(b x,b y),b h,b w 和正常预测概率(20%的猫[1],70%的狗[2])。在下文中我们将讲解更多的细节。
滑动窗口解决方案
这是一个非常直观的解决方案,你可以自己动手。我们不使用普通的汽车图像,而是尽可能的裁剪,因此图像只包含汽车。
使用裁剪后的图像训练类似于VGG-16或任何其他深度卷积神经网络。
这样做很有效,但该模型经过训练只可以检测有汽车的图像,因此它对真实图像造成麻烦,因为真实的图像中不仅有汽车,还有树木、人物和标志。此外,真实图像会更大一些。
为了克服这些问题,我们只分析图像的一小部分,并试图弄明白该部分是否有车。更确切的说,我们可以使用滑动矩形窗口扫描图像,每次都让我们的模型预测其中是否有车。我们看这样的例子:
总而言之,我们使用正常的卷积神经网络(VGG-16)来训练具有不同大小的裁剪图像的模型,然后使用矩形来扫描图像以寻找对象(汽车)。我们可以看到,通过使用不同尺寸的矩形,找出不同形状和位置的汽车。
这个算法并不复杂,并且非常有效。但是它有两个缺点。
1、性能。我们需要多次向模型询问预测结果。每次矩形移动时,都需要执行模型以获得预测。还要针对不同的矩形尺寸再次执行此操作。解决性能的一种方法是增加矩形的步幅,但是可能导致无法检测到一些对象。在过去,模型大多是线性的,并且具有手工设计的特征,因此预测并不昂贵。因此,这个算法非常好。如今,网络规模(VGG-16的参数为1.38亿个),这种算法速度很慢,对自动驾驶等实时视频对象检测几乎没有用。
当移动矩形(向右和向下)时,许多共享像素不会被重复使用,而是重新计算。在接下来的部分,我们将看到一种通过使用卷积来克服此问题的最先进技术。
2、即使使用不同大小的边界框,也可能无法使用边界框精确的标记对象。该模型可能无法输出非常准确的边界框。例如,框可以仅包括对象的一部分。接下来的部分将探讨YOLO算法,它为我们解决了这个问题。
卷积滑动窗口解决方案
在之前的文章中看到,图像分类体系结构,无论其大小和配置如何,为不同数量的层提供完全连接的神经网络,并根据类输出多个预测。
为简单起见,我们将以较小的网络模型为例,但相同的逻辑对任何卷积网络(VGG-16、AlexNet)都有效。
这个简单的网络将大小为32*32*3的彩色图像作为输入。它使用相同的卷积3*3*64以获得32的输出。它使用最大池化层来减少宽度和高度,并保持第三维不变(16*16*64)。之后,我们为一个完全连接的神经网络提供两个隐藏层,每个隐藏层有256个和128个神经元。最后,输出五个类的概率(通常使用soft-max)。
让我们看看我们如何使用卷积层替换全连接层,同时保持相同效果。
刚用卷积滤波器替换了全连接层。实际上,16*16*256卷积滤波器是16*16*64*256的矩阵(多个滤波器),因为第三维始终与输入第三维相同。为简单起见,我们将其称为16*16*256.这实际上意味着相当于一个全连接层,因为输出1*1*256的每个元素都是输入16*16*64的线性函数。
卷积滑动窗口
为了看到用卷积滤波器替换全连接背后的想法,我们输入比32*32*3的原始输入图像大的输入图像。让我们拍摄36*36*3的输入图像:
此图像有四行四列(绿色36*36),比原始图像(蓝色32*32)多。如果我们使用stride = 2和全连接的滑动窗口,那么需要将原始图像大小移动九次以覆盖所有内容(三个移动显示为黑色矩形)。因此,也要执行九次模型。
让我们尝试将这个新的更大的矩阵作为输入应用于新的模型,仅使用卷积层。
正如我们所看到的,输出从1*1*5变为3*3*5,与全连接相比,输出将始终为1*1*5.回想一下,我们必须将滑动窗口移动九次以覆盖所有图像,是不是等于3*3?实际上,那些3*3各自代表滑动窗口的1*1*5级概率预测结果。
这是一种非常先进的技术,因为我们能够一次获得所有的九个结果,而无需多次执行具有数百万个参数的模型。
YOLO
虽然我们已经通过引入卷积滑动窗口解决了性能问题,但是模型仍然可能无法输出非常精确的边界框。让我们看看YOLO如何解决这个问题。
首先,我们通常会对每个图像进行标记并标记要检测的对象。每个对象都由带有四个变量的边界框标记,对象的中心(b x,b y),矩形高度(b h),矩形宽度(b w)。之后,每个图像被分为较小数量的矩形,通常是19*19个矩形,但是为了简单,我们使用8*9.
边界框(红色)和对象可以是几个框(蓝色)的一部分,因此我们仅将对象和边界框分配给拥有对象中心的框(黄色)。我们用四个附件变量训练模型(除了告诉对象是汽车b x,b y,b h和b w),并将它们分配给拥有中心的框(b x,b y)。在使用此标记数据训练神经网络之后,让模型预测四个变量、值或边界框。
我们让模型学习如何使用边界框标记对象,而不是使用预定义的边界框大小进行扫描并尝试拟合对象。因此,边界框是灵活的,而且准确度更高。
让我们看看如何能够代表输出,因为除了1-car,2-pedestrian等类之外,我们还有其他四个变量(b x,b y,b h,b w)。实际上,还添加了另一个变量P c,它简单的告诉图像是否具有我们想要检测的任何对象。
P c = 1(红色)表示至少有一个对象,因此值得查看概率和边界框。
P c = 0(红色)表示图像没有我们想要的对象,因此我们不关心概率或边界框。
边界框规范
我们要以特定的方式标记训练数据,以使YOLO算法正常工作。YOLO V2格式要求边界框尺寸b x,b y和b h,b w相对于原始图像宽度和高度。假设有一个图像300*400,边界框尺寸是Bwidth =30, Bheight=15, Bx=150, By=80,必须转换为Bw=30/300, Bh=15/400, Bx=150/300, By=80/400。
本文展示了如何使用BBox标签工具标记数据。除了YOLO如何在内部标记训练数据之外,预测的执行方式也略有不同。
YOLO预测的边界框是相对于拥有对象中的框(黄色)定义的。框的左上角从(0,0)开始,右下角是(1,1)。中心(b x,b y)肯定在范围0-1(Sigmoid函数确定),因为该点在框内。b h,b w 与w和h 的值(黄色)成比例的计算,因此值可以大于1.在图中,我们看到边界框的宽度几乎是框宽的1.8倍。b h约为框高h的1.6倍。
在预测之后,我们看到预测框与开头标记真实边界框相交多少。基本上,我们尝试最大化它们之间的交集,因此,在理想情况下,预测的边界框与标记的边界框完全相交。
原则是这样的:使用边界框()提供更专业的标记数据,拆分图像,并分配到包含中心的框(负责检测对象),使用卷积滑动窗口网络进行训练,并预测对象及其位置。
存在的两个问题
首先,即使在训练时间内,对象被分配到一个框(包含对象中心的框),在测试是(预测时)几个框(黄色)可能认为他们具有对象的中心(红色),因此为同一个对象定义自己的边界框。目前,Deeplearning 4j没有提供实现,所以在GitHub上找了一个简单的实现,它的作用是首先选择具有最大的框作为预测概率(它不仅有1或0值,而是可以在0-1范围内)。然后,删除与该框相交超过某个阈值的每个框。它再次启动相同的逻辑,直到没有剩余的边界框。
其次,由于我们预测多个物体(汽车、行人、信号灯等),可能会发生两个或多个物体的中心是相同的。使用anchor boxes,我们选择几种形状的边界框,我们发现更多用于我们想要检测的对象。YOLO V2论文使用k-means算法完成此操作,也可以手动完成。之后,我们修改输出以包含之前看到的相同结构(P c,b x,b y,b h,b w,C1,C2 ...),但是对于每一个选定的anchor boxes形状,所以,现在可能有这样的事情:
应用
训练深度网络需要付出很大的努力,并且需要有意义的数据和处理能力。我们将使用Tiny YOLO,它基于Darknet参考网络,比普通的YOLO模型快得多,但是不太精确。要使用受过VOC训练的版本:
1wget https://pjreddie.com/media/files/tiny-yolo-voc.weights2./darknet detector test cfg/voc.data cfg/tiny-yolo-voc.cfg tiny-yolo-voc.weights data/dog.jpg//pjreddie.com/media/files/tiny-yolo-voc.weights 2./darknet detector test cfg/voc.data cfg/tiny-yolo-voc.cfg tiny-yolo-voc.weights data/dog.jpg
在GPU上,它以大于200FPS运行。
目前发布的版本Deeplearning 4j 0.9.1不停工TinyYOLO,但是0.9.2-SNAPSHOT确实如此。首先我们需要在Maven加载SNAPSHOT:
12 183 6a 4http://repo1.maven.org/maven2/ 57 17snapshots-repo 8https://oss.sonatype.org/content/repositories/snapshots 910 12false 1113 16true 14daily 1519 20 24org.deeplearning4j 21deeplearning4j-core 22${deeplearning4j} 2325 2org.nd4j 26nd4j-native-platform 27${deeplearning4j} 283 6a 4http://repo1.maven.org/maven2/ 57 17 18snapshots-repo 8https://oss.sonatype.org/content/repositories/snapshots 910 12false 1113 16true 14daily 1519 20 24org.deeplearning4j 21deeplearning4j-core 22${deeplearning4j} 2325 org.nd4j 26nd4j-native-platform 27${deeplearning4j} 28
然后,准备较短的代码加载模型:
1private TinyYoloPrediction() {2 try {3 preTrained = (ComputationGraph) new TinyYOLO().initPretrained();4 prepareLabels();5 } catch (IOException e) {6 throw new RuntimeException(e);7 }8}private TinyYoloPrediction() { 2 try { 3 preTrained = (ComputationGraph) new TinyYOLO().initPretrained(); 4 prepareLabels(); 5 } catch (IOException e) { 6 throw new RuntimeException(e); 7 } 8}
preareLabels只是使用来自用于训练模型的数据集PASCAL VOC的标签。
视频帧捕获使用JavaCV与CarVideoDetection:
1FFmpegFrameGrabber grabber; 2grabber = new FFmpegFrameGrabber(f); 3grabber.start(); 4while (!stop) { 5 videoFrame[0] = grabber.grab(); 6 if (videoFrame[0] == null) { 7 stop(); 8 break; 9 }10 v[0] = new OpenCVFrameConverter.ToMat().convert(videoFrame[0]);11 if (v[0] == null) {12 continue;13 }14 if (winname == null) {15 winname = AUTONOMOUS_DRIVING_RAMOK_TECH + ThreadLocalRandom.current().nextInt();16 }17 if (thread == null) {18 thread = new Thread(() -> {19 while (videoFrame[0] != null && !stop) {20 try {21 TinyYoloPrediction.getINSTANCE().markWithBoundingBox(v[0], videoFrame[0].imageWidth, videoFrame[0].imageHeight, true, winname);22 } catch (java.lang.Exception e) {23 throw new RuntimeException(e);24 }25 }26 });27 thread.start();28 }29 TinyYoloPrediction.getINSTANCE().markWithBoundingBox(v[0], videoFrame[0].imageWidth, videoFrame[0].imageHeight, false, winname);30 imshow(winname, v[0]); 2grabber = new FFmpegFrameGrabber(f); 3grabber.start(); 4while (!stop) { 5 videoFrame[0] = grabber.grab(); 6 if (videoFrame[0] == null) { 7 stop(); 8 break; 9 } 10 v[0] = new OpenCVFrameConverter.ToMat().convert(videoFrame[0]); 11 if (v[0] == null) { 12 continue; 13 } 14 if (winname == null) { 15 winname = AUTONOMOUS_DRIVING_RAMOK_TECH + ThreadLocalRandom.current().nextInt(); 16 } 17 if (thread == null) { 18 thread = new Thread(() -> { 19 while (videoFrame[0] != null && !stop) { 20 try { 21 TinyYoloPrediction.getINSTANCE().markWithBoundingBox(v[0], videoFrame[0].imageWidth, videoFrame[0].imageHeight, true, winname); 22 } catch (java.lang.Exception e) { 23 throw new RuntimeException(e); 24 } 25 } 26 }); 27 thread.start(); 28 } 29 TinyYoloPrediction.getINSTANCE().markWithBoundingBox(v[0], videoFrame[0].imageWidth, videoFrame[0].imageHeight, false, winname); 30 imshow(winname, v[0]);
所以代码正在做的是从视频中获取帧并传递给TinyYOLO预训练模型。从那里,首先将图像帧缩放到416*416*3,然后将其提供给TinyYOLO以预测和标记边界框。
1public void markWithBoundingBox(Mat file, int imageWidth, int imageHeight, boolean newBoundingBOx,String winName) throws Exception { 2 int width = 416; 3 int height = 416; 4 int gridWidth = 13; 5 int gridHeight = 13; 6 double detectionThreshold = 0.5; 7 Yolo2OutputLayer outputLayer = (Yolo2OutputLayer) preTrained.getOutputLayer(0); 8 9 INDArray indArray = prepareImage(file, width, height);10 INDArray results = preTrained.outputSingle(indArray);11 predictedObjects = outputLayer.getPredictedObjects(results, detectionThreshold);12 System.out.println("results = " + predictedObjects);13 markWithBoundingBox(file, gridWidth, gridHeight, imageWidth, imageHeight);14 imshow(winName, file);15}public void markWithBoundingBox(Mat file, int imageWidth, int imageHeight, boolean newBoundingBOx,String winName) throws Exception { 2 int width = 416; 3 int height = 416; 4 int gridWidth = 13; 5 int gridHeight = 13; 6 double detectionThreshold = 0.5; 7 Yolo2OutputLayer outputLayer = (Yolo2OutputLayer) preTrained.getOutputLayer(0); 8 9 INDArray indArray = prepareImage(file, width, height); 10 INDArray results = preTrained.outputSingle(indArray); 11 predictedObjects = outputLayer.getPredictedObjects(results, detectionThreshold); 12 System.out.println("results = " + predictedObjects); 13 markWithBoundingBox(file, gridWidth, gridHeight, imageWidth, imageHeight); 14 imshow(winName, file); 15}
在预测后,我们应该准备好边界框尺寸的预测值,我们已经实现了菲最大抑制,因为正如我们所提到的,YOLO在测试时预测每个对象有多个边界框。
之后,使用另一个线程(在播放视频的那个线程旁边),我们更新视频以获得矩形好喝检测到的对象的标签。
考虑到在CPU上运行,预测非常快(实时);在GPU上,将有更好的实时检测效果。
结果展示
只需要从源代码运行Run,或者如果你不想使用IDE打开它,只需运行mvn clean install exec:java即可。
运行后,可以看到以下视图:
长按二维码 ▲
订阅「架构师小秘圈」公众号
如有启发,帮我点个在看,谢谢↓
转载地址:http://ofmef.baihongyu.com/