目标检测之二(传统算法和深度学习的源码学习)

时间:2022-08-19 14:12:50

目标检测之二(传统算法和深度学习的源码学习)

本系列写一写关于目标检测的东西,包括传统算法和深度学习的方法都会涉及到,注重实验而不着重理论,理论相关的看论文去哈,主要依赖opencv。

  本文主要内容:简单分析下yolo9000的原理,然后使用opencv的dnn模块进行目标检测.

    接着上一篇提到的车辆检测(http://blog.csdn.net/baolinq/article/details/78579317),使用了Haar+Adaboost算法进行车辆检测,对于简单场景的检测效果还不错,但是对于稍微复杂点的场景或者由于光照等原因的影响,检测效果不尽人意,但是我们看到深度学习的检测方法,比如YOLO的检测效果仍然很好。

    YOLO9000秉承着“奥运理念”, Better, Faster, Stronger,翻译出来的意思就是检测精度更高,检测速度更快,性能更强(更鲁棒),主要原理和YOLO v1一样,YOLO9000增加了更多的深度学习的训练tricks和借鉴了其他算法的优点,

目标检测之二(传统算法和深度学习的源码学习)

上图是YOLOv1论文(https://arxiv.org/abs/1506.02640)里面的截图,我们简单看一下它的原理:

(1) 给个一个输入图像,首先将图像划分成7 * 7的网格。

(2) 对于每个网格,每个网格预测2个boudingbox(每个box包含5个预测量)以及20个类别概率,总共输出7×7×(2*5+20)=1470个tensor

(3) 根据上一步可以预测出7 * 7 * 2 = 98个目标窗口,然后根据阈值去除可能性比较低的目标窗口,再由NMS去除冗余窗口即可。

YOLOv1使用了end-to-end的回归方法,没有regionproposal步骤,直接回归便完成了位置和类别的判定。种种原因使得YOLOv1在目标定位上不那么精准,直接导致YOLO的检测精度并不是很高。

YOLO9000用了这么多tricks,每一种提高一点点,组合起来就很强大了

目标检测之二(传统算法和深度学习的源码学习)

    YOLO9000略有不同YOLOv2最后输出的是S * S * (B * (5 + C))维,它直接使用每个boxes进行预测,而yolov1不是这样的,它是每个cell只负责一个object。其中参数S,B,C的取值也不同:

S=13,YOLOv2对输入图像(416*416,yolov1是448*448)进行了卷积和池化等,最终输出的特征图尺寸是13 * 13。

B=5,这是由Dimension Clusters得到的。Fast-RCNN中使用3种scales和3中aspectratios(1:1,1:2,2:1)在每个位置产生了9个anchor boxes。作者认为这种手动选取的anchor不够好,虽然网络最终可以学出来,但如果我们可以给出更好的anchor,那么网络肯定更加容易训练而且效果更好。作者使用kmeans算法进行训练,使用基于IOU的度量方式d(box,centroid)=1−IOU(box,centroid)代替一般场景下的欧氏距离。作者的实验表明这种做法比手动选取的效果更好

C=20,表示object的种类,如果是用voc数据集的话,C=20。如果是coco数据集,C=80。

 

详细的原理自己去网上搜一下有一大堆,不过还是建议看下原文(https://arxiv.org/abs/1612.08242),项目主页(https://pjreddie.com/darknet/yolo/)上面有关于YOLO的详细介绍以及很多训练好的model可以使用以及yolo的源代码,作者可以说是非常良心了,点个赞。

    直接先去github上面下载YOLO的源代码(https://github.com/pjreddie/darknet),代码写的非常nice,使用darknet网络结构,几乎不用环境就可以独立运行的,当然如果你要显示窗口的话,最好还是要有一个opencv,他也会自动把运行结果保存到当前目录,不过看起来不方便。根据自己的环境编译一下,使用命令行cmd窗口就可以运行了。输入图片路径即可进行检测。

运行截图:

目标检测之二(传统算法和深度学习的源码学习)

电脑显卡很渣,暂时使用的是no gpu版本的。

运行结果:

 目标检测之二(传统算法和深度学习的源码学习)

按任意键关闭窗口,即可继续输入图片路径继续进行目标检测

 目标检测之二(传统算法和深度学习的源码学习)

这样测试很不方便,每次只能测试一张图像,我想测试序列图像或者测试视频该怎么办呢?有两种方法,一种修改源代码,改写输入文件的类型;第二种更简单使用opencv的dnn模块(Depth neural network)。详情见opencv官方文档https://docs.opencv.org/master/d6/d0f/group__dnn.html

我以第二种方法为例,我使用的是opencv3.3。Dnn模块是opencv新推出的用于深度学习的一个模块,目前支持Caffe、TensorFlow、Torch、PyTorch、darknet等深度学习框架。我觉得这个功能真的很赞,以后训练好的模型,移植就非常方便了,用来做一些日常测试也是很不错的。

目标检测之二(传统算法和深度学习的源码学习)

dnn::Net net=readNetFromDarknet()就可以从参数中读取darknet网络参数以及训练好的模型权重。

先把输入图像归一化一下,然后输入图像到网络中,再进行前向传播,就OK了,是不是非常的easy啊。剩下的只是怎么显示和保存数据的问题了,处理结果已经保存到detectionMat里面去了。

       //! [Prepare blob]
       Mat inputBlob = blobFromImage(resized, 1 / 255.F); //Convert Mat to batch of images
       //! [Prepare blob]
       //! [Set input blob]
       net.setInput(inputBlob, "data");               //set the network input
       //! [Set input blob]
 
       //! [Make forward pass]
       cv::Mat detectionMat = net.forward("detection_out");    //compute output
       //! [Make forward pass]

Talk is cheap,show me thecode。给出测试源码,其实也就是稍微改了一下opencv的samples。

 

#include <opencv2/dnn.hpp>
#include <opencv2/dnn/shape_utils.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
using namespace cv;
using namespace cv::dnn;

#include <fstream>
#include <iostream>
#include <algorithm>
#include <cstdlib>
using namespace std;

const size_t network_width = 416;
const size_t network_height = 416;

const char* about = "This sample uses You only look once (YOLO)-Detector "
"(https://arxiv.org/abs/1612.08242)"
"to detect objects on image\n"; // TODO: link

const char* params
= "{ help           | false | print usage         }"
"{ cfg            |    yolo-voc.cfg   | model configuration }"	//写入训练好的网络参数文件cfg格式
"{ model          |   yolo-voc.weights    | model weights       }"//写入训练好的模型的权重文件
"{ image          |       | image for detection }"				//测试的图片名,我没有使用
"{ min_confidence | 0.24  | min confidence      }";		//信心度阈值,低于该阈值的不输出,判断为没有检测到

vector<cv::Scalar> color = { Scalar(0,205,150),Scalar(255,0,0),Scalar(0,0,255),Scalar(200,150,0) };
int main(int argc, char** argv)
{
    cv::CommandLineParser parser(argc, argv, params);	//读入params参数文件
    
    if (parser.get<bool>("help"))
    {
        std::cout << about << std::endl;
        parser.printMessage();
        return 0;
    }
    
    //String modelConfiguration = "yolo.cfg"; // parser.get<string>("cfg");
    //String modelBinary = "yolo.weights"; //parser.get<string>("model");
	String modelConfiguration = parser.get<string>("cfg");
	String modelBinary =parser.get<string>("model");

    //! [Initialize network]
    dnn::Net net = readNetFromDarknet(modelConfiguration, modelBinary);//读取网络模型和参数,初始化
    //! [Initialize network]
    
    if (net.empty())
    {
        cerr << "Can't load network by using the following files: " << endl;
        cerr << "cfg-file:     " << modelConfiguration << endl;
        cerr << "weights-file: " << modelBinary << endl;
        cerr << "Models can be downloaded here:" << endl;
        cerr << "https://pjreddie.com/darknet/yolo/" << endl;
        exit(-1);
    }

	//  String imgpath =parser.get<string>("image")
    //cv::Mat frame = cv::imread(imgpath);

    // 检测目标种类的名称在voc.txt,读入保存到tag_names向量中
    vector<string> tag_names;
    ifstream fin;
    fin.open("voc.txt");
    string buf;
    while(fin && getline(fin, buf)) {
        tag_names.push_back(buf);
    }
	fin.close();

	//打开默认摄像头,用于检测摄像头的图像
	//VideoCapture cap(0);
	//if (!cap.isOpened())
	//	return -1;

	//图片的存放地址(相对路径),在当前目录的test_image文件夹里面
	String imgpath = "test_image/";
	vector<cv::String> imgvec;
	cv::glob(imgpath, imgvec);//保存文件名到imgvec向量中,便于批量处理
	Mat frame;
	bool stop = false;
	//while(!stop){

	int64 start = getTickCount();	//计算运行时间
	for (int imgnum = 0; (imgnum < imgvec.size())&&(!stop); ++imgnum) {
		
		//if (!cap.read(frame))
		//	break;
		frame = imread(imgvec[imgnum]);
		//imshow("原图",frame);
		//waitKey(5);
		//! [Resizing without keeping aspect ratio]
		cv::Mat resized;
		cv::resize(frame, resized, cv::Size(network_width, network_height));
		//! [Resizing without keeping aspect ratio]

		//! [Prepare blob]
		Mat inputBlob = blobFromImage(resized, 1 / 255.F); //Convert Mat to batch of images
		//! [Prepare blob]

		//! [Set input blob]输入归一化的图像
		net.setInput(inputBlob, "data");                //set the network input
		//! [Set input blob]

		//! [Make forward pass]前向传播进行计算
		cv::Mat detectionMat = net.forward("detection_out");    //compute output
		//! [Make forward pass]


		float confidenceThreshold = parser.get<float>("min_confidence");
		for (int i = 0; i < detectionMat.rows; i++)
		{
			const int probability_index = 5; //前5个参数为每一个bounding box包含5个值:x,y,w,h和confidence(置信度),置信值代表box包含一个目标的置信度
			const int probability_size = detectionMat.cols - probability_index;//后20个为voc数据集中有20个类别,每个类的概率
			float *prob_array_ptr = &detectionMat.at<float>(i, probability_index);

			size_t objectClass = std::max_element(prob_array_ptr, prob_array_ptr + probability_size) - prob_array_ptr;//选择一个概率最大的作为检测结果
			float confidence = detectionMat.at<float>(i, (int)objectClass + probability_index);

			if (confidence > confidenceThreshold)//大于信心度阈值才输出
			{
				float x = detectionMat.at<float>(i, 0);//检测的矩形框的计算,本来是中心点和长宽,这都是被归一化后的
				float y = detectionMat.at<float>(i, 1);
				float width = detectionMat.at<float>(i, 2);
				float height = detectionMat.at<float>(i, 3);
				float xLeftBottom = (x - width / 2) * frame.cols;//得到图像上的矩形框的左上角和右下角坐标,要反归一化
				float yLeftBottom = (y - height / 2) * frame.rows;
				float xRightTop = (x + width / 2) * frame.cols;
				float yRightTop = (y + height / 2) * frame.rows;

				//std::cout << "Class: " << tag_names[objectClass] << std::endl;
				//std::cout << "Confidence: " << confidence << std::endl;

				//std::cout << " " << xLeftBottom
				//	<< " " << yLeftBottom
				//	<< " " << xRightTop
				//	<< " " << yRightTop << std::endl;
				stringstream ss;
				ss << confidence;
				string str_text = tag_names[objectClass] + ": "+ss.str();//输出类别和信心度

				Rect object((int)xLeftBottom, (int)yLeftBottom,
					(int)(xRightTop - xLeftBottom),
					(int)(yRightTop - yLeftBottom));

				rectangle(frame, object, color[objectClass % 4],2);	//显示矩形框,我给了一个颜色数组,避免都是一个色的不好看
				int baseline = 0;
				Size labelSize = getTextSize(str_text, FONT_HERSHEY_SIMPLEX, 0.5,1, &baseline);//设置字体
				//int x = xLeftBottom - labelSize.width >= 0 ? xLeftBottom - labelSize.width : xLeftBottom + labelSize.width;
				int rect_y = yLeftBottom - labelSize.height >= 0 ? yLeftBottom - labelSize.height : yLeftBottom ;//避免在边界时,显示的内容看不到

				rectangle(frame, Rect(Point(xLeftBottom, rect_y),Size(labelSize.width,labelSize.height+baseline)), Scalar(0, 255, 0),CV_FILLED);
				//输出显示内容
				cv::putText(frame, str_text, Point(xLeftBottom, rect_y+labelSize.height), FONT_HERSHEY_SIMPLEX, 0.5, color[objectClass%4]);

			}
		}
		//cout << "用时: "<<1.0*(getTickCount() - start) / getTickFrequency() << endl;
		imshow("detections", frame);//显示图像
		//waitKey();
		if (waitKey(5) == 'q')
			stop = true;
	}
	//cout << "用时: " << 1.0*(getTickCount() - start) / getTickFrequency() << endl;//计算所有图片共运行的时间
    return 0;
} // main


代码上都有比较详细的注释,稍微看过论文或者看过其他人的写的论文笔记的,了解yolo的原理的都应该能看懂。我们简单分析一下网络的输出矩阵detectionMat,根据上面的分析。我们知道YOLO9000的输出是13*13*5*(5+20)维 的向量。

目标检测之二(传统算法和深度学习的源码学习)

果然像我们分析的一样,调试时可以看到detectionMat是一个845*25维的矩阵,相当于一共845个boxes,每行表示一个boxes,每行有25列,分别表示矩形框的定位(4),该boxes的置信值(confidence score)。置信值代表box包含一个目标的置信度,以及20个类别的概率值。

知道了网络输出矩阵的意义,我们只需要分别取出来即可,每次只取类别概率最大的那一类,作为预测结果,然后跟confidenceThreshold作比较,超过阈值才认为是正确的预测,否则认为没有预测到。

 目标检测之二(传统算法和深度学习的源码学习)

最后就是预测框的计算了,因为作者模仿了faster-rcnn的做法,使用偏移量表示位置,而不是直接预测,实验表明这样做更容易训练,且可以提高mAP。作者就没有采用预测直接的offset的方法,而使用了预测相对于grid cell的坐标位置的办法,作者又把ground truth限制在了0到1之间,利用logistic回归函数来进行这一限制。所以最后计算出来的坐标需要去归一化,乘以图像的宽或者高。

目标检测之二(传统算法和深度学习的源码学习)

 

最后部分的显示,我就不多说了,自己看看opencv的函数就可以了。

 

搞定收工。写的有点啰嗦,本来想和第一篇写在一起,结果写了这么多,如果你看到了这里,我只能说给你真有耐心,给你点个赞。下篇见~~

参考文献

[1] Redmon J, Farhadi A. YOLO9000: Better, Faster, Stronger[J]. 2016.

[2]http://blog.csdn.net/jesse_mx/article/details/53925356