1.预备知识
本章还是不讨论 SSD 是什么,只要能够理解 SSD 的目的是目标检测。
前提基础知识:解析mobile_ssd样例代码
上一章中具体的讲解了 "mobilenet_SSD"(下称MSSD)运行过程,为示例代码添上了注释。但是也很明显只是对一张图片进行检测。为了更近一步,我把 这个样例代码略做修剪,对摄像头的视频流图片进行检测。
但是如何使用视频流呢?
这里给出一段简单的 OpenCV 程序,该程序是 OpenCV 的样例程序,位于 "opencv-3.4.2/samples/cpp/example_cmake",同样使用 CLion 打开。
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/videoio.hpp"
#include
// 使用 opencv 的命名空间
using namespace cv;
using namespace std;
void drawText(Mat & image);
int main()
{
cout << "Built with OpenCV " << CV_VERSION << endl;
Mat image;
// 设置使用摄像头(0)
VideoCapture capture;
capture.open(0);
// 如果摄像头存在
if(capture.isOpened())
{
cout << "Capture is opened" << endl;
for(;;)
{
capture >> image;
if(image.empty())
break;
drawText(image);
// img 图像大小 480,640
imshow("Sample", image);
// 注意 imshow 后面必须跟一个 waitKey(),否则无法显示图片
if(waitKey(10) >= 0)
break;
}
}
// 如果摄像头不存在(可以省略)
else
{
cout << "No capture" << endl;
image = Mat::zeros(480, 640, CV_8UC1);
drawText(image);
imshow("Sample", image);
waitKey(0);
}
return 0;
}
void drawText(Mat & image)
{
putText(image, "Hello OpenCV",
Point(20, 50),
FONT_HERSHEY_COMPLEX, 1, // font face and scale
Scalar(255, 255, 255), // white
1, LINE_AA); // line thickness and type
}
该程序是一个简单的视频流程序,所以一个简单的想法,将该程序与 MSSD 进行结合。由 视频流 提供 图片(image),由 MSSD 对图片进行检测。所以我们需要
TODO:1.知道在 MSSD中那部分是实际的对图像进行处理的地方。
TODO:2.知道如何在 MSSD 中使用视频流。
TODO:3.将 MSSD 的图片地址,改为视频流的图片(CV::Mat)。
2.执行任务
1.知道在 MSSD中那部分是实际的对图像进行处理的地方。
比较容易找到的线索是,样例中 MSSD 使用的是一个图片路径,以图片路径为线索进行查找。
TODO: 1. 调用 init_tengine_library 函数初始化, 只能被调用一次
TODO: 2. 调用 load_model 载入训练好的模型
TODO: 3. 调用 create_runtime_graph 函数创建图(类似 pytorch 的 net)
TODO: 3.1 需要调用 check_graph_valid 来确定返回的句柄
TODO: 4.1 调用 get_graph_input_tensor 获取输入Tensor
TODO: 4.2 并用 check_tensor_valid 确认返回值是否合法
TODO: 4.3 并用 set_tensor_shape 设置输入Tensor的shape
TODO: 5. 调用 prerun_graph 函数预启动图(类似 malloc,申请资源)
TODO: 7.1.向 input_data 写入输入的数据,
TODO: 7.2.并调用 set_tensor_buffer 把数据转移到输入Tensor上
TODO: 8. 调用 run_graph 运行图(做一次前向传播)
TODO: 9.1.调用 get_graph_output_tensor 获取输出Tensor
TODO: 9.2.并用 get_tensor_shape 取得 tensor 的shape
TODO: 9.3.并用 get_tensor_buffer 取得缓冲区上的数据
TODO: 10. 最后在退出程序前依次释放各个申请的动态空间
TODO: 9.4 释放 out_tensor 所占空间
TODO: 4.4 释放 input_tensor 所占空间
TODO: 3.2 调用 destroy_runtime_graph() 来释放资源
TODO: 2.1 请调用 remove_model() to 释放导入的 model
参考具体代码发现
开始使用
TODO: 7.1.向 input_data 写入输入的数据,
get_input_data_ssd(image_file, input_data, img_h, img_w);
......(省略)
结束使用
TODO: 9.3.并用 get_tensor_buffer 取得缓冲区上的数据
中使用到
// 通过 outdata 对 image 进行绘制边框和label信息,并保存
post_process_ssd(image_file,show_threshold, outdata, num,save_name);
所以摄像头图片检索的for(;;)要加在这两行之间。
2.知道如何在 MSSD 中使用视频流。
在MSSD中使用摄像头,我们也要为OpenCV的样例代码做一些微调。比如
1.去掉前面的提示信息
2.去掉摄像头不存在的情况
3.去掉 "void drawText(Mat & image)" 部分
int main()
{
.....(MSSD 代码)
Mat image;
// 设置使用摄像头(0)
VideoCapture capture;
capture.open(0);
// 如果摄像头存在
if(capture.isOpened())
{
for(;;)
{
capture >> image;
if(image.empty())
break;
......(将图片交给 MSSD 处理)
// img 图像大小 480,640
imshow("Sample", image);、
if(waitKey(10) >= 0)
break;
}
}
......(MSSD 代码)
return 0;
}
3.将 MSSD 的图片地址,改为视频流的图片(CV::Mat)。
实际上使用到 image_file 只有两处,并且都是在函数中。
以下两个函数
void get_input_data_ssd(std::string& image_file, float* input_data, int img_h, int img_w)
void post_process_ssd(std::string& image_file,float threshold,float* outdata,int num,std::string& save_name)
所以我们要将函数稍作修改,对 "void get_input_data_ssd" 的修改。(实际只修改了两个地方)
//void get_input_data_ssd(std::string& image_file, float* input_data, int img_h, int img_w)(修改第一处)
void get_input_data_ssd(cv::Mat image, float* input_data, int img_h, int img_w)
{
// 读取数据, img 为图形的内容
//cv::Mat img = cv::imread(image_file);(修改第二处)
cv::Mat img = image.clone();
// 如果 img 为空
if (img.empty())
{
std::cerr << "Failed to read image file " << image << ".\n";
return;
}
// 将 img 进行 reshape
cv::resize(img, img, cv::Size(img_h, img_w));
// 转化数据类型 CV_32FC3:
img.convertTo(img, CV_32FC3);
float *img_data = (float *)img.data;
int hw = img_h * img_w;
/*类似数据归一化
* 127.5 = 255/2
* 0.007843 =1/127.5
*/
float mean[3]={127.5,127.5,127.5};
for (int h = 0; h < img_h; h++)
{
for (int w = 0; w < img_w; w++)
{
for (int c = 0; c < 3; c++)
{
input_data[c * hw + h * img_w + w] = 0.007843* (*img_data - mean[c]);
img_data++;
}
}
}
}
对 "post_process_ssd" 的修改
//(修改第一处)
//void post_process_ssd(std::string& image_file,float threshold,float* outdata,int num,std::string& save_name)
cv::Mat post_process_ssd(cv::Mat image,float threshold,float* outdata,int num,std::string& save_name)
{
//检测目标的类别
const char* class_names[] = {
"background", //背景
"aeroplane", //飞机
"bicycle", //自行车
"bird", //鸟
"boat", //船
"bottle", //瓶子
"bus", //公交车
"car", //私家车
"cat", //猫
"chair", //椅子
"cow", //奶牛
"diningtable", //餐桌
"dog", //狗
"horse", //马
"motorbike", //摩托车
"person", //人
"pottedplant", //盆栽
"sheep", //羊
"sofa", //沙发
"train", //火车
"tvmonitor"}; //电视机
// 图片路径转为矩阵
//(修改第二处)
//cv::Mat img = cv::imread(image_file);
cv::Mat img = image.clone();
// 图片的信息(height,width)
int raw_h = img.size().height;
int raw_w = img.size().width;
// boxes 为检测输出的信息
std::vector boxes;
// 设置使用的线宽(line_width)
int line_width=raw_w*0.005;
// logout: 检测到目标个数
printf("detect result num: %d \n",num);
for (int i=0;i=threshold)
{
//Box 为 输出内容的信息
Box box;
box.class_idx=outdata[0];
box.score=outdata[1];
box.x0=outdata[2]*raw_w;
box.y0=outdata[3]*raw_h;
box.x1=outdata[4]*raw_w;
box.y1=outdata[5]*raw_h;
// boxes 作为 栈(vector)
boxes.push_back(box);
// logout:输出output信息
printf("%s\t:%.0f%%\n", class_names[box.class_idx], box.score * 100);
printf("BOX:( %g , %g ),( %g , %g )\n",box.x0,box.y0,box.x1,box.y1);
}
outdata+=6;
}
/* output返回的信息并不是都有用
* 经过阈值过滤之后 数量 可能会 降低
*/
// 对过滤后的 正确目标点 进行绘制 矩阵框
for(int i=0;i<(int)boxes.size();i++)
{
Box box=boxes[i];
/*!
* img: 使用的img(Mat 格式)
* Rect rec:矩形结构
* 左上角点(x,y),w宽,h高
* const Scalar& color: 使用的颜色(scalar:BGR)
* thickness: 线宽(默认:1)
* lineType: 线形(默认:LINE_8,实线)
* shift: 坐标的小数点位数(默认为0,整形)
*/
cv::rectangle(img, cv::Rect(box.x0, box.y0,(box.x1-box.x0),(box.y1-box.y0)),
cv::Scalar(255, 255, 0),
line_width);
std::ostringstream score_str;
score_str<
对于函数的修改已经完成了,接下来对主函数加入 for循环
struct timeval t0, t1;
float mytime=0;
// 使用opencv
cv::VideoCapture capture;
capture.open(0);
if(capture.isOpened())
{
cv::Mat image;
std::cout<<"Capture is opened" << std::endl;
for(;;)
{
capture >> image;
if(image.empty())
break;
//TODO: 7.1 向 input_data 写入输入的数据,
get_input_data_ssd(image, input_data, img_h, img_w);
// 获取当前值,并赋值给 t0(开始值)
gettimeofday(&t0, NULL);
//TODO: 7.2 并调用 set_tensor_buffer 把数据转移到输入Tensor上
set_tensor_buffer(input_tensor, input_data, img_size * 4);
//TODO: 8. 调用 run_graph 运行图(做一次前向传播)
run_graph(graph, 1);
//获取当前时间,并赋值给 t1(结束时间)
gettimeofday(&t1, NULL);
mytime = (float)((t1.tv_sec * 1000000 + t1.tv_usec) - (t0.tv_sec * 1000000 + t0.tv_usec)) / 1000;
std::cout << "--------------------------------------\n";
std::cout << " times, avg time per run is " << mytime << " ms\n";
//TODO: 9.1.调用 get_graph_output_tensor 获取输出Tensor
tensor_t out_tensor = get_graph_output_tensor(graph, 0,0);//"detection_out");
//TODO: 9.2.并用 get_tensor_shape 取得 tensor 的shape
/*
* [0]:批次:1张图
* [1]:检测到目标个数:3个目标
* [2]:outdata 的 Box 6 个信息:
* 0. 属于的类别(下标)
* 1. 属于该类别的score
* 2. 左上角点(x)相对于宽的百分比
* 3. 左上角点(y)相对于高的百分比
* 4. 右上角点(x)相对于宽的百分比
* 5. 右上角点(y)相对于高的百分比
* [3]:1 一行
*/
int out_dim[4];
get_tensor_shape( out_tensor, out_dim, 4);
//std::cout<<"out_dim" << *out_dim <= 0)
break;
//TODO: 9.4 释放 out_tensor 所占空间
put_graph_tensor(out_tensor);
}
}
mssd.cpp 完整代码见文末。
3.进行编译
成功检测到杯子和显示屏。
任务完成。
附 MSSD.cpp完整代码
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* License); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* Copyright (c) 2018, Open AI Lab
* Author: [email protected]
*/
#include
#include
#include
#include
#include
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "tengine_c_api.h"
#include
#include "common.hpp"
//宏定义 prototxt 文件
#define DEF_PROTO "models/MobileNetSSD_deploy.prototxt"
//宏定义 caffemodel 文件
#define DEF_MODEL "models/MobileNetSSD_deploy.caffemodel"
//宏定义 默认图片文件
#define DEF_IMAGE "tests/images/myimages.jpg"
//预测的内容(6个)
struct Box
{
// 左上角点
float x0;
float y0;
// 右下角点
float x1;
float y1;
// 预测的类别
int class_idx;
// 该类别的 正确率
float score;
};
/*!
* @brief 将 图片 的路径名转化为 图片内容的矩阵,并赋值给 input_data
* @param image_file: 输入的图片路径
* @param input_data: 输出的图片的矩阵
* @param img_h: 输出图形的height
* @param img_w: 输出图形的weight
*/
//void get_input_data_ssd(std::string& image_file, float* input_data, int img_h, int img_w)(修改第一处)
void get_input_data_ssd(cv::Mat image, float* input_data, int img_h, int img_w)
{
// 读取数据, img 为图形的内容
//cv::Mat img = cv::imread(image_file);(修改第二处)
cv::Mat img = image.clone();
// 如果 img 为空
if (img.empty())
{
std::cerr << "Failed to read image file " << image << ".\n";
return;
}
// 将 img 进行 reshape
cv::resize(img, img, cv::Size(img_h, img_w));
// 转化数据类型 CV_32FC3:
img.convertTo(img, CV_32FC3);
float *img_data = (float *)img.data;
int hw = img_h * img_w;
/*类似数据归一化
* 127.5 = 255/2
* 0.007843 =1/127.5
*/
float mean[3]={127.5,127.5,127.5};
for (int h = 0; h < img_h; h++)
{
for (int w = 0; w < img_w; w++)
{
for (int c = 0; c < 3; c++)
{
input_data[c * hw + h * img_w + w] = 0.007843* (*img_data - mean[c]);
img_data++;
}
}
}
}
/*!
* @brief
*
* @param image_file: 图片的路径
* @param threshold:是否为检测目标的阈值
* @param outdata:输出数据
* @param num:检测到目标的个数
* @param save_name:
*
*/
//(修改第一处)
//void post_process_ssd(std::string& image_file,float threshold,float* outdata,int num,std::string& save_name)
cv::Mat post_process_ssd(cv::Mat image,float threshold,float* outdata,int num,std::string& save_name)
{
//检测目标的类别
const char* class_names[] = {
"background", //背景
"aeroplane", //飞机
"bicycle", //自行车
"bird", //鸟
"boat", //船
"bottle", //瓶子
"bus", //公交车
"car", //私家车
"cat", //猫
"chair", //椅子
"cow", //奶牛
"diningtable", //餐桌
"dog", //狗
"horse", //马
"motorbike", //摩托车
"person", //人
"pottedplant", //盆栽
"sheep", //羊
"sofa", //沙发
"train", //火车
"tvmonitor"}; //电视机
// 图片路径转为矩阵
//(修改第二处)
//cv::Mat img = cv::imread(image_file);
cv::Mat img = image.clone();
// 图片的信息(height,width)
int raw_h = img.size().height;
int raw_w = img.size().width;
// boxes 为检测输出的信息
std::vector boxes;
// 设置使用的线宽(line_width)
int line_width=raw_w*0.005;
// logout: 检测到目标个数
printf("detect result num: %d \n",num);
for (int i=0;i=threshold)
{
//Box 为 输出内容的信息
Box box;
box.class_idx=outdata[0];
box.score=outdata[1];
box.x0=outdata[2]*raw_w;
box.y0=outdata[3]*raw_h;
box.x1=outdata[4]*raw_w;
box.y1=outdata[5]*raw_h;
// boxes 作为 栈(vector)
boxes.push_back(box);
// logout:输出output信息
printf("%s\t:%.0f%%\n", class_names[box.class_idx], box.score * 100);
printf("BOX:( %g , %g ),( %g , %g )\n",box.x0,box.y0,box.x1,box.y1);
}
outdata+=6;
}
/* output返回的信息并不是都有用
* 经过阈值过滤之后 数量 可能会 降低
*/
// 对过滤后的 正确目标点 进行绘制 矩阵框
for(int i=0;i<(int)boxes.size();i++)
{
Box box=boxes[i];
/*!
* img: 使用的img(Mat 格式)
* Rect rec:矩形结构
* 左上角点(x,y),w宽,h高
* const Scalar& color: 使用的颜色(scalar:BGR)
* thickness: 线宽(默认:1)
* lineType: 线形(默认:LINE_8,实线)
* shift: 坐标的小数点位数(默认为0,整形)
*/
cv::rectangle(img, cv::Rect(box.x0, box.y0,(box.x1-box.x0),(box.y1-box.y0)),
cv::Scalar(255, 255, 0),
line_width);
std::ostringstream score_str;
score_str<> image;
if(image.empty())
break;
//TODO: 7.1 向 input_data 写入输入的数据,
get_input_data_ssd(image, input_data, img_h, img_w);
// 获取当前值,并赋值给 t0(开始值)
gettimeofday(&t0, NULL);
//TODO: 7.2 并调用 set_tensor_buffer 把数据转移到输入Tensor上
set_tensor_buffer(input_tensor, input_data, img_size * 4);
//TODO: 8. 调用 run_graph 运行图(做一次前向传播)
run_graph(graph, 1);
//获取当前时间,并赋值给 t1(结束时间)
gettimeofday(&t1, NULL);
mytime = (float)((t1.tv_sec * 1000000 + t1.tv_usec) - (t0.tv_sec * 1000000 + t0.tv_usec)) / 1000;
std::cout << "--------------------------------------\n";
std::cout << " times, avg time per run is " << mytime << " ms\n";
//TODO: 9.1.调用 get_graph_output_tensor 获取输出Tensor
tensor_t out_tensor = get_graph_output_tensor(graph, 0,0);//"detection_out");
//TODO: 9.2.并用 get_tensor_shape 取得 tensor 的shape
/*
* [0]:批次:1张图
* [1]:检测到目标个数:3个目标
* [2]:outdata 的 Box 6 个信息:
* 0. 属于的类别(下标)
* 1. 属于该类别的score
* 2. 左上角点(x)相对于宽的百分比
* 3. 左上角点(y)相对于高的百分比
* 4. 右上角点(x)相对于宽的百分比
* 5. 右上角点(y)相对于高的百分比
* [3]:1 一行
*/
int out_dim[4];
get_tensor_shape( out_tensor, out_dim, 4);
//std::cout<<"out_dim" << *out_dim <= 0)
break;
//TODO: 9.4 释放 out_tensor 所占空间
put_graph_tensor(out_tensor);
}
}
//TODO: 10. 最后在退出程序前依次释放各个申请的动态空间
// //TODO: 9.4 释放 out_tensor 所占空间
// //put_graph_tensor(out_tensor);
//TODO: 4.4 释放 input_tensor 所占空间
put_graph_tensor(input_tensor);
free(input_data);
//TODO: 3.2 调用 destroy_runtime_graph() 来释放资源
// 释放 graph 执行所占用的资源
postrun_graph(graph);
// 释放 graph 所占空间
destroy_runtime_graph(graph);
//TODO: 2.1 请调用 remove_model() to 释放导入的 model
remove_model(model_name);
return 0;
}