OpenCV之FCN图像分割

  • 💂 个人主页:风间琉璃
  • 🤟 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主
  • 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)订阅专栏

前言

Fully Convolutional Network(FCN)是一种深度学习架构,主要用于图像分割任务。FCN 架构的典型应用包括语义分割、实例分割、物体检测等图像分析任务。它已经成为了计算机视觉领域图像分割任务的重要工具,并在许多竞赛和实际应用中取得了出色的成绩。

一、FCN简介

通常CNN网络在卷积层之后会接上若干个全连接层, 将卷积层产生的特征图(feature map)映射成一个固定长度的特征向量

以AlexNet为代表的经典CNN结构适合于图像级的分类和回归任务,因为它们最后都期望得到整个输入图像的一个数值描述(概率),比如AlexNet的ImageNet模型输出一个1000维的向量表示输入图像属于每一类的概率(softmax归一化)。

下图中的猫, 输入AlexNet, 得到一个长为1000的输出向量, 表示输入图像属于每一类的概率, 其中在“tabby cat”这一类统计概率最高。

FCN对图像进行像素级的分类,从而解决了语义级别的图像分割(semantic segmentation)问题。与经典的CNN在卷积层之后使用全连接层得到固定长度的特征向量进行分类(全联接层+softmax输出)不同,FCN可以接受任意尺寸的输入图像,采用反卷积层对最后一个卷积层的feature map进行上采样, 使它恢复到输入图像相同的尺寸,从而可以对每个像素都产生了一个预测, 同时保留了原始输入图像中的空间信息, 最后在上采样的特征图上进行逐像素分类。 

最后逐个像素计算softmax分类的损失, 相当于每一个像素对应一个训练样本。下图是Longjon用于语义分割所采用的全卷积网络(FCN)的结构示意图:

所以,FCN与CNN的区域在把于CNN最后的全连接层换成卷积层,输出的是一张已经Label好的图片。 

FCN原理: FCN将传统卷积网络后面的全连接层换成卷积层,这样网络输出不再是类别而是heatmap,同时为了解决因为卷积和池化对图像尺寸的影响,提出使用上采样的方式恢复。

核心思想:

  • - 不含全连接层(fc)的全卷积(fully conv)网络。可适应任意尺寸输入。 
  • - 增大数据尺寸的反卷积(deconv)层。能够输出精细的结果。 
  • - 结合不同深度层结果的跳级(skip)结构。同时确保鲁棒性和精确性。

网络结构详图如下,输入可为任意尺寸图像彩色图像,输出与输入尺寸相同,深度为:20类目标+背景=21。 

二、加载网络模型

这里使用Caffe深度学习框架中已经预训练好的FCN网络,需要相应的模型权重文件(.caffemodel)以及模型配置文件(.prototxt)。

加载模型和配置文件如下所示:

String model = "F:/data/CQU/VS/FCN_Segment/fcn8s-heavy-pascal.caffemodel";
String config = "F:/data/CQU/VS/FCN_Segment/fcn8s-heavy-pascal.prototxt";


//加载网络
Net net = readNetFromCaffe(config, model);

使用 CUDA进行加速添加下面两行代码:

//使用cuda加速
net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA_FP16);

三、预处理

将FCN网络对应的目标检测分类标签(20个类别)以及对应颜色调入内存这样便于访问它们,通常这些类别信息存储在txt文件中。 

String label = "F:/data/CQU/VS/FCN_Segment/pascal-classes.txt";

//读取分割对象颜色
vector<Vec3b> readColor(String labelpath)
{
    vector<Vec3b> colors;
    ifstream fp(labelpath);
    if (!fp.is_open())
    {
        printf("could not open the file...\n");
        exit(-1);
    }
	string line;
	while (!fp.eof()) 
	{
		//读取每一行
		getline(fp, line);  
		if (line.length()) 
		{
			stringstream ss(line);
			string name;
			ss >> name;   //分割对象
			int temp;
			Vec3b color;  //依次读取三个通道的颜色值大小
			ss >> temp;
			color[0] = (uchar)temp;
			ss >> temp;
			color[1] = (uchar)temp;
			ss >> temp;
			color[2] = (uchar)temp;
			colors.push_back(color);
		}
	}
	return colors;
}

//读取标签颜色
vector<Vec3b> colors = readColor(label);

 然后需要对输入图像进行预处理,主要要使输入图像尺寸满足网络输入的大小,网络输入的大小可以在配置文件prototxt中查看。

//图像预处理
resize(frame, frame, Size(500, 500));
Mat blobImage = blobFromImage(frame);

四、执行推理

图片预处理完成,就可以利用网络进行预测,这个过程也是把输入图像在网络各层中前向进行传播。

//构建输入
net.setInput(blobImage, "data");
//执行推理
Mat score = net.forward("score");

 data和score是网络模型输入层和输出层的名称,可以网络配置文件prototxt中查看。

五、解析输出

在score中存储着网络的所有输出,还需要对score进行后处理,然后将分割结果显示。

//分割并显示
const int chns = score.size[1];
const int rows = score.size[2];
const int cols = score.size[3];
//每个像素的最大类别(通道)
Mat maxCl(rows, cols, CV_8UC1);
//每个像素的最大值
Mat maxVal(rows, cols, CV_32FC1);

//LUT查找
for(int c = 0; c < chns; c++)
{
	for(int row = 0; row < rows; row++) 
	{
		//获取每个分割对象的置信度
		const float* ptrScore = score.ptr<float>(0, c, row);
		uchar* ptrMaxCl = maxCl.ptr<uchar>(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)c;
			}
		}
	}
}

//为每个像素分配一个对应的颜色,并将结果存储在result矩阵
Mat result = Mat::zeros(rows, cols, CV_8UC3);
for (int row = 0; row < rows; row++) 
{
	const uchar* ptrMaxCl = maxCl.ptr<uchar>(row);
	Vec3b* ptrColor = result.ptr<Vec3b>(row);
	for (int col = 0; col < cols; col++) 
	{
		ptrColor[col] = colors[ptrMaxCl[col]];
	}
}
Mat dst;
imshow("FCN", result);
//将两个图像按照一定的权重进行线性叠加,加入背景
//frame,result尺寸要相同
addWeighted(frame, 0.3, result, 0.7, 0, dst);
imshow("Src-FCN", dst);

运行结果:

源码:资源下载:OpenCVFCN图像分割资源-CSDN文库

// FCN_Segment.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/highgui/highgui_c.h>
#include <vector>
#include <fstream>

using namespace cv;
using namespace cv::dnn;
using namespace std;


const size_t width = 500;
const size_t height = 500;
String label = "F:/data/CQU/VS/FCN_Segment/pascal-classes.txt";
String model = "F:/data/CQU/VS/FCN_Segment/fcn8s-heavy-pascal.caffemodel";
String config = "F:/data/CQU/VS/FCN_Segment/fcn8s-heavy-pascal.prototxt";

//读取分割对象颜色
vector<Vec3b> readColor(String labelpath)
{
    vector<Vec3b> colors;
    ifstream fp(labelpath);
    if (!fp.is_open())
    {
        printf("could not open the file...\n");
        exit(-1);
    }
	string line;
	while (!fp.eof()) 
	{
		//读取每一行
		getline(fp, line);  
		if (line.length()) 
		{
			stringstream ss(line);
			string name;
			ss >> name;   //分割对象
			int temp;
			Vec3b color;  //依次读取三个通道的颜色值大小
			ss >> temp;
			color[0] = (uchar)temp;
			ss >> temp;
			color[1] = (uchar)temp;
			ss >> temp;
			color[2] = (uchar)temp;
			colors.push_back(color);
		}
	}
	return colors;
}


int main()
{
	Mat frame = imread("F:/data/CQU/VS/FCN_Segment/luo.jpg");
	if (frame.empty()) 
	{
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("src", CV_WINDOW_AUTOSIZE);
	imshow("src", frame);

	//图像预处理
	resize(frame, frame, Size(500, 500));
	Mat blobImage = blobFromImage(frame);

	//读取标签颜色
	vector<Vec3b> colors = readColor(label);

	//加载网络
	Net net = readNetFromCaffe(config, model);
	//使用cuda加速
	net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
	net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA_FP16);
	//构建输入
	net.setInput(blobImage, "data");
	//执行推理
	Mat score = net.forward("score");

	//分割并显示
	const int chns = score.size[1];
	const int rows = score.size[2];
	const int cols = score.size[3];
	//每个像素的最大类别(通道)
	Mat maxCl(rows, cols, CV_8UC1);
	//每个像素的最大值
	Mat maxVal(rows, cols, CV_32FC1);

	//LUT查找
	for(int c = 0; c < chns; c++)
	{
		for(int row = 0; row < rows; row++) 
		{
			//获取每个分割对象的置信度
			const float* ptrScore = score.ptr<float>(0, c, row);
			uchar* ptrMaxCl = maxCl.ptr<uchar>(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)c;
				}
			}
		}
	}

	//为每个像素分配一个对应的颜色,并将结果存储在result矩阵
	Mat result = Mat::zeros(rows, cols, CV_8UC3);
	for (int row = 0; row < rows; row++) 
	{
		const uchar* ptrMaxCl = maxCl.ptr<uchar>(row);
		Vec3b* ptrColor = result.ptr<Vec3b>(row);
		for (int col = 0; col < cols; col++) 
		{
			ptrColor[col] = colors[ptrMaxCl[col]];
		}
	}
	Mat dst;
	imshow("FCN", result);
	//将两个图像按照一定的权重进行线性叠加,加入背景
	//frame,result尺寸要相同
	addWeighted(frame, 0.3, result, 0.7, 0, dst);
	imshow("Src-FCN", dst);

	waitKey(0);

	return 0;
}

结束语
感谢你观看我的文章呐~本次航班到这里就结束啦 🛬

希望本篇文章有对你带来帮助 🎉,有学习到一点知识~

躲起来的星星🍥也在努力发光,你也要努力加油(让我们一起努力叭)。

最后,博主要一下你们的三连呀(点赞、评论、收藏),不要钱的还是可以搞一搞的嘛~

不知道评论啥的,即使扣个666也是对博主的鼓舞吖 💞 感谢 💐

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Super.Bear

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值