在地图中找到每个像素的最近邻。

3

我希望你能够用Python找到一种简单的方法,对于二维掩模中的每个像素,都可以获取最近的非零邻居的索引。在Matlab中有一个名为bwdist的函数专门用来实现这个功能。 例如:如果我的输入是:

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

我的输出应该是:
array [[(1,1) (1,1) (1,1) (1,1) (2,5) (2,5) (2,5)]
       [(1,1) (1,1) (1,1) (1,1) (2,5) (2,5) (2,5)]
       [(1,1) (1,1) (1,1) (2,5) (2,5) (2,5) (2,5)]
       [(1,1) (1,1) (1,1) (2,5) (2,5) (2,5) (2,5)]]

该函数还可以像Matlab中的bwdist一样返回绝对索引(对于1维数组),使用scipy中的distance_transform_edt无法得到像素本身,仅能获取到最近像素的距离。此外,我在代码其他部分也使用了OpenCV和VLfeat库。谢谢!

你是否在使用OpenCV或scikit-image? - alkasm
@AlexanderReynolds 是的,我编辑了。这是一个与我正在使用OpenCV处理的图像相关的numpy掩码。 - JunProg
太好了!我会添加OpenCV标签并很快写出答案。 - alkasm
2个回答

7

官方文档:

OpenCV有distanceTransform()distanceTransformWithLabels()函数,它们的工作原理类似,但与这个Matlab函数有些区别。从Matlab docs for bwdist中可以看到:

D = bwdist(BW)计算二进制图像BW的欧几里得距离变换。对于BW中的每个像素,距离变换会分配一个数字,该数字是该像素与BW中最近的非零像素之间的距离。

将此与OpenCV docs for distanceTransformWithLabels()进行比较:

计算源图像中每个像素到最接近的零像素的距离。

因此Matlab给出了最近的非零像素到的距离,而OpenCV给出了最近的零像素到的距离。因此,您需要反转图像以适用于OpenCV。此外,Matlab的可选输出与标签一起提供了与该最近像素相对应的线性索引:

[D,idx] = bwdist(BW)还计算了最接近像素图,并以索引数组idx的形式表示。 idx的每个元素包含BW中最近的非零像素的线性索引。最接近像素图也称为特征图、特征变换或最近邻变换。

使用OpenCV时,输出的标签不是图像的坐标或索引。相反,它只是一个数字标签,类似于连接组件标签,与像素位置/索引无关。

此函数的这个变量不仅计算每个像素(x,y)的最小距离,还识别由零像素组成的最近连接组件(labelType==DIST_LABEL_CCOMP)或最近的零像素(labelType==DIST_LABEL_PIXEL)。

这意味着您将不得不使用此标记图像来遮罩输入并查找与该标记对应的像素(据我所知,这是至少可以做到的最好方法)。

解决方案:

因此,为了理解如何达到我们想要的结果,让我们看一下此函数将我们带到哪里(使用反转图像作为输入,如前所述):

In [138]: img
Out[138]:
array([[  0,   0,   0,   0,   0,   0,   0],
       [  0, 255,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0, 255,   0],
       [  0,   0,   0,   0,   0,   0,   0]], dtype=uint8)

In [139]: dist, labels = cv2.distanceTransformWithLabels(~a, distanceType=cv2.DIST_L2, maskSize=3)

In [140]: print(dist)
[[1.3999939 1.        1.3999939 2.1968994 2.1968994 2.        2.1968994]
 [1.        0.        1.        2.        1.3999939 1.        1.3999939]
 [1.3999939 1.        1.3999939 2.        1.        0.        1.       ]
 [2.1968994 2.        2.1968994 2.1968994 1.3999939 1.        1.3999939]]

In [141]: print(labels)
[[1 1 1 1 2 2 2]
 [1 1 1 1 2 2 2]
 [1 1 1 2 2 2 2]
 [1 1 1 2 2 2 2]]

所以,假如我们只是循环遍历标签中的唯一值,为每个唯一值创建一个掩码,将原始图像进行掩蔽...然后在这个标记的区域内找到白色像素,我们就可以得到索引:

In [146]: for l in np.unique(labels):
     ...:     mask = label == l
     ...:     i = np.where(img * mask)
     ...:     print(i)
     ...:
(array([1]), array([1]))
(array([2]), array([5]))

这不是您要求的精确输出,但这是索引的列表,您已经有了标签。现在我们只需要进行映射。我将创建一个空的双通道矩阵来保存索引值,然后根据标签中的掩码进行填充:

In [177]: index_img = np.zeros((*img.shape, 2), dtype=np.intp)

In [178]: for l in np.unique(labels):
     ...:     mask = label == l
     ...:     index_img[mask] = np.dstack(np.where(img * mask))

以下是包含所需信息的双通道数组。结构有所不同(每个条目不使用元组),但通常这是其他OpenCV函数所需的结构(双通道数组):

In [204]: index_img[:, :, 0]
Out[204]:
array([[1, 1, 1, 1, 2, 2, 2],
       [1, 1, 1, 1, 2, 2, 2],
       [1, 1, 1, 2, 2, 2, 2],
       [1, 1, 1, 2, 2, 2, 2]])

In [205]: index_img[:, :, 1]
Out[205]:
array([[1, 1, 1, 1, 5, 5, 5],
       [1, 1, 1, 1, 5, 5, 5],
       [1, 1, 1, 5, 5, 5, 5],
       [1, 1, 1, 5, 5, 5, 5]])

将所有内容整合在一起

这是一个函数,它可以执行此操作,并具有将两个通道输出或仅像Matlab一样的线性输出的选项:

def bwdist(img, metric=cv2.DIST_L2, dist_mask=cv2.DIST_MASK_5, label_type=cv2.DIST_LABEL_CCOMP, ravel=True):
    """Mimics Matlab's bwdist function.

    Available metrics:
        https://docs.opencv.org/3.4/d7/d1b/group__imgproc__misc.html#gaa2bfbebbc5c320526897996aafa1d8eb
    Available distance masks:
        https://docs.opencv.org/3.4/d7/d1b/group__imgproc__misc.html#gaaa68392323ccf7fad87570e41259b497
    Available label types:
        https://docs.opencv.org/3.4/d7/d1b/group__imgproc__misc.html#ga3fe343d63844c40318ee627bd1c1c42f
    """
    flip = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV)[1]
    dist, labeled = cv2.distanceTransformWithLabels(flip, metric, dist_mask)

    # return linear indices if ravel == True (default)
    if ravel:  
        idx = np.zeros(img.shape, dtype=np.intp)  # np.intp type is for indices
        for l in np.unique(labeled):
            mask = labeled == l
            idx[mask] = np.flatnonzero(img * mask)
        return dist, idx

    # return two-channel indices if ravel == False
    idx = np.zeros((*img.shape, 2), dtype=np.intp)  
    for l in np.unique(labeled):
        mask = labeled == l
        idx[mask] = np.dstack(np.where(img * mask))
    return dist, idx

以下是Matlab文档中提供的示例:

In [241]: bw = np.zeros((5, 5), dtype=np.uint8)
     ...: bw[1, 1] = 1
     ...: bw[3, 3] = 1
     ...: print(bw)
     ...:
[[0 0 0 0 0]
 [0 1 0 0 0]
 [0 0 0 0 0]
 [0 0 0 1 0]
 [0 0 0 0 0]]

In [244]: d, idx = bwdist(bw)

In [245]: print(d)
[[1.3999939 1.        1.3999939 2.1968994 3.1968994]
 [1.        0.        1.        2.        2.1968994]
 [1.3999939 1.        1.3999939 1.        1.3999939]
 [2.1968994 2.        1.        0.        1.       ]
 [3.1968994 2.1968994 1.3999939 1.        1.3999939]]

In [246]: print(idx)
[[ 6  6  6  6 18]
 [ 6  6  6  6 18]
 [ 6  6  6 18 18]
 [ 6  6 18 18 18]
 [ 6 18 18 18 18]]

你帮了我很多,我非常感激。谢谢! :) - JunProg
@junprog 不用担心,这是一个有趣的问题。我很高兴能帮助那些从Matlab转向Python的人! - alkasm

7

使用scipy时,这实际上是一行代码。

如果您的输入矩阵是mat,则最近的非零值的坐标如下:

import scipy.ndimage

nearest_neighbor = scipy.ndimage.morphology.distance_transform_edt(
    mat==0, return_distances=False, return_indices=True)

对于问题中给出的矩阵,这将导致以下索引矩阵,这是正确答案:

[[[1 1 1 1 2 2 2]
  [1 1 1 1 2 2 2]
  [1 1 1 2 2 2 2]
  [1 1 1 2 2 2 2]]

 [[1 1 1 1 5 5 5]
  [1 1 1 1 5 5 5]
  [1 1 1 5 5 5 5]
  [1 1 1 5 5 5 5]]]

索引矩阵的读取方式如下: 点(0,0)最近的邻居是(1,1)。 点(0,4)最近的邻居是(2,5)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接