1、图像分割是什么
图像分割分类是对图像中属于特定类别的像素进行分类的过程,因此图像分割可以认为是按像素进行分类的问题。
传统的图像分割算法均是基于灰度值的不连续和相似的性质。而基于深度学习的图像分割技术则是利用卷积神经网络,来理解图像中的每个像素所代表的真实世界物体,这在以前是难以想象的。
2、图像分割的类型有哪些?
基于深度学习的图像分割技术主要分为两类:语义分割和实例分割
语义分割会为图像中的每个像素分配一个类别,但是同一类别之间的对象不会区分。
实例分割是将图像中的每个物体实例标记出来,并为每个实例生成一个像素级的分割掩码。
实例分割看起来与目标检测相似,不同的是目标检测输出目标的边界框和类别,实例分割输出的是目标的Mask和类别。
看一下下面的图就明白了。
3、分割网络是怎么搭建的?
图像分割网络的构建通常涉及以下几个步骤:
1. 选择网络架构:首先要选择适合您任务的网络架构。常用的图像分割网络包括 U-Net、FCN、DeepLab 等。这些网络具有不同的结构和特点,适用于不同的应用场景。
2. 构建编码器和解码器部分:图像分割网络通常包括编码器和解码器两部分。编码器负责从输入图像中提取特征,通常通过卷积神经网络(CNN)来实现。解码器负责将编码器提取的特征映射回原始图像尺寸的预测结果。编码器和解码器之间通常会有一些连接,用于跳跃连接或信息传递,例如 U-Net 中的跳跃连接结构。
3. 选择损失函数:图像分割任务通常会使用像素级别的标签进行训练,因此常用的损失函数包括交叉熵损失、Dice 损失等。这些损失函数可以帮助网络学习正确的像素分类。
4. 选择优化器:选择合适的优化器来优化网络参数,例如 SGD、Adam 等。优化器的选择可以影响网络的收敛速度和性能。
5. 数据准备和预处理:准备训练数据集,并进行必要的预处理操作,例如图像缩放、裁剪、归一化等。同时,也要准备相应的标签数据,确保标签与图像对应匹配。
6. 训练网络: 使用准备好的训练数据集对网络进行训练。在训练过程中,输入图像被提供给网络,通过反向传播算法更新网络参数,以最小化损失函数。
7. 模型评估和调优:使用验证集或测试集对训练好的模型进行评估,并根据评估结果对网络进行调优。常见的评估指标包括 IoU(Intersection over Union)、Dice 系数等。
8. 模型应用:在实际应用中使用训练好的模型对新的图像进行分割预测。
创建个简单的分类网络实战:
数据存放格式:
├─data ├─test │ ├─last │ └─last_msk └─train ├─last └─last_msk
展示: train/last图片 train/last_msk图片
加载数据集代码:
from torch.utils.data import Dataset import os import cv2 import numpy as np class MyDataset(Dataset): def __init__(self, train_path, transform=None): self.images = os.listdir(train_path + '/last') self.labels = os.listdir(train_path + '/last_msk') assert len(self.images) == len(self.labels), 'Number does not match' self.transform = transform self.images_and_labels = [] # 存储图像和标签路径 for i in range(len(self.images)): self.images_and_labels.append((train_path + '/last/' + self.images[i], train_path + '/last_msk/' + self.labels[i])) def __getitem__(self, item): img_path, lab_path = self.images_and_labels[item] img = cv2.imread(img_path) img = cv2.resize(img, (224, 224)) lab = cv2.imread(lab_path, 0) lab = cv2.resize(lab, (224, 224)) lab = lab / 255 # 转换成0和1 lab = lab.astype('uint8') # 不为1的全置为0 lab = np.eye(2)[lab] # one-hot编码 lab = np.array(list(map(lambda x: abs(x-1), lab))).astype('float32') # 将所有0变为1(1对应255, 白色背景),所有1变为0(黑色,目标) lab = lab.transpose(2, 0, 1) # [224, 224, 2] => [2, 224, 224] if self.transform is not None: img = self.transform(img) return img, lab def __len__(self): return len(self.images) if __name__ == '__main__': img = cv2.imread('data/train/last_msk/150.jpg', 0) img = cv2.resize(img, (16, 16)) img2 = img/255 img3 = img2.astype('uint8') hot1 = np.eye(2)[img3] hot2 = np.array(list(map(lambda x: abs(x-1), hot1))) print(hot2.shape) print(hot2.transpose(2, 0, 1))
模型代码:
import torch from torch import nn class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.encode1 = nn.Sequential( nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(64), nn.ReLU(True), nn.MaxPool2d(2, 2) ) self.encode2 = nn.Sequential( nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(128), nn.ReLU(True), nn.MaxPool2d(2, 2) ) self.encode3 = nn.Sequential( nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(256), nn.ReLU(True), nn.Conv2d(256, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.ReLU(True), nn.MaxPool2d(2, 2) ) self.encode4 = nn.Sequential( nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(512), nn.ReLU(True), nn.Conv2d(512, 512, 3, 1, 1), nn.BatchNorm2d(512), nn.ReLU(True), nn.MaxPool2d(2, 2) ) self.encode5 = nn.Sequential( nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(512), nn.ReLU(True), nn.Conv2d(512, 512, 3, 1, 1), nn.BatchNorm2d(512), nn.ReLU(True), nn.MaxPool2d(2, 2) ) self.decode1 = nn.Sequential( nn.ConvTranspose2d(in_channels=512, out_channels=256, kernel_size=3, stride=2, padding=1, output_padding=1), nn.BatchNorm2d(256), nn.ReLU(True) ) self.decode2 = nn.Sequential( nn.ConvTranspose2d(256, 128, 3, 2, 1, 1), nn.BatchNorm2d(128), nn.ReLU(True) ) self.decode3 = nn.Sequential( nn.ConvTranspose2d(128, 64, 3, 2, 1, 1), nn.BatchNorm2d(64), nn.ReLU(True) ) self.decode4 = nn.Sequential( nn.ConvTranspose2d(64, 32, 3, 2, 1, 1), nn.BatchNorm2d(32), nn.ReLU(True) ) self.decode5 = nn.Sequential( nn.ConvTranspose2d(32, 16, 3, 2, 1, 1), nn.BatchNorm2d(16), nn.ReLU(True) ) self.classifier = nn.Conv2d(16, 2, kernel_size=1) def forward(self, x): # b: batch_size out = self.encode1(x) # [b, 3, 224, 224] => [b, 64, 112, 112] out = self.encode2(out) # [b, 64, 112, 112] => [b, 128, 56, 56] out = self.encode3(out) # [b, 128, 56, 56] => [b, 256, 28, 28] out = self.encode4(out) # [b, 256, 28, 28] => [b, 512, 14, 14] out = self.encode5(out) # [b, 512, 14, 14] => [b, 512, 7, 7] out = self.decode1(out) # [b, 512, 7, 7] => [b, 256, 14, 14] out = self.decode2(out) # [b, 256, 14, 14] => [b, 128, 28, 28] out = self.decode3(out) # [b, 128, 28, 28] => [b, 64, 56, 56] out = self.decode4(out) # [b, 64, 56, 56] => [b, 32, 112, 112] out = self.decode5(out) # [b, 32, 112, 112] => [b, 16, 224, 224] out = self.classifier(out) # [b, 16, 224, 224] => [b, 2, 224, 224] 2表示类别数,目标和非目标两类 return out if __name__ == '__main__': img = torch.randn(2, 3, 224, 224) net = Net() sample = net(img) print(sample.shape)
训练代码:
# python # 导入必要的库 import os # 导入操作系统库,用于文件操作 import model # 导入自定义的模型模块 import torch # 导入PyTorch库 import torch.nn as nn # 导入PyTorch神经网络模块 import torch.optim as optim # 导入PyTorch优化器模块 import numpy as np # 导入NumPy库,用于数组操作 from load_img import MyDataset # 从load_img模块中导入自定义的MyDataset类 from torchvision import transforms # 导入torchvision的transforms模块,用于图像预处理 from torch.utils.data import DataLoader # 导入PyTorch的数据加载器模块 # 设置超参数 batchsize = 8 # 设置每个批次的大小为8 epochs = 50 # 设置训练的总轮数为50 train_data_path = 'data/train' # 设置训练数据的路径 # 定义图像预处理流程:转换为tensor并归一化 transform = transforms.Compose([ transforms.ToTensor(), # 将图像转换为Tensor transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 归一化处理,使用预定义的均值和标准差 ]) # 加载数据集,并设置批处理大小和是否打乱数据 bag = MyDataset(train_data_path, transform) # 初始化MyDataset类,加载数据并应用预处理 dataloader = DataLoader(bag, batch_size=batchsize, shuffle=True) # 使用DataLoader来加载数据,设置批次大小和打乱数据 # 选择使用GPU进行计算 device = torch.device('cuda') # 设置设备为CUDA(即GPU) # 将模型加载到GPU上 net = model.Net().to(device) # 初始化模型,并将其移动到GPU上 # 设置损失函数和优化器 criterion = nn.BCELoss() # 使用二分类交叉熵损失函数 optimizer = optim.SGD(net.parameters(), lr=1e-2, momentum=0.7) # 使用随机梯度下降优化器,设置学习率和动量 # 检查是否存在保存模型的文件夹,如果不存在则创建 if not os.path.exists('checkpoints'): os.mkdir('checkpoints') # 开始训练循环 for epoch in range(1, epochs + 1): # 对每个epoch进行循环 for batch_idx, (img, lab) in enumerate(dataloader): # 对每个batch的数据进行循环 img, lab = img.to(device), lab.to(device) # 将数据和标签移动到GPU上 # 通过模型进行前向传播,并使用sigmoid函数得到输出 output = torch.sigmoid(net(img)) # sigmoid函数用于将输出转换为概率值 # 计算损失 loss = criterion(output, lab) # 使用损失函数计算损失值 # 每20个batch打印一次损失值 if batch_idx % 20 == 0: print('Epoch:[{}/{}]\tStep:[{}/{}]\tLoss:{:.6f}'.format( epoch, epochs, (batch_idx + 1) * len(img), len(dataloader.dataset), loss.item() )) # 清空梯度,反向传播,更新权重 optimizer.zero_grad() # 清空梯度 loss.backward() # 反向传播计算梯度 optimizer.step() # 更新模型的权重 # 每10个epoch保存一次模型 if epoch % 10 == 0: torch.save(net, 'checkpoints/model_epoch_{}.pth'.format(epoch)) # 保存模型 print
测试代码:
# 导入必要的库 import torch # 导入PyTorch库 import cv2 # 导入OpenCV库,用于图像处理 from torch.utils.data import Dataset, DataLoader # 从PyTorch库中导入Dataset和DataLoader,用于构建数据集和加载数据 from torchvision import transforms # 从PyTorch的vision库中导入transforms,用于图像预处理 import numpy as np # 导入NumPy库,用于数值计算 import os # 导入os库,用于处理文件路径 # 定义一个名为TestDataset的类,继承自Dataset class TestDataset(Dataset): def __init__(self, test_img_path, transform=None): # 初始化方法,接收测试图像的路径和转换方法作为参数 self.test_img = os.listdir(test_img_path) # 获取测试图像路径下的所有文件名 self.transform = transform # 将转换方法保存为类的属性 self.images = [] # 初始化一个空列表,用于存储完整的图像路径 for i in range(len(self.test_img)): # 遍历所有文件名 self.images.append(os.path.join(test_img_path, self.test_img[i])) # 拼接完整的图像路径并添加到列表中 def __getitem__(self, item): # 根据索引获取图像的方法 img_path = self.images[item] # 根据索引获取图像的完整路径 img = cv2.imread(img_path) # 使用OpenCV读取图像 img = cv2.resize(img, (224, 224)) # 将图像缩放到224x224的大小 if self.transform is not None: # 如果有转换方法 img = self.transform(img) # 使用转换方法处理图像 return img # 返回处理后的图像 def __len__(self): # 返回数据集的长度的方法 return len(self.test_img) # 返回图像文件名的数量 # 定义测试图像的路径和模型检查点的路径 test_img_path = 'data/test/last' checkpoint_path = 'checkpoints/model_epoch_10.pth' # 定义图像转换的流水线 transform = transforms.Compose([transforms.ToTensor(), # 将图像转换为Tensor transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])]) # 对图像进行归一化处理 # 使用定义的TestDataset类创建数据集对象 bag = TestDataset(test_img_path, transform) # 使用DataLoader加载数据集 dataloader = DataLoader(bag, batch_size=1, shuffle=None) # 批处理大小为1,不打乱数据顺序 # 加载模型检查点 net = torch.load(checkpoint_path) # 将模型移动到GPU上 net = net.cuda() # 遍历数据加载器中的每一个数据批次 for idx, img in enumerate(dataloader): # 将图像数据移动到GPU上 img = img.cuda() # 前向传播,得到模型输出 output = torch.sigmoid(net(img)) # 对输出应用sigmoid函数,将其转换为概率值 # 将输出从GPU转移到CPU,并转换为NumPy数组 output_np = output.cpu().data.numpy().copy() # 找到每个样本输出中的最小值索引 output_np = np.argmin(output_np, axis=1) # 挤压数组,移除可能的单维度条目 img_arr = np.squeeze(output_np) # 将索引值转换为0-255的范围(这看起来是不正确的,因为索引通常是整数,而不是概率值) img_arr = img_arr*255 # 保存结果图像 cv2.imwrite('result/%03d.png'%idx, img_arr) # 打印保存的图像的文件名 print('result/%03d.png'%idx)
结果展示:
4、所以分割网络到底是什么?
代码就是上面那些,直接运行的话也是可以的,但是我还是不理解分割到底是怎么进行运算的?
继续理解吧。
首先,模型代码分析:
encode代表编码器,一共有5个encode,操作就是进行卷积+下采样
decode代表解码器,同样得有5个decode,操作就是进行卷积+上采样
最后一个分类器classifier,进行分类,
注意它的输出shape是1 x 2 x 224 x 224
代表: batch x channels x weight x height #batch 通道数 宽 高
注意这里的channel是前景类别 + 背景 #例如上面的类别是2,代表只有一个前景和一个背景输出的tensor是这样的
tensor([[[[ 3.1585, 3.3064, 3.1652, ..., 3.3435, 3.2202, 3.1270],
[ 3.2913, 3.4627, 3.4873, ..., 3.4024, 3.3583, 3.0580],
[ 3.3163, 3.4247, 3.2877, ..., 3.4584, 3.1003, 3.1352],
...,
[ 3.3558, 3.0914, 3.2765, ..., 3.3156, 3.3142, 3.0663],
[ 3.3003, 3.2883, 3.0972, ..., 3.3562, 3.2461, 3.1394],
[ 3.1776, 2.7403, 3.0708, ..., 2.8127, 3.1564, 2.8254]],
[[-3.1017, -3.2601, -3.1177, ..., -3.2974, -3.1925, -3.0627],
[-3.2573, -3.6687, -3.4113, ..., -3.6558, -3.3187, -3.0988],
[-3.2599, -3.2627, -3.2116, ..., -3.3546, -3.0332, -3.1106],
...,
[-3.3207, -3.2899, -3.2340, ..., -3.5797, -3.2753, -3.0780],
[-3.2396, -3.2038, -3.0360, ..., -3.2971, -3.1837, -3.0911],
[-3.1533, -2.7486, -3.0507, ..., -2.8135, -3.1188, -2.8821]]]],
device='cuda:0', grad_fn=<ConvolutionBackward0>)
然后,测试代码分析:
在这个代码里注意看前向传播这里。
output = torch.sigmoid(net(img))这里sigmoid是将数据归一化到0-1范围内,输出的数据是这样的
tensor([[[[0.9592, 0.9646, 0.9595, ..., 0.9659, 0.9616, 0.9580],
[0.9641, 0.9696, 0.9703, ..., 0.9678, 0.9664, 0.9551],
[0.9650, 0.9685, 0.9640, ..., 0.9695, 0.9569, 0.9583],
...,
[0.9663, 0.9565, 0.9636, ..., 0.9650, 0.9649, 0.9555],
[0.9644, 0.9640, 0.9568, ..., 0.9663, 0.9625, 0.9585],
[0.9600, 0.9394, 0.9557, ..., 0.9434, 0.9592, 0.9440]],[[0.0430, 0.0370, 0.0424, ..., 0.0357, 0.0394, 0.0447],
[0.0371, 0.0249, 0.0319, ..., 0.0252, 0.0349, 0.0432],
[0.0370, 0.0369, 0.0387, ..., 0.0337, 0.0460, 0.0427],
...,
[0.0349, 0.0359, 0.0379, ..., 0.0271, 0.0364, 0.0440],
[0.0377, 0.0390, 0.0458, ..., 0.0357, 0.0398, 0.0435],
[0.0410, 0.0602, 0.0452, ..., 0.0566, 0.0423, 0.0530]]]],
device='cuda:0', grad_fn=<SigmoidBackward0>)还可以知道,单个像素点的tensor是这样的
tensor([[[[0.9592],
[0.0430]],[[0.9641],
[0.0371]],[[0.9650],
[0.0370]],...,
[[0.9663],
[0.0349]],[[0.9644],
[0.0377]],[[0.9600],
[0.0410]]],
[[[0.9646],
[0.0370]],[[0.9696],
[0.0249]],[[0.9685],
[0.0369]],...,
[[0.9565],
[0.0359]],[[0.9640],
[0.0390]],[[0.9394],
[0.0602]]],
[[[0.9595],
[0.0424]],[[0.9703],
[0.0319]],[[0.9640],
[0.0387]],...,
[[0.9636],
[0.0379]],[[0.9568],
[0.0458]],[[0.9557],
[0.0452]]],
...,
[[[0.9659],
[0.0357]],[[0.9678],
[0.0252]],[[0.9695],
[0.0337]],...,
[[0.9650],
[0.0271]],[[0.9663],
[0.0357]],[[0.9434],
[0.0566]]],
[[[0.9616],
[0.0394]],[[0.9664],
[0.0349]],[[0.9569],
[0.0460]],...,
[[0.9649],
[0.0364]],[[0.9625],
[0.0398]],[[0.9592],
[0.0423]]],
[[[0.9580],
[0.0447]],[[0.9551],
[0.0432]],[[0.9583],
[0.0427]],...,
[[0.9555],
[0.0440]],[[0.9585],
[0.0435]],[[0.9440],
[0.0530]]]], device='cuda:0', grad_fn=<PermuteBackward0>)从这三个tensor可以明白,针对二分类任务,经过模型输出的预测结果包含两个通道,一个通道里存储的是前景的预测概率值,另一个通道里存储的是背景的预测概率值。或者说单个像素点会被预测两个值,分别存储在不同的通道里。多类别任务以此类推。。。
继续:
output_np = output.cpu().data.numpy().copy()是numpy数组的转换
继续:
output_np = np.argmin(output_np, axis=1)这里np.argmin是取最小值索引的操作,维度沿着通道维度,axis=1
这句代码的作用是将
output_np
数组中的每个像素(或像素区域)的类别概率转换为对应的类别索引。输出就是这样的:
然后就是:
img_arr = np.squeeze(output_np)目的在于去掉batch维度。之后就是进行上色了。如果想在原图上展示,就是先生成蒙版,再与图像进行合成,注意设置透明度。
参考:
感谢博主:阿里云云栖号
感谢博主:一文读懂语义分割与实例分割 - 知乎
感谢博主:Pytorch搭建训练简单的图像分割模型_pytorch分割使用预训练模型-CSDN博客
初识分割,写的不对的地方,请大家指教!
标签:__,...,nn,img,浅浅,self,pytorch,图像 From: https://blog.csdn.net/weixin_46319994/article/details/136689191