模型下载地址:
http://dl.caffe.berkeleyvision.org/
配置文件下载:
https://github.com/opencv/opencv_extra/tree/4.x/testdata/dnn
该段代码是一个利用深度学习进行语义分割的OpenCV应用实例。下面将详细解释代码的功能和方法。
引入库
引入了一些必要的C++和OpenCV库,其中包括文件流、字符串处理、深度学习网络(dnn模块)、图像处理和高层图形界面。
参数字符串
定义了一些命令行参数,用于获取模型信息、图像处理以及设备选择等配置。
自定义函数
定义了showLegend和colorizeSegmentation两个函数,用于显示分类的图例及为分割结果上色。
主函数main
主函数执行以下主要步骤:
解析命令行参数:使用OpenCV的CommandLineParser来获取命令行输入的参数。
读取模型和配置:根据参数从文件中读取深度学习模型及其配置。
读取类别和颜色信息:如果提供了类名和颜色的文件路径,则从文件中读取这些信息。
设置网络:将模型和配置设置到网络中,并且选择计算后端和目标设备。
处理视频帧或图片:循环从视频流或文件中读取帧,并对其进行处理:
将帧转换为模型输入所需的blob。
将blob作为网络的输入。
进行网络前向传播,获取分割的score。
使用colorizeSegmentation函数将分割的score转换成彩色分割图。
结合原始帧和彩色分割图,显示在用户界面上。
如果有类别信息,显示图例窗口。
图像分割和上色
colorizeSegmentation函数计算每个像素点的最大得分类别,并将对应颜色填充到分割图中。
showLegend函数在独立窗口中显示每个类别及其对应颜色的图例。
运行流程
用户通过命令行运行程序,可以选择加载本地视频文件、图像文件或打开摄像头。程序会连续读取帧,并将每一帧通过神经网络进行分析,实现实时的图像分割功能。分割结果彩色化后与原图结合,展现给用户。
总的来说,这段代码实现了通过深度学习模型对图像进行实时语义分割,并通过OpenCV的GUI功能将结果呈现给用户。它可以很好地适用于视频流分析,如自动驾驶车辆的视觉系统中实时理解道路情况。
#include <fstream> // 包含fstream库,用于文件的读写操作
#include <sstream> // 包含sstream库,用于字符串流的操作
#include <opencv2/dnn.hpp> // 包含OpenCV深度神经网络(dnn)部分的头文件
#include <opencv2/imgproc.hpp> // 包含OpenCV图像处理部分的头文件
#include <opencv2/highgui.hpp> // 包含OpenCV用户界面部分的头文件
#include "common.hpp" // 包含示例代码中定义的通用函数和变量
// 声明并初始化一个存储命令行参数的字符串
std::string keys =
"{ help h | | Print help message. }" // 帮助信息
"{ @alias |fcn8s | An alias name of model to extract preprocessing parameters from models.yml file. }" // 模型别名
"{ zoo | models.yml | An optional path to file with preprocessing parameters }" // models.yml文件路径
"{ device | 0 | camera device number. }" // 摄像头设备号
"{ width | 500 | }"
"{ height | 500 | }"
"{ input i |test1.mp4 | Path to input image or video file. Skip this argument to capture frames from a camera. }" // 输入图片或视频文件的路径
"{ framework f | | Optional name of an origin framework of the model. Detect it automatically if it does not set. }" // 模型框架,默认自动检测
"{ classes | pascal-classes.txt| Optional path to a text file with names of classes. }" // 类名文件路径
"{ colors | | Optional path to a text file with colors for an every class. "
"An every color is represented with three values from 0 to 255 in BGR channels order. }" // 类颜色文件路径
"{ backend | 5 | Choose one of computation backends: "
"0: automatically (by default), "
"1: Halide language (http://halide-lang.org/), "
"2: Intel's Deep Learning Inference Engine (https://software.intel.com/openvino-toolkit), "
"3: OpenCV implementation, "
"4: VKCOM, "
"5: CUDA }" // 计算后端,默认自动选择
"{ target | 6 | Choose one of target computation devices: "
"0: CPU target (by default), "
"1: OpenCL, "
"2: OpenCL fp16 (half-float precision), "
"3: VPU, "
"4: Vulkan, "
"6: CUDA, "
"7: CUDA fp16 (half-float preprocess) }"; // 计算设备,默认CPU
using namespace cv; // 使用cv命名空间
using namespace dnn; // 使用dnn命名空间
std::vector<std::string> classes; // 存储类名的向量
std::vector<Vec3b> colors; // 存储每个类对应颜色的向量
void showLegend(); // 前向声明showLegend函数,用于显示图例
void colorizeSegmentation(const Mat &score, Mat &segm); // 前向声明colorizeSegmentation函数,用于给分割结果上色
// 主函数
int main(int argc, char** argv)
{
CommandLineParser parser(argc, argv, keys); // 命令行参数解析器
const std::string modelName = parser.get<String>("@alias"); // 获取模型别名参数
const std::string zooFile = parser.get<String>("zoo"); // 获取zoo文件参数
keys += genPreprocArguments(modelName, zooFile); // 为命令行参数解析器添加预处理参数
parser = CommandLineParser(argc, argv, keys); // 使用更新后的参数集重新构建命令行参数解析器
// 打印脚本使用帮助
parser.about("Use this script to run semantic segmentation deep learning networks using OpenCV.");
if (argc == 1 || parser.has("help"))
{
parser.printMessage(); // 打印帮助信息
return 0; // 退出程序
}
float scale = parser.get<float>("scale"); // 获取缩放比例参数
Scalar mean = parser.get<Scalar>("mean"); // 获取均值参数
bool swapRB = parser.get<bool>("rgb"); // 获取是否交换红蓝通道的参数
int inpWidth = parser.get<int>("width"); // 获取输入宽度参数
int inpHeight = parser.get<int>("height"); // 获取输入高度参数
String model = findFile(parser.get<String>("model")); // 查找模型文件
String config = findFile(parser.get<String>("config")); // 查找配置文件
String framework = parser.get<String>("framework"); // 获取框架参数
int backendId = parser.get<int>("backend"); // 获取后端ID参数
int targetId = parser.get<int>("target"); // 获取目标设备ID参数
// 打开类名文件
if (parser.has("classes"))
{
std::string file = parser.get<String>("classes");
std::ifstream ifs(file.c_str());
if (!ifs.is_open())
CV_Error(Error::StsError, "File " + file + " not found"); // 文件未能打开,则报错
std::string line;
while (std::getline(ifs, line))
{
classes.push_back(line); // 将类名逐行读入classes向量
}
}
// 打开颜色文件
if (parser.has("colors"))
{
std::string file = parser.get<String>("colors");
std::ifstream ifs(file.c_str());
if (!ifs.is_open())
CV_Error(Error::StsError, "File " + file + " not found"); // 文件未能打开,则报错
std::string line;
while (std::getline(ifs, line))
{
std::istringstream colorStr(line.c_str()); // 使用字符串流读取颜色信息
Vec3b color;
for (int i = 0; i < 3 && !colorStr.eof(); ++i)
colorStr >> color[i];
colors.push_back(color); // 将颜色逐行读入colors向量
}
}
if (!parser.check())
{
parser.printErrors(); // 打印参数解析错误
return 1; // 退出程序
}
CV_Assert(!model.empty()); //! [Read and initialize network] 确保模型路径不为空,并初始化网络
Net net = readNet(model, config, framework); // 读取网络模型
net.setPreferableBackend(backendId); // 设置计算后端
net.setPreferableTarget(targetId); // 设置目标计算设备
//! [Read and initialize network]
// 创建一个窗口
static const std::string kWinName = "Deep learning semantic segmentation in OpenCV";
namedWindow(kWinName, WINDOW_NORMAL);
//! [Open a video file or an image file or a camera stream]
VideoCapture cap; // 视频捕获对象
if (parser.has("input"))
cap.open(parser.get<String>("input")); // 打开输入的图片或视频文件
else
cap.open(parser.get<int>("device")); // 打开摄像头
//! [Open a video file or an image file or a camera stream]
// 处理帧数据
Mat frame, blob; // 定义用来存放帧和blob的矩阵
cap >> frame;
VideoWriter video("fcn8s-heavy-pascal_video.avi", VideoWriter::fourcc('M', 'J', 'P', 'G'), 10, frame.size(), true);
if (!video.isOpened())
{
std::cout << "Could not open the output video file for write\n";
return -1;
}
while (waitKey(1) < 0) // 等待按键事件
{
cap >> frame; // 从视频捕获对象读取一帧
if (frame.empty())
{
waitKey(); // 若帧为空,等待按键后退出循环
break;
}
//! [Create a 4D blob from a frame]
blobFromImage(frame, blob, scale, Size(inpWidth, inpHeight), mean, swapRB, false); // 从帧中创建一个4维blob
//! [Create a 4D blob from a frame]
//! [Set input blob]
net.setInput(blob); // 设置网络输入
//! [Set input blob]
//! [Make forward pass]
Mat score = net.forward(); // 执行前向传播
//! [Make forward pass]
Mat segm; // 用于存储分割结果的矩阵
colorizeSegmentation(score, segm); // 给分割结果上色
resize(segm, segm, frame.size(), 0, 0, INTER_NEAREST); // 调整分割结果的大小以匹配原始帧大小
addWeighted(frame, 0.1, segm, 0.9, 0.0, frame); // 将帧和分割结果合并显示
// 显示效率信息
std::vector<double> layersTimes; // 存储每层时间的向量
double freq = getTickFrequency() / 1000; // 获取时钟频率
double t = net.getPerfProfile(layersTimes) / freq; // 计算网络执行时间
std::string label = format("Inference time: %.2f ms", t); // 格式化时间信息
putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0)); // 在帧上绘制时间信息
imshow(kWinName, frame); // 显示窗口
video.write(frame);
if (!classes.empty())
showLegend(); // 显示图例
}
return 0; // 程序正常退出
}
// 给分割结果上色的函数
void colorizeSegmentation(const Mat &score, Mat &segm)
{
const int rows = score.size[2]; // 获取score的行数
const int cols = score.size[3]; // 获取score的列数
const int chns = score.size[1]; // 获取score的通道数
if (colors.empty())
{
// 产生颜色
colors.push_back(Vec3b()); // 添加黑色
for (int i = 1; i < chns; ++i)
{
Vec3b color; // 定义颜色
for (int j = 0; j < 3; ++j)
color[j] = (colors[i - 1][j] + rand() % 256) / 2; // 随机生成颜色
colors.push_back(color); // 添加颜色到colors向量
}
}
else if (chns != (int)colors.size())
{
CV_Error(Error::StsError, format("Number of output classes does not match "
"number of colors (%d != %zu)", chns, colors.size())); // 检测颜色数是否与通道数匹配
}
Mat maxCl = Mat::zeros(rows, cols, CV_8UC1); // 创建类别的索引矩阵
Mat maxVal(rows, cols, CV_32FC1, score.data); // 创建分数矩阵
// 遍历通道,通道数从1开始,因为通道0为背景
for (int ch = 1; ch < chns; ch++)
{
// 遍历得分图的每一行
for (int row = 0; row < rows; row++)
{
// 获取当前行的得分数据指针
const float *ptrScore = score.ptr<float>(0, ch, row);
// 获取最大类别的索引的行指针
uint8_t *ptrMaxCl = maxCl.ptr<uint8_t>(row);
// 获取最大值的行指针
float *ptrMaxVal = maxVal.ptr<float>(row);
// 遍历当前行的每一列
for (int col = 0; col < cols; col++)
{
// 如果当前位置的得分大于之前的最大值,则更新最大值和最大类别索引
if (ptrScore[col] > ptrMaxVal[col])
{
ptrMaxVal[col] = ptrScore[col];
ptrMaxCl[col] = (uchar)ch;
}
}
}
}
// 根据最大类别索引创建分割图
segm.create(rows, cols, CV_8UC3);
for (int row = 0; row < rows; row++)
{
// 获取最大类别索引的指针
const uchar *ptrMaxCl = maxCl.ptr<uchar>(row);
// 获取分割图的行指针
Vec3b *ptrSegm = segm.ptr<Vec3b>(row);
for (int col = 0; col < cols; col++)
{
// 根据类别索引设置分割图的颜色
ptrSegm[col] = colors[ptrMaxCl[col]];
}
}
}
// 显示类别的图例
void showLegend()
{
// 定义图例块的高度
static const int kBlockHeight = 30;
// 定义图例Mat对象
static Mat legend;
// 如果图例为空,则创建一个新的图例
if (legend.empty())
{
// 获取类别的数量
const int numClasses = (int)classes.size();
// 如果颜色数量和类别数量不匹配,则报错
if ((int)colors.size() != numClasses)
{
CV_Error(Error::StsError, format("Number of output classes does not match "
"number of labels (%zu != %zu)", colors.size(), classes.size()));
}
// 创建图例Mat对象
legend.create(kBlockHeight * numClasses, 200, CV_8UC3);
for (int i = 0; i < numClasses; i++)
{
// 获取每个类别的图例块
Mat block = legend.rowRange(i * kBlockHeight, (i + 1) * kBlockHeight);
// 设置图例块的颜色
block.setTo(colors[i]);
// 在图例块上写上类别的名称
putText(block, classes[i], Point(0, kBlockHeight / 2), FONT_HERSHEY_SIMPLEX, 0.5, Vec3b(255, 255, 255));
}
// 创建一个窗口显示图例
namedWindow("Legend", WINDOW_NORMAL);
imshow("Legend", legend);
}
}
这段代码是一个基于OpenCV实现的语义分割深度学习网络的应用。其中包含处理图像数据转化为Blob,通过神经网络前向传播输出分割得分图,再对得分图进行处理,提取出每个像素点的最大得分对应的类别,并根据这个最大类别类别的索引来进行图像分割的颜色填充。此外,还有一个showLegend函数用于生成并展示一个包含所有类别及其对应颜色的图例。整体而言,这段代码是实现图像语义分割功能的一个部分。
函数colorizeSegmentation负责对图像分类得分进行上色,从而生成彩色的分割图。函数首先计算输入得分score的行数、列数以及通道数。然后,它检查是否已经定义了颜色映射,如果没有定义,则生成一组颜色映射。接下来,函数遍历每个像素位置,找到具有最大得分的通道,并记录这个通道索引到maxCl中。最后,根据通道索引,在最终的分割图像segm上应用对应的颜色。这样做的结果是得到一个彩色标记了各个类别区域的图像,便于视觉分析和理解。
笔记:
blobFromImage(frame, blob, scale, Size(inpWidth, inpHeight), mean, swapRB, false);
Mat score = net.forward();
The End
作者陈晓永:智能装备专业高级职称,软件工程师,机械设计中级职称,机器人与自动化产线仿真动画制作