使用GoogleNet进行图像分类
前提环境
本文所用环境VS2017+OpenCV4.4+win10
模型下载
在贾老师的github上有模型的完整文件
https://github.com/gloomyfish1998/opencv_tutorial/
本文将其下载到D:\OpenCV\project\下
本文所用模型
打开下列文件路径
D:\OpenCV\project\opencv_tutorial-master\data\models\googlenet
可以看到三个文件(在程序中都会用到)
- bvlc_googlenet.caffemodel这是个caffe模型,在OpenCV中支持离线加载,不依赖caffe,这是模型的权重文件
- bvlc_googlenet.prototxt这是模型的描述文件
- classification_classes_ILSVRC2012.txt是一个标签文件,使用editplus打开可以看到1000个分类标签
图像分类模型介绍
• Inception模型 来自Google - Going Deeper with Convolutions, CVPR 2015
• 输入:[NxCxHxW], 通道顺序:RGB or BGR
• 输出:softmax层 – prob,Nx1000
• 基于ImageNet数据集
部分操作
引入头文件
#include <opencv2/opencv.hpp> //opencv头文件
#include <opencv2/dnn.hpp> //包含dnn模块的头文件
#include <iostream> //输入输出流
#include <fstream> //文件流进行txt文件读取
using namespace cv; //OpenCV的空间
using namespace cv::dnn; //包含dnn的命名空间
using namespace std; //输出输出流的空间
用于加载模型的函数
(1)在OpenCV中加载模型要通过readNetWork函数
(2)readNet函数有三个参数,参数1加载网络(caffe、TensorFlow等),参数2,3为权重路径和描述文件
(3)或者readNetFromCaffe(TensorFlow、darknet、ONNX等),意思为读进来的必须是caffe模型,此外还支持TensorFlow等模型,只有两个参数
(4)还有readNetFromModelOptimizer从dnn模型优化器中读取模型优化后的模型
Net net = readNetFromCaffe(protxt, bin_model); //此处只能加载caffe的
计算后台设置
(OpenCVdnn模块支持设置不同的计算后台和在不同的设备上进行)
- 设置计算后台
-
net.setPreferableBackend(DNN_BACKEND_OPENCV); //setPreferableBackend实际计算后台,default默认是DNN_BACKEND_OPENCV作为计算后台,使用此就行(也有加速后台)
- 设置计算设备
net.setPreferableTarget(DNN_TARGET_CPU); //设置在什么设备上进行计算(opencl(需要有interl图形卡)、FPGA、CPU)
当上面两个设置之后,他在执行网络进行推算时就会执行此计算后台进行计算(不同的计算后台有不同的效果,速度也有差别)
具体操作步骤
1.加载模型(读取网络信息)
部分代码如下:
string bin_model = "D:/OpenCV/project/opencv_tutorial-master/data/models/googlenet/bvlc_googlenet.caffemodel"; //定义模型权重文件的加载路径
string protxt = "D:/OpenCV/project/opencv_tutorial-master/data/models/googlenet/bvlc_googlenet.prototxt"; //定义模型描述文件的加载路径
//Imagenet支持1000个分类,分类类别在classification_classes_ILSVRC2012.txt可以看到,一直到1000
//有了模型的权重和描述文件就可以加载模型了
//在OpenCV中加载模型要通过readNetWork,
//readNet函数有三个参数,参数1加载网络(caffe、TensorFlow等),参数2,权重路径,参数3,描述文件
//或者readNetFromCaffe(TensorFlow、darknet、ONNX等),意思为读进来的必须是caffe模型,此外还支持TensorFlow等模型,只有两个参数
//还有readNetFromModelOptimizer从dnn模型优化器中读取模型优化后的模型
Net net = readNetFromCaffe(protxt, bin_model);
//设置计算后台(OpenCVdnn模块支持设置不同的计算后台和在不同的设备上进行)
net.setPreferableBackend(DNN_BACKEND_OPENCV); //setPreferableBackend实际计算后台,default默认是DNN_BACKEND_OPENCV作为计算后台,使用此就行(也有加速后台ENGINE)
net.setPreferableTarget(DNN_TARGET_CPU); //设置在什么设备上进行计算(opencl(需要有interl图形卡)、FPGA、CPU)
//当上面两个设置之后,他在执行网络进行推算时就会执行此计算后台进行计算(不同的计算后台有不同的效果,速度也有差别)
//获取各层信息
vector<string> layer_names = net.getLayerNames(); //此时我们就可以获取所有层的名称了,有了这些可以将其ID取出
for (int i = 0; i < layer_names.size(); i++) {
int id = net.getLayerId(layer_names[i]); //通过name获取其id
auto layer = net.getLayer(id); //通过id获取layer
printf("layer id:%d,type:%s,name:%s\n", id, layer->type.c_str(), layer->name.c_str()); //将每一层的id,类型,姓名打印出来(可以明白此网络有哪些结构信息了)
//printf("name2:%s\n", layer_names[i].c_str());
}
输出网络的每层信息如下
可以看到共有142层网络信息,其中有Convolution卷积层,relu激活函数,pooling池化层等信息,我们可以清晰的了解此网络结构
2.构建输入
参考链接:https://www.pianshen.com/article/9895277588/
使用模型实现预测的时候,需要读取图像作为输入,网络模型支持的输入数据是四维的输入,所以要把读取到的Mat对象转换为四维张量,OpenCV的提供的API为如下:
Mat blobFromImage(
InputArray image, //输入图像
double scalefactor = 1.0, //默认是1.0表示0-255范围的
const Size & size = Size(), //网络接受的数据大小
const Scalar & mean = Scalar(), //表示训练时数据集的均值
bool swapRB = false, //是否互换Red与Blur通道(有些网络需要的输入通道类型为RGB类型,而OpenCV读取的是BGR类型)
bool crop = false, //crop是剪切
int ddepth = CV_32F //ddepth是数据类型。
)
- 构建输入可以参考OpenCV自带的配置文件,根据此文件进行构建输入
D:\OpenCV\opencv-4.4.0-vc14_vc15\opencv\sources\samples\dnn\models.yml
打开文件可以看到,上方链接为模型的下载链接,下方参数为构建输入,进行图像预处理时的参数
- 在描述文件中也有构建输入时的部分信息
以下为构建输入的部分代码
Mat src = imread("G:/OpenCV/opencv笔记所用图片/plane.jpg"); //plane
if (src.empty()) {
cout << "could not load image.." << endl;
getchar();
return -1;
}
imshow("src", src);
因为googlenet的通道类型是RGB通道的,而OpenCVread读取的是BGR通道的,要进行通道转换(实际在后面的blobFromImage就能进行通道转换,此处无需转换)
//Mat rgb;
//cvtColor(src, rgb, COLOR_BGR2RGB);
//构建输入
//使用blobFromImage函数对单张图像进行预处理,使其符合网络的输入
//对于网络来说原始输入层的时候都有指定的输入大小
int w = 224;
int h = 224;
Mat inputBlob = blobFromImage(src, 1.0, Size(w, h),Scalar(104, 117, 123), false,false); //我们要将图像resize成224*224的才是我们神经网络可以接受的宽高
//参数1:输入图像,参数2:默认1.0表示0-255范围的,参数3:设置输出的大小,参数4:均值对所有数据中心化预处理,参数5:是否进行通道转换,参数6:,参数7:默认深度为浮点型
3.输入网络并推测得到输出
//设置输入
//前面已经将图像预处理完成,就可以执行分类了
//现在要将其输入到创建的网络中
net.setInput(inputBlob);
//进行推断得到输出
//让网络执行得到output,调用forward可以得到一个结果
//此处不给参数,得到的是最后一层的结果,也可以输入层数得到任何一层的输出结果
Mat probMat = net.forward(); //通过前面的输出层看最后一层,可以知道输出100个分类,每个分类的得分是多少
//softmax层出来的总和为1,找到max值对应的index就知道对应的分类了,在classification_classes_ILSVRC2012.txt可以知道具体名称
//上方得到的probMat是1000*1*1(有imagewatch得到)
4.解析输出结果
在上面使用forward函数得到的输出结果都保存在probMat 变量中,里面是1000*1的1通道的Mat数组,1000对应1000个分类标签,每个值都是在0-1之间,值越大表示当前处于位置的类别的可能性就越大。
首先读取标签文件,此标签文件中的类名需要按一定顺序得到,令其与索引index一一对应,在后面的打印分类得到的类名时会用到。
定义一个读取文件的函数readLabels()
vector<string> readLabels() {
string label_map_txt = "D:/OpenCV/project/opencv_tutorial-master/data/models/googlenet/classification_classes_ILSVRC2012.txt";
vector<string> classNames;
ifstream fp(label_map_txt);
if (!fp.is_open()) {
printf("could not find the file \n");
exit(-1);
}
std::string name;
while (!fp.eof()) { //当没有结束时就一直读取
getline(fp, name); //每次读取一行
if (name.length()) {
classNames.push_back(name);
}
}
fp.close(); //关闭文件输入流
return classNames;
}
main函数中的部分代码
//要解析输出(forward得到的)
//读取标签文件
vector<string> names = readLabels(); //使用自定义函数读取特定文件中的类名到names中,在后面输出对应名称时会用到
//对数据进行序列化(变成1行n列的,可以在后面进行方便的知道是哪个index了)
Mat prob = probMat.reshape(1, 1); //reshape函数可以进行序列化,(输出为1通道1行的数据,参数1:1个通道,参数2:1行)将输出结果变成1行n列的,但前面probMat本身就是1000*1*1
//实际结果probMat和prob相同
//当其他网络probMat需要序列化的时候,reshape就可以了
//此时找到最大的那个
Point classNum;
double classProb;
minMaxLoc(prob, NULL, &classProb, NULL, &classNum);//此时只获取最大值及最大值位置,最小值不管他
int index = classNum.x; //此时得到的是最大值的列坐标。就是其类的索引值,就可以知道其类名了
printf("\n current index=%d,possible:%2f,name=%s\n",index,classProb,names[index].c_str()); // current index=812,possible:0.999675,用editplus打开classification_classes_ILSVRC2012.txt,找到第813行(因为812+1)就是结果
//这样自己在classification_classes_ILSVRC2012.txt找答案挺麻烦的,我们使用程序打开此文件,得到类名即可
//此时可以将名称打印到图片上去
putText(src, names[index].c_str(), Point(50, 50), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(0, 0, 255), 2, 8);
imshow("result", src);
输出结果如下图
完整代码
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp> //包含dnn模块的头文件
#include <iostream>
#include <fstream> //文件流进行txt文件读取
using namespace cv;
using namespace cv::dnn; //包含dnn的命名空间
using namespace std;
vector<string> readLabels(); //将类名的txt文件读取到容器中
int main() {
string bin_model = "D:/OpenCV/project/opencv_tutorial-master/data/models/googlenet/bvlc_googlenet.caffemodel"; //定义模型权重文件的加载路径
string protxt = "D:/OpenCV/project/opencv_tutorial-master/data/models/googlenet/bvlc_googlenet.prototxt"; //定义模型描述文件的加载路径
//Imagenet支持1000个分类,分类类别在classification_classes_ILSVRC2012.txt可以看到,一直到1000
//有了模型的权重和描述文件就可以加载模型了
//在OpenCV中加载模型要通过readNetWork,
//readNet函数有三个参数,参数1加载网络(caffe、TensorFlow等),参数2、3,权重路径和描述文件,不同网络参数2、3的位置可能互换
//或者readNetFromCaffe(TensorFlow、darknet、ONNX等),意思为读进来的必须是caffe模型,此外还支持TensorFlow等模型,只有两个参数
//还有readNetFromModelOptimizer从dnn模型优化器中读取模型优化后的模型
Net net = readNetFromCaffe(protxt, bin_model);
//设置计算后台(OpenCVdnn模块支持设置不同的计算后台和在不同的设备上进行)
net.setPreferableBackend(DNN_BACKEND_OPENCV); //setPreferableBackend实际计算后台,default默认是DNN_BACKEND_OPENCV作为计算后台,使用此就行(也有加速后台ENGINE)
net.setPreferableTarget(DNN_TARGET_CPU); //设置在什么设备上进行计算(opencl(需要有interl图形卡)、FPGA、CPU)
//当上面两个设置之后,他在执行网络进行推算时就会执行此计算后台进行计算(不同的计算后台有不同的效果,速度也有差别)
//获取各层信息
vector<string> layer_names = net.getLayerNames(); //此时我们就可以获取所有层的名称了,有了这些可以将其ID取出
for (int i = 0; i < layer_names.size(); i++) {
int id = net.getLayerId(layer_names[i]); //通过name获取其id
auto layer = net.getLayer(id); //通过id获取layer
printf("layer id:%d,type:%s,name:%s\n", id, layer->type.c_str(), layer->name.c_str()); //将每一层的id,类型,姓名打印出来(可以明白此网络有哪些结构信息了)
}
Mat src = imread("G:/OpenCV/opencv笔记所用图片/plane.jpg"); //plane
if (src.empty()) {
cout << "could not load image.." << endl;
getchar();
return -1;
}
imshow("src", src);
有些通道类型是RGB通道的,而OpenCVread读取的是BGR通道的,要进行通道转换(实际在后面的blobFromImage就能进行通道转换,此处无需转换)
//Mat rgb;
//cvtColor(src, rgb, COLOR_BGR2RGB);
//构建输入
//使用blobFromImage函数对单张图像进行预处理,使其符合网络的输入
//对于网络来说原始输入层的时候都有指定的输入大小
int w = 224;
int h = 224;
Mat inputBlob = blobFromImage(src, 1.0, Size(w, h),Scalar(104, 117, 123), false,false); //我们要将图像resize成224*224的才是我们神经网络可以接受的宽高
//参数1:输入图像,参数2:默认1.0表示0-255范围的,参数3:设置输出的大小,参数4:均值对所有数据中心化预处理,参数5:是否进行通道转换(此处根据model.yml文件进行设置),参数6:剪切,参数7:默认深度为浮点型
//上方得到的inputBlob是4维的(在变量窗口看dim),所以在imagewatch中无法查看
//设置输入
//前面已经将图像预处理完成,就可以执行分类了
//现在要将其输入到创建的网络中
net.setInput(inputBlob);
//进行推断得到输出
//让网络执行得到output,调用forward可以得到一个结果
//此处不给参数,得到的是最后一层的结果,也可以输入层数得到任何一层的输出结果
Mat probMat = net.forward(); //通过前面的输出层看最后一层,可以知道输出100个分类,每个分类的得分是多少
//softmax层出来的总和为1,找到max值对应的index就知道对应的分类了,在classification_classes_ILSVRC2012.txt可以知道具体名称
//上方得到的probMat是1000*1*1(有imagewatch得到)
//此处通过forward()推测得到的probMat是2维的,所以在imagewatch中可以直观的查看(有些网络得到的是4维的无法查看)
//要解析输出(forward得到的)
//读取标签文件
vector<string> names = readLabels(); //使用自定义函数读取特定文件中的类名到names中,在后面输出对应名称时会用到
//对数据进行序列化(变成1行n列的,可以在后面进行方便的知道是哪个index了)
Mat prob = probMat.reshape(1, 1); //reshape函数可以进行序列化,(输出为1通道1行的数据,参数1:1个通道,参数2:1行)将输出结果变成1行n列的,但前面probMat本身就是1000*1*1
//实际结果probMat和prob相同
//当其他网络probMat需要序列化的时候,reshape就可以了
//此时找到最大的那个
Point classNum;
double classProb;
minMaxLoc(prob, NULL, &classProb, NULL, &classNum);//此时只获取最大值及最大值位置,最小值不管他
int index = classNum.x; //此时得到的是最大值的列坐标。就是其类的索引值,就可以知道其类名了
printf("\n current index=%d,possible:%2f,name=%s\n",index,classProb,names[index].c_str()); // current index=812,possible:0.999675,用editplus打开classification_classes_ILSVRC2012.txt,找到第813行(因为812+1)就是结果
//这样自己在classification_classes_ILSVRC2012.txt找答案挺麻烦的,我们使用程序打开此文件,得到类名即可
//此时可以将名称打印到图片上去
putText(src, names[index].c_str(), Point(50, 50), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(0, 0, 255), 2, 8);
imshow("result", src);
waitKey(0);
return 0;
}
vector<string> readLabels() {
string label_map_txt = "D:/OpenCV/project/opencv_tutorial-master/data/models/googlenet/classification_classes_ILSVRC2012.txt";
vector<string> classNames;
ifstream fp(label_map_txt);
if (!fp.is_open()) {
printf("could not find the file \n");
exit(-1);
}
std::string name;
while (!fp.eof()) { //当没有结束时就一直读取
getline(fp, name); //每次读取一行
if (name.length()) {
classNames.push_back(name);
}
}
fp.close(); //关闭文件输入流
return classNames;
}