Engee 文档
Notebook

图像索引和分割

本演示将展示如何使用 IndirectArray 处理索引图像,以及如何使用 ImageSegmentation 分割图像。

索引

索引图像由两部分组成:索引和像素本身。

下面是图像作为数组的 "直接 "表示。

In [ ]:
Pkg.add(["IndirectArrays", "TestImages"])
In [ ]:
using Images
img = [
    RGB(0.0, 0.0, 0.0) RGB(1.0, 0.0, 0.0) RGB(0.0, 1.0, 0.0) RGB(0.0, 0.0, 1.0) RGB(1.0, 1.0, 1.0)
    RGB(1.0, 0.0, 0.0) RGB(0.0, 1.0, 0.0) RGB(0.0, 0.0, 1.0) RGB(1.0, 1.0, 1.0) RGB(0.0, 0.0, 0.0)
    RGB(0.0, 1.0, 0.0) RGB(0.0, 0.0, 1.0) RGB(1.0, 1.0, 1.0) RGB(0.0, 0.0, 0.0) RGB(1.0, 0.0, 0.0)
    RGB(0.0, 0.0, 1.0) RGB(1.0, 1.0, 1.0) RGB(0.0, 0.0, 0.0) RGB(1.0, 0.0, 0.0) RGB(0.0, 1.0, 0.0)
    RGB(1.0, 1.0, 1.0) RGB(0.0, 0.0, 0.0) RGB(1.0, 0.0, 0.0) RGB(0.0, 1.0, 0.0) RGB(0.0, 0.0, 1.0)]
Out[0]:
No description has been provided for this image

或者,我们也可以将其保存为索引图像:

In [ ]:
indices = [1 2 3 4 5; 2 3 4 5 1; 3 4 5 1 2; 4 5 1 2 3; 5 1 2 3 4] # `i` records the pixel value `palette[i]`
palette = [RGB(0.0, 0.0, 0.0), RGB(1.0, 0.0, 0.0), RGB(0.0, 1.0, 0.0), RGB(0.0, 0.0, 1.0), RGB(1.0, 1.0, 1.0)]

palette[indices] == img
Out[0]:
true

现在让我们分析一下使用 IndirectArray 的利弊。

由于要存储两个单独的字段,普通索引图像可能需要更多内存。另一方面,如果我们使用 unique(img),大小就会接近图片本身的大小。 此外,标准索引需要两次操作,因此可能比直接图像表示法更慢(而且不适合 SIMD 向量化)。

此外,使用索引图像格式和两个单独的数组可能会很麻烦,因此 IndirectArrays 提供了一个数组抽象来合并这两部分:

In [ ]:
using IndirectArrays

indexed_img = IndirectArray(indices, palette)
img == indexed_img
Out[0]:
true

因为 IndirectArray 只是一个数组,所以常见的图像操作也适用于这种类型,例如

In [ ]:
new_img = imresize(indexed_img; ratio=2)
Out[0]:
No description has been provided for this image
In [ ]:
dump(indexed_img)
IndirectArray{RGB{Float64}, 2, Int64, Matrix{Int64}, Vector{RGB{Float64}}}
  index: Array{Int64}((5, 5)) [1 2 … 4 5; 2 3 … 5 1; … ; 4 5 … 2 3; 5 1 … 3 4]
  values: Array{RGB{Float64}}((5,))
    1: RGB{Float64}
      r: Float64 0.0
      g: Float64 0.0
      b: Float64 0.0
    2: RGB{Float64}
      r: Float64 1.0
      g: Float64 0.0
      b: Float64 0.0
    3: RGB{Float64}
      r: Float64 0.0
      g: Float64 1.0
      b: Float64 0.0
    4: RGB{Float64}
      r: Float64 0.0
      g: Float64 0.0
      b: Float64 1.0
    5: RGB{Float64}
      r: Float64 1.0
      g: Float64 1.0
      b: Float64 1.0
In [ ]:
dump(new_img)
Array{RGB{Float64}}((10, 10))
  1: RGB{Float64}
    r: Float64 0.0
    g: Float64 0.0
    b: Float64 0.0
  2: RGB{Float64}
    r: Float64 0.25
    g: Float64 0.0
    b: Float64 0.0
  3: RGB{Float64}
    r: Float64 0.75
    g: Float64 0.0
    b: Float64 0.0
  4: RGB{Float64}
    r: Float64 0.75
    g: Float64 0.25
    b: Float64 0.0
  5: RGB{Float64}
    r: Float64 0.25
    g: Float64 0.75
    b: Float64 0.0
  ...
  96: RGB{Float64}
    r: Float64 0.75
    g: Float64 0.25
    b: Float64 0.0
  97: RGB{Float64}
    r: Float64 0.25
    g: Float64 0.75
    b: Float64 0.0
  98: RGB{Float64}
    r: Float64 0.0
    g: Float64 0.75
    b: Float64 0.25
  99: RGB{Float64}
    r: Float64 0.0
    g: Float64 0.25
    b: Float64 0.75
  100: RGB{Float64}
    r: Float64 0.0
    g: Float64 0.0
    b: Float64 1.0

我们可以看到,在对索引数组进行操作后,该数组已简化为标准像素数组的形式。

分割

接下来,我们使用分水岭算法对图像进行分割。这是数学形态学用于图像分割的基本工具。

In [ ]:
using TestImages
using IndirectArrays
In [ ]:
img = testimage("blobs")
Out[0]:
No description has been provided for this image
In [ ]:
img_example = zeros(Gray, 5, 5)
img_example[2:4,2:4] .=  Gray(0.6)
bw = Gray.(img) .> 0.5
bw_example = img_example .> 0.5
Out[0]:
5×5 BitMatrix:
 0  0  0  0  0
 0  1  1  1  0
 0  1  1  1  0
 0  1  1  1  0
 0  0  0  0  0

feature_transform 函数允许我们找到二值图像的特征变换 (bw)。它能为 bw 中的每个位置找到最近的 "特征"(bw 为真的位置)。

如果两个或多个对象的距离相同,则会选择一个任意对象。

例如,bw_example[1,1]中最近的 true 位于 CartesianIndex(2, 2),因此它被赋值为 CartesianIndex(2,2)。

In [ ]:
bw_transform = feature_transform(bw)
bw_transform_example = feature_transform(bw_example)
Out[0]:
5×5 Matrix{CartesianIndex{2}}:
 CartesianIndex(2, 2)  CartesianIndex(2, 2)  …  CartesianIndex(2, 4)
 CartesianIndex(2, 2)  CartesianIndex(2, 2)     CartesianIndex(2, 4)
 CartesianIndex(3, 2)  CartesianIndex(3, 2)     CartesianIndex(3, 4)
 CartesianIndex(4, 2)  CartesianIndex(4, 2)     CartesianIndex(4, 4)
 CartesianIndex(4, 2)  CartesianIndex(4, 2)     CartesianIndex(4, 4)

距离变换*函数按指定的标签执行变换。

例如,bw_transform[1,1] 的 CartesianIndex(2, 2),该元素的 D[i] 将是 CartesianIndex(1, 1) 和 CartesianIndex(2, 2) 之间的距离,等于 sqrt(2)。

In [ ]:
dist = 1 .- distance_transform(bw_transform)
dist_example = 1 .- distance_transform(bw_transform_example)
Out[0]:
5×5 Matrix{Float64}:
 -0.414214  0.0  0.0  0.0  -0.414214
  0.0       1.0  1.0  1.0   0.0
  0.0       1.0  1.0  1.0   0.0
  0.0       1.0  1.0  1.0   0.0
 -0.414214  0.0  0.0  0.0  -0.414214

label_components 函数在 dist_trans 二进制数组中查找相关的元素。您可以提供一个列表,指明哪些维度用于确定连通性。例如,如果 region = [1,3],则不会检查维度 2 上邻居的连通性。这仅对应于最近邻,即 2 维矩阵中的 4 连通性和 3 维矩阵中的 6 连通性。默认值 region = 1:ndims(A): .输出 label 是一个整数数组,其中 0 表示背景像素,每个连接像素区域都有自己的整数索引。

In [ ]:
dist_trans = dist .< 1
markers = label_components(dist_trans)
markers_example = label_components(dist_example .< 0.5)
Gray.(markers/32.0)
Out[0]:
No description has been provided for this image

函数 watershed 方法使用分水岭变换对图像进行分割。

分水岭变换形成的每个盆地都对应一个片段。在标记矩阵中,0 表示没有标记。如果两个标记的索引相同,它们的区域将合并为一个区域。

In [ ]:
segments = watershed(dist, markers)
segments_example = watershed(dist_example , markers_example)
Out[0]:
Segmented Image with:
  labels map: 5×5 Matrix{Int64}
  number of labels: 1

下图从左到右依次显示了原始图像、标记区域和最终分割后的图像,图像分割按颜色划分。

In [ ]:
labels = labels_map(segments)
colored_labels = IndirectArray(labels, distinguishable_colors(maximum(labels)))
masked_colored_labels = colored_labels .* (1 .- bw)
mosaic(img, colored_labels, masked_colored_labels; nrow=1)
Out[0]:
No description has been provided for this image

输出

在本演示中,我们展示了如何使用图像索引来分割单个图像区域。这些技术可用于从原始图像中分离出感兴趣的区域。