De um modo geral, o campo da visão computacional inclui três tarefas principais: classificação, detecção e segmentação. Entre eles, a tarefa de classificação tem requisitos relativamente simples para o modelo. Ela foi apresentada em detalhes no tutorial introdutório anterior do Pytorch. Amigos interessados podem conferir o blog anterior; enquanto as tarefas de detecção e segmentação requerem recursos de alto nível em várias escalas. informações, então os requisitos para a estrutura do modelo também são um pouco mais complicados. Neste artigo, apresentarei principalmente todo o processo de tarefas de segmentação semântica com base em imagens de TC de pulmão e rede UNet. Sem mais delongas, vamos ao que interessa.
1 O que é Segmentação Semântica?
A segmentação semântica é a principal tecnologia no campo da visão computacional.Ao classificar cada pixel da imagem, a imagem é dividida em várias regiões com categorias semânticas específicas. Em termos leigos, a tarefa de detecção de alvo é localizar e classificar o primeiro plano (vários objetos alvo) na imagem e detectar objetos de instância, como gatos, gatos, cachorros e pessoas, enquanto a tarefa de segmentação semântica requer que a rede detecte o objetos em primeiro plano na imagem. A categoria de cada pixel é julgada e a segmentação precisa no nível do pixel é executada, o que é amplamente utilizado no campo da direção automática.
2 Estojo de segmentação de imagem de TC de pulmão
Para iniciantes em segmentação semântica, a segmentação de imagens pulmonares é de fato um projeto relativamente fácil de entender e não muito difícil de começar. Abaixo, apresento o caso principalmente de três aspectos: dados, modelo, resultado e previsão.
2.1 Produção de conjunto de dados
2.1.1 Visão geral do conjunto de dados
Essa segmentação semântica usa principalmente imagens 2D, incluindo imagens CT e imagens de rótulos, ambas imagens de canal único com resolução de 512x512 e 267 imagens cada. Os dados são exibidos da seguinte forma:
2.1.2 Pré-processamento de dados
Como o fundo na imagem do rótulo é representado por 0, a imagem do pulmão é representada por 255, mas ao usar a classificação pytorch, as categorias precisam ser representadas na ordem de 0 (a categoria precisa ser um tensor contínuo a partir de Lei, que já foi mencionado antes). Portanto, precisamos alterar o valor 255 da imagem do pulmão da mesa para 1. O código principal relevante é mostrado na lista de códigos 1.
# 代码清单1
# 介绍:读入原始2D图像数据,对像素标签进行映射:0=>0 255=>1
image = cv.imread(image_fullpath,0)
img_array = np.asarray(image)
for i in img_array:
for j in i:
if j == 255:
label_img.append(1)
else:
label_img.append(0)
output_img = op_dir + each_image
label_img = np.array(label_img)
label_img = label_img.reshape((512, 512))
cv.imwrite(output_img, label_img)
n = n + 1
print("处理完成label: %d" % n)
Após a conclusão do mapeamento do valor do pixel, o tamanho da imagem é padronizado através da função resize e, finalmente, obtém-se uma imagem 512*512 contendo apenas valores de 0 e 1 pixel. Como o brilho representado por 1 é muito baixo, a imagem do rótulo processado aparece completamente preta a olho nu, e o rótulo da imagem processada é mostrado na figura.
Alguns alunos aqui podem perguntar: por que as imagens de TC não podem ser vistas após o processamento? Há algum problema? Na verdade, não é esse o caso, pois precisamos mapear os valores dos pixels para 0 e 1, então os pixels da imagem do rótulo são compostos apenas por 0 e 1. Para o olho humano, é difícil distinguir essa diferença sutil de valor de pixel, a menos que seus olhos sejam olhos eletrônicos. ... Se você não estiver à vontade, pode selecionar algumas imagens de rótulos aleatoriamente e lê-las com opencv ou PIL e imprimir os valores de pixel das fotos para verificar os resultados.
2.1.3 Gerar caminho de dados
Para ler a imagem convenientemente, precisamos gerar três arquivos txt para registrar o caminho da imagem original e sua imagem de etiqueta correspondente (a base de processamento de imagem relevante foi mencionada no blog anterior, e quem tiver dúvidas pode conferir sozinhos). O caminho de geração da imagem e a lista de códigos do rótulo correspondente são os seguintes:
# 代码清单2
# 介绍:读入原始2D图像数据,生成路径及标签
import os
def walk_dir(dir):
dir_list=[]
for image in os.listdir(dir):
dir_list.append(os.path.join(dir,image))
return dir_list
original_dir=r'CT_image'
save_dir=r'CT_txt'
if not save_dir:
os.mkdir(save_dir)
img_dir=os.listdir(original_dir)
img_test=walk_dir(os.path.join(original_dir,img_dir[0]))
img_test_label=walk_dir(os.path.join(original_dir,img_dir[1]))
img_t_v=walk_dir(os.path.join(original_dir,img_dir[2]))
img_t_v_label=walk_dir(os.path.join(original_dir,img_dir[3]))
img_train=img_t_v[:188]
img_val=img_t_v[188:]
img_train_label=img_t_v_label[:188]
img_val_label=img_t_v_label[188:]
# 查看每个图片与标签是否对应
# sum=0
# for index in range(len(img_train)):
# train=img_train[index].split("\\")[-1]
# train_label=img_train_label[index].split("\\")[-1]
# if train==train_label:
# print(train," ",train_label)
# sum+=1
# print(sum)
# 将训练集写入train.txt
with open(os.path.join(save_dir, 'train.txt'), 'a')as f:
for index in range(len(img_train)):
f.write(img_train[index]+'\t' +img_train_label[index]+'\n')
print("训练集及标签写入完毕")
# 将验证集写入val.txt
with open(os.path.join(save_dir, 'val.txt'), 'a')as f:
for index in range(len(img_val)):
f.write(img_val[index] + '\t' +img_val_label[index] + '\n')
print("验证集及标签写入完毕")
# 测试集
with open(os.path.join(save_dir, 'test.txt'), 'a')as f:
for index in range(len(img_test)):
f.write(img_test[index] + '\t' +img_test_label[index]+ '\n')
Após a execução, três documentos de texto train.txt, val.txt e test.txt são obtidos. train e val são usados para treinar e verificar o modelo, incluindo o caminho de dados e rótulos; test é usado para testar o modelo, incluindo apenas o caminho de dados.
2.1.4 Definir conjunto de dados
No Pytorch, a rede pode lidar com tensores, então precisamos converter as imagens lidas em dados de tensor e inseri-los na rede. Aqui usamos uma biblioteca muito importante: a biblioteca arch.utils.Dataset.
Dataset é uma classe wrapper, que é usada para agrupar dados em uma classe Dataset e, em seguida, passá-los para DataLoader. Em seguida, usamos a classe DataLoader para operar os dados mais rapidamente. Para herdar a classe Dataset, o método __len__ e o método __getitem__ devem ser reescritos. __len__ retorna o comprimento do dataset, e __getitem__ pode obter dados por índice. Sua implementação é mostrada na Listagem 3.
# 代码清单3
# 介绍:将读取到的图像数据转化为张量
import torch
import numpy as np
from PIL import Image
from torch.utils.data.dataset import Dataset
def read_txt(path):
# 读取文件
ims, labels = [], []
with open(path, 'r') as f:
for line in f.readlines():
im, label = line.strip().split("\t")
ims.append(im)
labels.append(label)
return ims, labels
class UnetDataset(Dataset):
def __init__(self, txtpath, transform):
super().__init__()
self.ims, self.labels = read_txt(txtpath)
self.transform = transform
def __getitem__(self, index):
im_path = self.ims[index]
label_path = self.labels[index]
image = Image.open(im_path)
image = self.transform(image).float().cuda()
label = torch.from_numpy(np.asarray(Image.open(label_path), dtype=np.int32)).long().cuda()
return image, label
def __len__(self):
return len(self.ims)
2.2 Visão geral da estrutura da rede
A estrutura da rede UNet é semelhante a uma grande letra U: primeiro execute a redução de amostragem de Conv+Pooling; depois a deconvolução de Deconv para upsampling, recorte o mapa de recursos de baixo nível antes da fusão e, em seguida, aumente a amostragem novamente. Repita este processo até obter o mapa de características que dá como saída 388 388 2 e, finalmente, o mapa de segmento de saída é obtido através do softmax. Ao contrário da adição ponto a ponto do FCN, o U-Net usa recursos para serem costurados na dimensão do canal para formar recursos mais profundos. Os detalhes da estrutura de rede específica não serão repetidos aqui.
2.3 Resultados e previsões
2.3.1 Transformação dos resultados da previsão
A função de previsão está no link de compartilhamento no final do artigo. O código é muito longo e não será exibido. Falarei principalmente sobre a transformação dos resultados. No processo de produção de dados, mapeamos os dados com um valor de pixel de 255 para 1, e a previsão da rede também é 0 e 1, então precisamos converter 1 em um valor de pixel de 255 para a saída do resultado. processo é o seguinte.
# 代码清单4
# 介绍:将预测结果转化为实际黑白影像
def translabeltovisual(save_label, path):
visual_img = []
im = cv2.imread(save_label, 0)
img_array = np.asarray(im)
for i in img_array:
for j in i:
if j == 1:
visual_img.append(255)
else:
visual_img.append(0)
visual_img = np.array(visual_img)
visual_img = visual_img.reshape((Height, Width))
cv2.imwrite(path, visual_img)
2.3.2 Exibição do resultado
2.3.3 Explicação parcial da função de avaliação do modelo
2.3.4
Use o Tensorboard para registrar a perda, precisão e IOU durante o processo de treinamento, com as rodadas de treinamento no eixo horizontal e cada indicador no eixo vertical.As respectivas curvas são mostradas na figura.
As seguintes conclusões podem ser tiradas da curva durante o processo de treinamento:
Primeiro, a perda geral do modelo no processo de treinamento nos dados de treinamento diminui lentamente sem nenhuma oscilação; no entanto, as oscilações são óbvias no conjunto de verificação, indicando que o parâmetros iniciais de treinamento não são apropriados.
Em segundo lugar, a precisão na segmentação semântica não pode representar totalmente o desempenho do algoritmo, e o desempenho real depende mais da interseção média e da pontuação da união. Seja o conjunto de treinamento ou o conjunto de verificação, a lacuna entre a precisão e a pontuação IoU é de cerca de 20 pontos percentuais; portanto, a tarefa de segmentação semântica não deve apenas prestar atenção à precisão, mas também à pontuação IoU do modelo.
3 Código-fonte e compartilhamento de dados
Siga a conta pública do WeChat "Alchemy Little Genius" e responda ao CT para obter dados de imagem e código-fonte!