【图像增广库imgaug】官方文档翻译(三):增强分割图

分割图是2D数组,其中每个空间位置都只分配给一个类。它们在imgaug中使用imgaug.augmentables.segmaps.SegmentationMapsOnImage表示。这个类被实例化为SegmentationMapsOnImage(arr, shape)。arr包含 2D 分割图并且shape是对应图像的形状(不是分割图数组的形状)。该类称为“…MapsOnImage”而不是“…MapOnImage”,因为您实际上可以为同一图像提供许多分割图(假设它们都具有相同的高度和宽度)。这意味着arr可能具有形状(H,W)或(H,W,1),但也可以具有(H,W,C)形状C>1。这对于例如堆叠的布尔掩码或实例分割图(每个类一个实例图)很有用。

类似于构造函数参数,SegmentationMapsOnImage具有属性.shape(相应图像的形状)和.arr(内部分割图表示)。

SegmentationMapsOnImage的重要方法:

  • get_arr():将分割图的内部表示转换为最初提供给构造函数的相同形状和数据类型。(内部表示是 shape (H,W,C)(可能有C=1)和 dtype int32。)
  • draw([size], [colors]):将分割图转换为 RGB 图像。
  • draw_on_image(image, [alpha], [resize], [colors], [draw_background]):将分割图转换为 RGB 图像并将其与提供的图像混合。
  • pad([top], [right], [bottom], [left], [mode], [cval]):在其两侧填充分割图。
  • pad_to_aspect_ratio(aspect_ration, [mode], [cval], [return_pad_amounts]): 将分割图填充到纵横比 ( ratio=width/height)。
  • resize(sizes, [interpolation]):将分割图调整为提供的大小。默认使用最近邻插值。

要增强分割图,请使用所有增强器都提供的augment_segmentation_maps() 。它需要一个SegmentationMapsOnImage实例或a list of SegmentationMapsOnImage。您也可以调用augment(image=…, segmentation_map=…)或其别名__call__(images=…, segmentation_maps=…)(例如Affine(scale=1.5)(images=…, segmentation_maps=…)),它们都允许将分割图提供为类似 int 的数组。
有关更多详细信息,请参阅相关api。

对于绘图例程SegmentationMapsOnImage,使用一组预定义的颜色。这些会保存在常量SegmentationMapsOnImage.DEFAULT_SEGMENT_COLORS中。它们可能会在未来的某个时候被 matplotlib colormap所取代。

重要: imgaug的分割图增强适用于ground truth输出。因此,只有改变图像几何形状的增强技术才会应用于分割图,即使其他增强技术是pipeline的一部分。例如水平翻转或仿射变换。要同时应用非几何增强技术,把augmenter.augment_images()改为输入分割图数组。


从给定为点的多边形创建示例分割图

我们第一个示例的目标是加载图像,创建分割图并增强它们。让我们首先加载并可视化我们的示例图像:

import imageio
import imgaug as ia
%matplotlib inline

image = imageio.imread("https://upload.wikimedia.org/wikipedia/commons/f/f4/Tamias_striatus_CT.jpg")
image = ia.imresize_single_image(image, 0.15)
print(image.shape)
ia.imshow(image)

(319, 479, 3)
在这里插入图片描述
现在我们需要该图像的分割图。我们将创建两个类,一个用于树(底部),一个用于花栗鼠(中心)。其他一切都将是背景。这两个类都将创建为多边形,然后绘制在分割图数组上。首先,我们定义树多边形的四个角点:

import numpy as np
from imgaug.augmentables.kps import KeypointsOnImage

tree_kps_xy = np.float32([
    [0, 300],  # left top of the tree
    [image.shape[1]-1, 230],  # right top
    [image.shape[1]-1, image.shape[0]-1],  # right bottom
    [0, image.shape[0]-1]  # left bottom
])

# 可视化
kpsoi_tree = KeypointsOnImage.from_xy_array(tree_kps_xy, shape=image.shape)
ia.imshow(kpsoi_tree.draw_on_image(image, size=13))

在这里插入图片描述
现在我们创建花栗鼠多边形。那需要更多的角点,但基本方法是相同的:

chipmunk_kps_xy = np.float32([
    [200, 50],  # left ear, top (from camera perspective)
    [220, 70],
    [260, 70],
    [280, 50],  # right ear, top
    [290, 80],
    [285, 110],
    [310, 140],
    [330, 175], # right of cheek
    [310, 260], # right of right paw
    [175, 275], # left of left paw
    [170, 220],
    [150, 200],
    [150, 170], # left of cheek
    [160, 150],
    [186, 120], # left of eye
    [185, 70]
])

# 可视化
kpsoi_chipmunk = KeypointsOnImage.from_xy_array(chipmunk_kps_xy, shape=image.shape)
ia.imshow(kpsoi_chipmunk.draw_on_image(image, size=7))

在这里插入图片描述
在下一步中,我们将两组角点转换为 imgaug.augmentables.polys.Polygon 的实例:

from imgaug.augmentables.polys import Polygon

# 创建多边形
poly_tree = Polygon(kpsoi_tree.keypoints)
poly_chipmunk = Polygon(kpsoi_chipmunk.keypoints)

# 可视化多边形
ia.imshow(np.hstack([
    poly_tree.draw_on_image(image),
    poly_chipmunk.draw_on_image(image)
]))

在这里插入图片描述
现在我们将两个多边形转换为单个分割图。为此,我们首先为我们的三个类(背景、树、花栗鼠)创建一个(H,W,3)空数组。然后我们将树和花栗鼠多边形绘制到该数组上,就好像它是一个图像一样。我们使用 100% 的绿色(树)和 100% 的蓝色(花栗鼠)颜色,从而仅将树绘制到第二个图像通道中,而将花栗鼠仅绘制到第三个通道中。然后我们将三个通道合并为一个分割图。

# 创建空分割图: background, tree, chipmunk
segmap = np.zeros((image.shape[0], image.shape[1], 3), dtype=np.uint8)

# 把树多边形draw到通道2
segmap = poly_tree.draw_on_image(
    segmap,
    color=(0, 255, 0),
    alpha=1.0, alpha_lines=0.0, alpha_points=0.0)

# 把花栗鼠多边形draw到通道3
segmap = poly_chipmunk.draw_on_image(
    segmap,
    color=(0, 0, 255),
    alpha=1.0, alpha_lines=0.0, alpha_points=0.0)

# 将三个通道合并为一个
segmap = np.argmax(segmap, axis=2)

# 将 dtype 从 int64 更改为 int32,因为 int32 是 SegmentationMapsOnImage 允许的最大值
segmap = segmap.astype(np.int32)

# 打印分割图信息
print("Shape:", segmap.shape, "min value:", segmap.min(), "max value:", segmap.max())

Shape: (319, 479) min value: 0 max value: 2

现在我们只需要调用imgaug.augmentables.segmaps.SegmentationMapsOnImage和分割图就可以在图像上绘制结果。

from imgaug.augmentables.segmaps import SegmentationMapsOnImage

# 将数组转换为 SegmentationMapsOnImage 实例
segmap = SegmentationMapsOnImage(segmap, shape=image.shape)

# 可视化
# 请注意,分割图绘制方法返回 RGB 图像列表。
# 是因为分割图可能有多个通道
# -- (H,W,C) 中的 C -- 为这些通道中的每一个绘制一个图像。
# 我们这里有 C=1,所以我们在这里得到一个单一图像的列表,并通过 [0] 访问它。
ia.imshow(segmap.draw_on_image(image)[0])

在这里插入图片描述

增强示例分割图

创建示例分割图之后,我们的下一个目标是增强它(以及相应的图像)。首先,我们创建增强sequence,包括一些粗略的 dropout(将矩形区域设置为零)、仿射变换和弹性变换(水样效果)。请注意,只有Affine并且ElasticTransformation实际上会更改分割图,因为分割图增强仅受更改图像几何的增强器的影响。

import imgaug.augmenters as iaa
ia.seed(2)

seq = iaa.Sequential([
    iaa.CoarseDropout(0.1, size_percent=0.2),
    iaa.Affine(rotate=(-30, 30)),
    iaa.ElasticTransformation(alpha=10, sigma=1)
])

我们的下一步是将增强pipeline应用于分割图和图像。为此,我们使用seq.augment(image=…, segmentation_maps=…)或其快捷方式seq(image=…, segmentation_maps=…):

image_aug, segmap_aug = seq(image=image, segmentation_maps=segmap)

# 可视化
ia.imshow(np.hstack([
    segmap_aug.draw_on_image(image_aug)[0],  # show blend of (augmented) image and segmentation map
    segmap_aug.draw()[0]  # show only the augmented segmentation map
]))

在这里插入图片描述
注意分割图是如何受到仿射变换(即旋转)和弹性变换elastic(即水样效果,这里相当嘈杂)的影响。它不受粗略丢失的影响,因为该增强器不会改变图像几何形状。(还要注意,这里的粗略丢弃确实会丢弃矩形区域,这些区域看起来是非矩形的,因为随后应用了弹性变换并使矩形变得粗糙。)

上面的弹性变换噪声很大,很难看出图像和分割图增强是否对齐。让我们以较少噪音的设置自行执行它。让我们还生成一张输出图像,其中输入图像和分割图都自行增强,这将导致不同的采样随机值,从而导致未对齐的增强。

aug = iaa.ElasticTransformation(alpha=200, sigma=10)

# 对齐(正确)
image_aug, segmap_aug = aug(image=image, segmentation_maps=segmap)

# 没有 to_deterministic() (不正确) 
image_aug_unaligned = aug(image=image)
segmap_aug_unaligned = aug(segmentation_maps=segmap)

ia.imshow(
    np.hstack([
        segmap_aug.draw_on_image(image_aug, alpha=0.4)[0],
        segmap_aug.draw_on_image(image_aug_unaligned, alpha=0.4)[0]
    ])
)

在这里插入图片描述
如您所见,右侧图像中的图像和分割图之间存在显着差异(例如对于树很明显)。在具有对齐采样值(左)的版本中,图像和分割图匹配良好。


缩放分割图

分割图通常必须调整大小,例如为网络创建较低分辨率的ground truth输出。这可以使用resize()方法来完成。以下示例使用该方法将分割图的大小调整为原始大小的 1/4。或者,也可以使用固定大小(例如(100, 200),获取高度为 100 和宽度为 200 的输出map)。

segmap_small = segmap.resize(0.25)
print("Before:", segmap.arr.shape, "After:", segmap_small.arr.shape)

Before: (319, 479, 1) After: (80, 120, 1)

ia.imshow(segmap_small.draw()[0])

在这里插入图片描述
让我们可视化调整大小之前和之后的分割图,投影到图像上(即再次调整大小,回到图像大小)。缩小+放大(右)后的地图明显比没有(左)的地图更粗糙。

ia.imshow(
    np.hstack([
        segmap.draw_on_image(image)[0],
        segmap_small.draw_on_image(image)[0]
    ])
)

在这里插入图片描述

增强分割图小于其对应图像

网络的地面实况输出分割图小于输入图像是很常见的。这种情况由 imgaug 自动处理。只需通过增强pipeline输入较小尺寸的分割图,就好像它与图像大小相同。只需确保 SegmentationMapsOnImage 的.shape属性与输入图像大小匹配(一如既往)。

以下代码块增强了示例图像和先前缩小的分割图。我们首先打印数组形状:

print("Image size: %s Segmentation Map size: %s (on image with shape: %s)" % (
    image.shape, segmap_small.arr.shape, segmap_small.shape))

Image size: (319, 479, 3) Segmentation Map size: (80, 120, 1) (on image with shape: (319, 479, 3))

现在我们增强和可视化图像和小尺度分割图。

aug = iaa.ElasticTransformation(alpha=200, sigma=10)

image_aug, segmap_small_aug = aug(image=image, segmentation_maps=segmap_small)

ia.imshow(segmap_small_aug.draw_on_image(image_aug, alpha=0.4)[0])

在这里插入图片描述
即使参数表示像素值,小尺寸分割图的增强仍然有效。以下示例显示了一个常见场景,其中图像被裁剪了一定数量的像素(底部为 50,左侧为 200)。像素量被转换为较小尺寸分割图的对应像素量。(为了比较,我们还增加了图像大小的分割图。)

aug = iaa.Crop(px=(0, 0, 50, 200))  # (top, right, bottom, left)

# 我们必须用完全相同的随机值来扩充两个分割图。
# 这就是我们在这里切换到确定性模式的原因,它会在每次增强后重置随机状态。
# (由于使用非随机裁剪值,我们也可以跳过这个,但这样做似乎更干净。)
aug_det = aug.to_deterministic()
image_aug = aug_det.augment_image(image)  # 增强图像
segmap_aug = aug_det.augment_segmentation_maps(segmap)  # 增强正常大小的分割图
segmap_small_aug = aug_det.augment_segmentation_maps(segmap_small) # 增强更小尺寸的分割图

ia.imshow(np.hstack([
    segmap_aug.draw_on_image(image_aug, alpha=0.4)[0],  # 绘制增强的正常尺寸分割图
    segmap_small_aug.draw_on_image(image_aug, alpha=0.4)[0]  # 绘制增强的小尺寸分割图
]))

在这里插入图片描述
尽管两个增强分割图(左:正常比例,右:小比例)在这里都自动放大到图像大小,因此两者似乎具有相同的分辨率,但较小的分割图(右)由于其实际上具有明显更粗糙的边缘在自动放大之前降低分辨率。


将增强分割图转换为 Numpy 数组

SegmentationMapsOnImage的内部数组,可通过.arr属性访问,默认具有 dtype int32和 shape (H,W,C)。如果构造SegmentationMapsOnImage函数的原始数组具有相同的 dtype 和 shape,您可以简单地访问.arr。否则,您应该调用get_arr(),它会自动转换.arr为您的原始 dtype 和 shape。(准确地说:它不会转换为原始形状,而是转换为原始尺寸。创建对象后形状可能会因调整大小操作而发生变化。

arr = segmap.arr
arr_int = segmap.get_arr()

print("[segmap.arr]       Shape:", arr.shape, "dtype:", arr.dtype)
print("[segmap.get_arr()] Shape:", arr_int.shape, "dtype:", arr_int.dtype)

[segmap.arr] Shape: (319, 479, 1) dtype: int32
[segmap.get_arr()] Shape: (319, 479) dtype: int32


填充分割图

与图像类似,可以使用 SegmentationMapsOnImage.pad() 填充分割图。填充支持几种不同的模式,最合适的模式通常是constant(使用常量值cval填充)和edge(在图像边框周围不断重复类 ID)。以下示例展示了这两种模式。它首先用常数值0(左图)填充图像和分割图,然后使用edge模式(右图)填充它们。

image_pad_constant = ia.pad(image, left=100, top=20)  # mode="constant" and cval=0 are the defaults of pad()
segmap_pad_constant = segmap.pad(left=100, top=20)    # mode="constant" and cval=0 are the defaults of pad()

image_pad_edge = ia.pad(image, left=100, top=20, mode="edge")
segmap_pad_edge = segmap.pad(left=100, top=20, mode="edge")

ia.imshow(np.hstack([
    segmap_pad_constant.draw_on_image(image_pad_constant, alpha=0.5)[0],
    segmap_pad_edge.draw_on_image(image_pad_edge, alpha=0.5)[0],
]))

在这里插入图片描述
使用edge模式时,必须小心没有完全扩展到图像边缘的类。下面的示例使用edge模式填充图像和分割图,但现在填充左侧和右侧。虽然分割图的填充在左侧表现如预期,但它不会扩展右侧的类。

image_wide = ia.pad_to_aspect_ratio(image, 3.0, mode="edge")
segmap_wide = segmap.pad_to_aspect_ratio(3.0, mode="edge")

ia.imshow(segmap_wide.draw_on_image(image_wide, alpha=0.5)[0])

在这里插入图片描述
出现上述问题的原因是,用于将树多边形添加到分割图中的绘图例程在图像右边缘稍远处停止。下面的输出证明了这一点。它显示了分割图最后五列的Class ID。树类只扩展到倒数第二列。

print(segmap.arr[:, -5:, 0])

[[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[1 1 1 1 0]
[1 1 1 1 0]
[0 0 0 0 0]]


绘制分割图

上面的例子已经使用了 SegmentationMapsOnImage 提供的绘图方法。其中一种方法是draw(),它将分割图转换为 RGB 图像列表。对于(H,W,C)分割图,它返回CRGB 图像。在大多数情况下,C是1,因此该列表仅包含单个图像。下面的示例使用该draw()方法在增强前后可视化分割图:

segmap_aug = iaa.ElasticTransformation(alpha=50, sigma=5).augment_segmentation_maps(segmap)
ia.imshow(np.hstack([
    segmap.draw()[0],
    segmap_aug.draw()[0],
]))

在这里插入图片描述
如果需要默认颜色以外的其他颜色,colors可以使用该参数来定义它们。它期望每个类有一个可迭代的列表。每个可迭代对象必须包含三个uint8值。

ia.imshow(np.hstack([
    segmap.draw()[0],
    segmap.draw(colors=[(255, 255, 255), (128, 128, 0), (0, 0, 128)])[0],
]))

在这里插入图片描述
为了方便缩放,draw还提供了一个size参数,可以设置为例如(height, width)元组或分数(相对于保存在 SegmentationMapsOnImage.shape 中的大小),如下例所示:

ia.imshow(segmap.draw(0.1)[0])

在这里插入图片描述
该方法draw_on_image()类似于draw(),但适用于在另一个图像上可视化分割图。该方法执行以下三个步骤:(1)将分割图转换为 RGB 图像列表(就像这样draw()做),(2)将它们中的每一个调整为与提供的基本图像相同的大小(或者调整基础图像到绘制的分割图大小,具体取决于参数),(3)它对图像和分割图图像进行 alpha 混合。以下示例展示了draw_on_image(). 请注意,背景(class id 0)默认设置为透明。

ia.imshow(segmap.draw_on_image(image)[0])

在这里插入图片描述
在 alpha 混合步骤中,可以使用以下alpha参数控制分割图的不透明度:

ia.imshow(np.hstack([
    segmap.draw_on_image(image, alpha=0.25)[0],
    segmap.draw_on_image(image, alpha=0.50)[0],
    segmap.draw_on_image(image, alpha=0.75)[0]
]))
ia.imshow(np.hstack([
    segmap.draw_on_image(image, alpha=0.25)[0],
    segmap.draw_on_image(image, alpha=0.50)[0],
    segmap.draw_on_image(image, alpha=0.75)[0]
]))

在这里插入图片描述
通常,具有背景class id 的像素0被设置为透明,即它们显示基本图像。分割图图像的这些背景类像素也可以通过设置draw_background=True来绘制。背景的默认颜色是黑色。

ia.imshow(segmap.draw_on_image(image, draw_background=True)[0])

在这里插入图片描述
或者,使用background_class_id它可以控制哪个类应该被视为背景,因此在混合中被忽略。

ia.imshow(segmap.draw_on_image(image, draw_background=False, background_class_id=2)[0])

在这里插入图片描述
draw_on_image()可以处理图像和分割图之间的不同大小。默认情况下,使用最近邻插值将分割图调整为图像大小。以下示例使用具有 20% 图像大小的分割图来显示这一点:

segmap_small = segmap.resize(0.2)
print(segmap_small.arr.shape, image.shape)

ia.imshow(segmap_small.draw_on_image(image)[0])

(64, 96, 1) (319, 479, 3)
在这里插入图片描述
不将分割图调整为图像的大小,也可以使用resize参数将图像调整为分割图的大小:

ia.imshow(segmap_small.draw_on_image(image, resize="image")[0])

在这里插入图片描述


使用非几何增强改变分割图

默认情况下,分割图增强使用augment(segmentation_maps=…)或augment_segmentation_maps(…)面向ground truth增强,因此仅应用几何影响增强。如果不希望有这种限制,则必须将分割图视为原始数组并通过augment(images=…)或augment_images(…)。下面的代码块首先介绍一个使用普通的例子augment(image=…, segmentation_maps=…)。在第二个代码块中,这将更改为 using two times augment(image=…)。

# example augmentation pipeline
aug = iaa.Sequential([
    iaa.Affine(rotate=(-20, 20)),  # only this affects segmentation maps via augment_segmentation_maps()
    iaa.CoarseDropout(0.2, size_percent=0.1),
    iaa.AdditiveGaussianNoise(scale=0.2*255)
])

# standard way of augmenting segmentation maps via augment_segmentation_maps()
image_aug, segmap_aug = aug(image=image, segmentation_maps=segmap)

# visualize before/after
ia.imshow(np.hstack([
    segmap_aug.draw_on_image(image_aug)[0],
    segmap_aug.draw()[0]
]))

在这里插入图片描述
现在我们用 Affine 和 非几何的CoarseDropout 来扩充分割图。但是,我们不想应用于AdditiveGaussianNoise分割图,只应用于图像。实现这一点的一种相当简单的方法是使用两种不同的增强pipelines,一种用于图像,另一种用于分割图。然后我们通过手动调整种子初始化每个增强器random_state=< seed >。通过在两条pipelines之间选择匹配的种子,我们可以确保增强器抽取相同的样本。

# augmentation pipeline for images
aug_images = iaa.Sequential([
    iaa.Affine(rotate=(-20, 20), random_state=1),
    iaa.CoarseDropout(0.2, size_percent=0.05, random_state=2),
    iaa.AdditiveGaussianNoise(scale=0.2*255, random_state=3)
], random_state=4)

# augmentation pipeline for segmentation maps - with coarse dropout, but without gaussian noise
aug_segmaps = iaa.Sequential([
    iaa.Affine(rotate=(-20, 20), random_state=1),
    iaa.CoarseDropout(0.2, size_percent=0.05, random_state=2)
], random_state=4)

# First, augment image.
image_aug = aug_images(image=image)

# Second, augment segmentation map.
# We convert to uint8 as that dtype has usually best support and hence is safest to use.
segmap_arr_aug = aug_segmaps(image=segmap.get_arr().astype(np.uint8))
segmap_aug = SegmentationMapsOnImage(segmap_arr_aug, shape=segmap.shape)

# visualize
ia.imshow(np.hstack([
    image_aug,
    segmap_aug.draw_on_image(image_aug, alpha=0.5)[0],
    segmap_aug.draw()[0]
]))

在这里插入图片描述
请注意,当通过augment(image=…)或增加分割图时augment_images(…),不会自动考虑大小差异。augment(segmentation_maps=…)因此,虽然通过或augment_segmentation_maps(…)可以使用小于相应图像的分割图进行增强,但如果使用前一种基于图像的方法,尺寸应该匹配。否则,尽管大小不同,但必须手动调整两条pipelines以匹配。然后,例如Crop应该使用分数而不是原始像素值作为参数 - 或者像素值必须在pipelines之间不同。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值