# 生成图像中所有像素的 (y, x) 坐标网格
# 例如,对于 480x640 图像,xx 从 0 到 639,yy 从 0 到 479
# indexing='ij' 使得 yy 对应行,xx 对应列
yy, xx = np.meshgrid(np.arange(img_size[0]), np.arange(img_size[1]), indexing='ij')
# 将 (yy, xx) 展平为 (num_pixels, 2) 的列表,每个元素是 (y_pixel, x_pixel)
pixel_coords = np.vstack([yy.ravel(), xx.ravel()]).T 

这行代码非常经典,在计算机视觉和图像处理中,当需要对每个像素的坐标进行操作时,几乎都会用到它。它的核心目标是将两个表示坐标的二维网格矩阵 (yyxx),转换成一个更方便使用的、N行2列的像素坐标列表。

为了方便理解,我们不用 480x640 那么大的图像,而是用一个非常小的 3x4(3行4列)的图像来举例。

img_size = (3, 4)


第零步:准备工作 - xxyy 的初始形态

在执行这行代码之前,我们先看看 yyxx 是什么样子的。根据您给出的前文代码:

import numpy as np
img_size = (3, 4)
# indexing='ij' 使得 yy 的形状是(3, 4),xx 的形状也是(3, 4)
yy, xx = np.meshgrid(np.arange(img_size[0]), np.arange(img_size[1]), indexing='ij')

# np.arange(3) -> [0, 1, 2]  (y坐标)
# np.arange(4) -> [0, 1, 2, 3]  (x坐标)

meshgrid 会生成两个矩阵:

yy 矩阵 (存储每个点的y坐标):

[[0, 0, 0, 0],
 [1, 1, 1, 1],
 [2, 2, 2, 2]]

xx 矩阵 (存储每个点的x坐标):

[[0, 1, 2, 3],
 [0, 1, 2, 3],
 [0, 1, 2, 3]]

现在,我们有了这两个 (3, 4) 形状的矩阵,开始对 np.vstack([yy.ravel(), xx.ravel()]).T 进行分步拆解。


第一步:.ravel() - 展平操作

ravel() 是 NumPy 的一个函数,它的作用是将一个多维数组“展平”或“拉直”成一个一维数组。它会按照“C语言风格”的顺序(逐行)来读取元素。

  • yy.ravel() 的结果:
    它会先读取 yy 的第一行 [0, 0, 0, 0],然后是第二行 [1, 1, 1, 1],最后是第三行 [2, 2, 2, 2],然后将它们拼接成一个一维数组。

    # yy.ravel() ->
    [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]  # 长度为 3*4 = 12
    
  • xx.ravel() 的结果:
    同理,它会逐行读取 xx 的元素。

    # xx.ravel() ->
    [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]  # 长度为 3*4 = 12
    

现在,我们得到了两个长度为12的一维数组。


第二步:np.vstack([...]) - 垂直堆叠

np.vstack (vertical stack) 的作用是将传入的数组在垂直方向上堆叠起来,形成一个新的、更高维度的数组。

我们传入的参数是 [yy.ravel(), xx.ravel()],这是一个包含两个一维数组的列表。vstack 会将第一个数组作为新矩阵的第一行,第二个数组作为第二行

# np.vstack([
#     [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2],  # yy.ravel()
#     [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]   # xx.ravel()
# ])

执行 np.vstack 后的结果是一个 2行12列 的二维矩阵:

[[0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2],
 [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]]

观察这个矩阵,我们可以发现,它的每一列 [y, x] 都代表了一个像素的坐标。例如,第五列是 [1, 0],代表坐标 (y=1, x=0) 的像素。

但是,我们通常希望的格式是每一代表一个像素坐标,即 [[y1, x1], [y2, x2], ...]。这就引出了最后一步。


第三步:.T - 转置操作

.T 是 NumPy 数组的一个属性,用于对矩阵进行转置(Transpose),即交换矩阵的行和列。

我们将上一步得到的 (2, 12) 矩阵进行转置,就会得到一个 12行2列 的矩阵。

# 对上一步的矩阵进行 .T 操作
[[0, 0],   # 原来是第1列
 [0, 1],   # 原来是第2列
 [0, 2],
 [0, 3],
 [1, 0],   # 原来是第5列
 [1, 1],
 [1, 2],
 [1, 3],
 [2, 0],
 [2, 1],
 [2, 2],
 [2, 3]]  # 原来是第12列

最终结果

现在我们得到的这个最终矩阵 pixel_coords,它的形状是 (12, 2),也就是 (像素总数, 2)

它的每一行都精确地对应了图像中一个像素的 (y, x) 坐标,并且是按照从左到右、从上到下的顺序排列的。

总结

所以,pixel_coords = np.vstack([yy.ravel(), xx.ravel()]).T 这行代码的完整工作流程是:

  1. ravel():将存储y坐标和x坐标的两个二维网格拉直成两个一维长向量。
  2. vstack():将这两个一维向量垂直堆叠成一个2xN的矩阵,其中第一行是所有y坐标,第二行是所有x坐标。
  3. .T:将这个2xN的矩阵转置成一个Nx2的矩阵,使得每一行都变成一个 (y, x) 坐标对。

通过这一系列优雅的操作,我们高效地创建了一个包含图像所有像素坐标的列表,为后续的批量计算和坐标变换提供了极大的便利。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐