如何为numpy数组创建一个圆形蒙版?

33

我正在尝试在Python中创建一个圆形遮罩层。我在网上找到了一些示例代码,但是我不确定如何更改数学公式以使得我的圆形在正确的位置。

我有一张图片image_data,类型为numpy.ndarray,形状为(3725, 4797, 3)

total_rows, total_cols, total_layers = image_data.shape
X, Y = np.ogrid[:total_rows, :total_cols]
center_row, center_col = total_rows/2, total_cols/2
dist_from_center = (X - total_rows)**2 + (Y - total_cols)**2
radius = (total_rows/2)**2
circular_mask = (dist_from_center > radius)
我看到这段代码应用了欧几里得距离来计算dist_from_center,但是我不理解X - total_rowsY - total_cols部分。这将产生一个四分之一圆形的掩模,以图像左上角为中心。

XY在圆上扮演什么角色?我该如何修改这段代码,以便产生一个以图像其他位置为中心的掩膜?

那其实不是欧几里得距离。应该是那个总和的平方根。而且,我认为你是对的,应该是center_rowcenter_col而不是total...我认为这段代码会在左上角产生一个四分之一圆形掩模,而不是在你的图像中心的圆形掩模(请注意,在这种情况下,dist_from_center仍然是错误的)。 - alkasm
@AlexanderReynolds 是的,它正在形成一个四分之一圆形掩模,您能否解释一下您是如何确定的?并且您能否用更简单的术语解释 np.ogrid,网上关于 np.ogrid 的所有解释都超出了我的理解范围。 - user7469692
当时我在手机上看到这个问题,所以没有写完整的答案。我想着等我回家的时候肯定会有其他人回答了,但看来并没有。那我就来回答一下吧。 - alkasm
@AlexanderReynolds 谢谢,我会等待您的回复。 - user7469692
3个回答

98
您在网上获取的算法在某种程度上是错误的,至少对于您的目的来说是如此。如果我们有以下图像,我们希望它被掩蔽如下:

Image to mask Masked image

创建这样的遮罩最简单的方法是按照你的算法进行操作,但它并没有按照你想要的方式呈现,也不能轻松地进行修改。我们需要查看图像中每个像素的坐标,并获取一个真/假值,以确定该像素是否在半径内。例如,下面是一张放大的图片,显示了圆形半径和严格在该半径内的像素:

Pixels inside the radius

现在,为了确定哪些像素位于圆内,我们需要知道图像中每个像素的索引。函数np.ogrid()提供了两个向量,分别包含列索引和行索引的像素位置(或索引):其中有一个列向量用于列索引,另一个行向量用于行索引:
>>> np.ogrid[:4,:5]
[array([[0],
       [1],
       [2],
       [3]]), array([[0, 1, 2, 3, 4]])]

这种格式对广播很有用,因为如果我们在某些函数中使用它们,它实际上会创建所有索引的网格,而不仅仅是这两个向量。因此,我们可以使用np.ogrid()来创建图像的索引(或像素坐标),然后检查每个像素坐标是否在圆内或圆外。为了判断它是否在中心内部,我们只需找到从中心到每个像素位置的欧几里得距离,然后如果该距离小于圆的半径,我们将把其标记为包括在掩码中,否则我们将排除它。
现在我们已经拥有了创建这个掩码的所有必要条件。此外,我们将添加一些好的功能;我们可以发送中心和半径,或者让它自动计算它们。
def create_circular_mask(h, w, center=None, radius=None):

    if center is None: # use the middle of the image
        center = (int(w/2), int(h/2))
    if radius is None: # use the smallest distance between the center and image walls
        radius = min(center[0], center[1], w-center[0], h-center[1])

    Y, X = np.ogrid[:h, :w]
    dist_from_center = np.sqrt((X - center[0])**2 + (Y-center[1])**2)

    mask = dist_from_center <= radius
    return mask

在这种情况下,dist_from_center是一个与指定高度和宽度相同的矩阵。它将列索引向量和行索引向量广播到矩阵中,每个位置的值都是距离中心的距离。如果我们将该矩阵可视化为图像(将其缩放到适当的范围内),那么它将成为从我们指定的中心辐射出的渐变:

Distance gradient

因此,当我们将其与半径进行比较时,它等同于对该梯度图像进行阈值处理。

请注意,最终的掩码是一个布尔矩阵;如果该位置在指定中心点的半径内,则为True,否则为False。因此,我们可以将此掩码用作我们关心的像素区域的指示器,或者我们可以取该布尔值的相反值(即numpy中的~)以选择该区域外的像素。因此,像我在本文顶部所做的那样,使用此函数将圆外的像素涂成黑色就非常简单:

h, w = img.shape[:2]
mask = create_circular_mask(h, w)
masked_img = img.copy()
masked_img[~mask] = 0

但是如果我们想在不同于中心的位置创建一个圆形蒙版,我们可以指定它(请注意,该函数期望以x,y顺序而不是索引row, col = y, x顺序提供中心坐标):

center = (int(w/4), int(h/4))
mask = create_circular_mask(h, w, center=center)

鉴于我们没有给出半径,这将为我们提供最大的半径,以使圆仍适合图像边界:

Off-center mask

或者我们可以让它计算中心但使用指定的半径:

radius = h/4
mask = create_circular_mask(h, w, radius=radius)

给我们一个居中的圆,半径不会恰好延伸到最小的尺寸:

Specified radius mask

最后,我们可以指定任何半径和中心点,包括超出图像边界的半径(甚至中心点可以在图像边界之外!):

center = (int(w/4), int(h/4))
radius = h/2
mask = create_circular_mask(h, w, center=center, radius=radius)

Off-center, larger radius mask

你在网上找到的算法相当于将中心设置为(0, 0),半径设置为h
mask = create_circular_mask(h, w, center=(0, 0), radius=h)

Quarter-circle mask


1
+1. 我在顶部添加了 if h % 2 == 0: h=h+1if w % 2 == 0: w=w+1,这样它就不会在偶数上切割最后一列和行。 - el3ien

6
其他答案是可行的,但它们速度较慢,因此我将提出一个使用skimage.draw.disk的答案。使用它更快,而且我发现它很容易使用。只需指定圆的中心和半径,然后使用输出创建掩模即可。
import numpy as np
from skimage.draw import disk
mask = np.zeros((10, 10), dtype=np.uint8)
row = 4
col = 5
radius = 5
# modern scikit uses a tuple for center
rr, cc = disk((row, col), radius)
mask[rr, cc] = 1

3

我希望提供一种不涉及 np.ogrid() 函数的方法来实现这一点。我将剪裁一张名为“robot.jpg”的图片,该图片大小为491 x 491像素。为了可读性,我不会像在真正的程序中那样定义太多变量:

导入库:

import matplotlib.pyplot as plt
from matplotlib import image
import numpy as np

导入图片,我将其称为“z”。这是一张彩色图片,因此我还要提取单个颜色通道。随后,我会显示它:

z = image.imread('robot.jpg')  
z = z[:,:,1]

zimg = plt.imshow(z,cmap="gray")
plt.show()

matplotlib.pyplot 显示的 robot.jpg

为了得到一个包含圆形掩模的 numpy 数组(图像矩阵),我将从以下内容开始:

x = np.linspace(-10, 10, 491)
y = np.linspace(-10, 10, 491)
x, y = np.meshgrid(x, y)
x_0 = -3
y_0 = -6
mask = np.sqrt((x-x_0)**2+(y-y_0)**2)

注意最后一行的圆的方程式,在该方程式中,x_0和y_0定义了一个网格中圆心点的位置,该网格高度和宽度都为491个元素。由于我将网格从x和y的-10到10定义为单位系统,因此x_0和y_0设置了圆在图像中心点的位置。

为了查看其产生的效果,我运行:

maskimg = plt.imshow(mask,cmap="gray")
plt.show()

我们的“原型”遮罩圆

为了将其转换为实际的二进制掩码,我只需将每个像素值低于某个值的像素设置为0,并将每个像素值高于某个值的像素设置为256。该“某个值”将以定义上述单位相同的单位确定圆的半径,因此我将称之为'r'。在这里,我将设置'r'的某个值,然后循环遍历掩码中的每个像素以确定它是否应该是“开”还是“关”:

r = 7
for x in range(0,490):
        for y in range(0,490):
                if mask[x,y] < r:
                        mask[x,y] = 0
                elif mask[x,y] >= r:
                        mask[x,y] = 256

maskimg = plt.imshow(mask,cmap="gray")
plt.show()

这个遮罩

现在我将逐个元素地将该遮罩与图像相乘,然后显示结果:

z_masked = np.multiply(z,mask)

zimg_masked = plt.imshow(z_masked,cmap="gray")
plt.show()

为了翻转掩模,我只需在上面的阈值循环中交换0和256,并且这样做会得到如下结果: 机器人.jpg的遮罩版本

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