点击下方卡片,关注“小白玩转Python”公众号
图像分割是一种计算机视觉技术,它为图像中的每个像素分配一个标签,使得具有相同标签的像素具有某些特征。例如,在街景中,所有属于汽车的像素可能被标记为一种颜色,而属于道路的像素可能被标记为另一种颜色。但是,要理解图像分割以及它为什么有用,让我们回到基础知识……
分类器
可爱的小狗
但是如果我们想要知道狗到底在哪里呢?一种方法是在狗周围画一个边界框,这称为目标检测。
可爱的小狗 + 边界框
但是如果你想要在像素级别精确地知道狗在哪里,那么你就需要更好的东西。这就是图像分割发挥作用的地方。
图像分割
街道分割
在上面的街景中,有5个类别:道路(粉色)、车辆(红色)、建筑物(黄色)、自然(绿色)、天空(蓝色)。每个像素被分配为这些类别中的一个。但是有时你想要能够区分不同的汽车或不同的树。为此,有3种主要类型的图像分割,每种提供不同的细节和信息级别。
语义分割 vs. 实例分割 vs. 泛化分割
语义分割 vs. 实例分割 vs. 泛化分割
语义分割根据像素的语义类别对其进行分类。所有的鸟属于同一个类别。
实例分割为不同的实例分配唯一的标签,即使它们是相同的语义类别。每只鸟都属于不同的类别。
泛化分割结合了两者,提供了类别级别和实例级别的标签。每只鸟都有自己的类别,但它们都被识别为“鸟”。
很酷,但我们怎么实际实现图像分割呢?有几种方法,比如阈值处理和聚类,但是当涉及到图像分割时,深度学习(我的最爱)确实是焦点。
U-Net
U-Net 架构最初设计用于医学图像分割,但现已被适用于许多其他用例。
U-Net
U-Net 具有编码器-解码器结构。编码器用于通过卷积和下采样将输入图像压缩为潜在空间表示。解码器用于通过卷积和上采样将潜在表示扩展到分割图像。沿着“U”形的长灰色箭头是跳过连接,它们具有两个主要目的:
在前向传播期间,它们使解码器能够访问来自编码器的信息。
在反向传播期间,它们充当梯度“超高速公路”,使解码器的梯度能够流向编码器。
模型的输出具有与输入相同的宽度和高度,但通道数将等于我们正在分割的类别数。
编码
U-Net 架构
定义模型架构相当简单:
from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D,
concatenate, Conv2DTranspose
def conv_block(x, n_filters):
"""two convolutions"""
x = Conv2D(n_filters, (3, 3), padding='same', activation='relu')(x)
x = Conv2D(n_filters, (3, 3), padding='same', activation='relu')(x)
return x
def encoder_block(x, n_filters):
"""conv block and max pooling"""
x = conv_block(x, n_filters)
p = MaxPooling2D((2, 2))(x)
return x, p # we will need x for the skip connections later
def decoder_block(x, p, n_filters):
"""upsample, skip connection, and conv block"""
x = Conv2DTranspose(n_filters, (2, 2), strides=(2, 2), padding='same')(x)
x = concatenate([x, p]) # concatenate = skip connection
x = conv_block(x, n_filters)
return x
def unet_model(n_classes, img_height, img_width, img_channels):
inputs = Input((img_height, img_width, img_channels)) # 512x512x3
# Contraction path, encoder
c1, p1 = encoder_block(inputs, n_filters=64) # c1=512x512x64 p1=256x256x64
c2, p2 = encoder_block(p1, n_filters=128) # c2=256x256x128 p2=128x128x128
c3, p3 = encoder_block(p2, n_filters=256) # c3=128x128x256 p3=64x64x256
c4, p4 = encoder_block(p3, n_filters=512) # c4=64x64x512 p4=32x32x512
# Bottleneck
bridge = conv_block(p5, n_filters=1024) # bridge=32x32x1024
# Expansive path, decoder
u4 = decoder_block(bridge, p4, n_filters=512) # 64x64x512
u3 = decoder_block(u4, p3, n_filters=256) # 128x128x256
u2 = decoder_block(u3, p2, n_filters=128) # 256x256x128
u1 = decoder_block(u2, p1, n_filters=64) # 512x512x64
outputs = Conv2D(n_classes, (1, 1), activation='softmax')(u1) # 512x512xn_classes
# notice the softmax activation in the final layer
model = Model(inputs=[inputs], outputs=[outputs])
return model
# example classes: [road, vehicles, buildings, nature, background]
# instantiate model to predict 5 classes
unet_model = multi_unet_model(
n_classes=5,
img_height=IMG_HEIGHT,
img_width=IMG_WIDTH,
img_channels=3
)
# input: 512x512x3
# output: 512x512x5
损失函数:分类交叉熵
我们如何优化这个模型?嗯,因为图像分割实际上只是在像素级别上的分类,我们可以使用标准的分类损失函数,即分类交叉熵。
model.compile(
loss="categorical_crossentropy",
categorical_crossentropy
)
我们可以将结果(512x512x5)体积的每个像素解释为长度为 5 的向量。由于最后一层在最后一个维度上使用 softmax 激活,因此每个像素向量包含该像素属于每个类别的概率。
模型输出
在训练模型之前,我们需要一个数据集。数据集应包含(图像,掩码)对,其中图像(x)的形状为(512x512x3),掩码(y)的形状为(512x512x5)。
这是一个示例的地面真实掩码:
每个像素只能属于一个类别,因此它包含一个类别通道中的“1”,并在其他通道中包含一个“0”。您可以将每个像素视为一个独热向量(因为它就是)。一旦准备好你的数据集,你就可以开始训练了:
model.fit(
train_ds,
validation_data=val_ds,
epochs=10,
)
当然,这段代码不足以运行一个成功的模型。如果你真的想要实现这个,你需要考虑预处理、重新缩放、批处理等。我准备了一个运行图像分割模型的完整代码,可以用于汽车分割(对汽车的不同部分进行分割),链接:https://www.kaggle.com/code/rajpulapakura/car-segmentation-model-dev/notebook#Segmentation-model
最后说明
类别不平衡:在图像分割中经常存在严重的类别不平衡。例如,在平均街景图像中,汽车和建筑物占据了大量像素,但停车标志占用的像素非常少。模型在分割停车标志时数据较少,因此它的性能会较差。为了解决这个问题,您可以使用 Focal 分类交叉熵和类别权重,它们强调少数类别。
其他架构:U-Net 不是唯一的图像分割架构,尽管概念上是最简单的。其他架构包括 SegNet、Mask R-CNN 和 PSPNet。
二进制分割:如果你只有一个类别要分割(例如在 MRI 扫描中分割脑肿瘤),那么模型的输出只需要是(512x512)。对于掩码,每个像素将包含一个“1”,表示该像素属于肿瘤,或者是一个“0”,表示该像素不属于肿瘤。确保在模型的最终激活中也将“softmax”更改为“sigmoid”,并使用(Focal)二元交叉熵损失函数。
· END ·
HAPPY LIFE
本文仅供学习交流使用,如有侵权请联系作者删除