【问题标题】:Optimize the calculation of horizontal and vertical adjacency using numpy使用numpy优化水平和垂直邻接的计算
【发布时间】:2021-12-19 13:45:16
【问题描述】:

我有以下单元格:

cells = np.array([[1, 1, 1],
                  [1, 1, 0],
                  [1, 0, 0],
                  [1, 0, 1],
                  [1, 0, 0],
                  [1, 1, 1]])

我想计算水平和垂直邻接来得出这个结果:

# horizontal adjacency 
array([[3, 2, 1],
       [2, 1, 0],
       [1, 0, 0],
       [1, 0, 1],
       [1, 0, 0],
       [3, 2, 1]])

# vertical adjacency 
array([[6, 2, 1],
       [5, 1, 0],
       [4, 0, 0],
       [3, 0, 1],
       [2, 0, 0],
       [1, 1, 1]])

实际的解决方案是这样的:

def get_horizontal_adjacency(cells):
    adjacency_horizontal = np.zeros(cells.shape, dtype=int)
    for y in range(cells.shape[0]):
        span = 0
        for x in reversed(range(cells.shape[1])):
            if cells[y, x] > 0:
                span += 1
            else:
                span = 0
            adjacency_horizontal[y, x] = span
    return adjacency_horizontal

def get_vertical_adjacency(cells):
    adjacency_vertical = np.zeros(cells.shape, dtype=int)
    for x in range(cells.shape[1]):
        span = 0
        for y in reversed(range(cells.shape[0])):
            if cells[y, x] > 0:
                span += 1
            else:
                span = 0
            adjacency_vertical[y, x] = span
    return adjacency_vertical

算法基本上是(对于水平邻接):

  1. 循环通过行
  2. 通过列向后循环
  3. 如果单元格的 x、y 值不为零,则在实际跨度上加 1
  4. 如果单元格的 x、y 值为为零,则将实际跨度重置为零
  5. 将跨度设置为结果数组的新 x、y 值

由于我需要在所有数组元素上循环两次,这对于较大的数组(例如图像)来说很慢。

有没有办法使用矢量化或其他一些 numpy 魔法来改进算法?

总结:

joni 和 Mark Setchell 提出了很好的建议!

我创建了一个带有示例图像的small Repo 和一个带有比较的python 文件。结果令人惊讶:

  • 原始方法:3.675 秒
  • 使用 Numba:0.002 秒
  • 使用 Cython:0.005 秒

【问题讨论】:

标签: python numpy performance optimization vectorization


【解决方案1】:

我对 Numba 进行了非常快速的尝试,但并没有彻底检查它,尽管结果似乎是正确的:

#!/usr/bin/env python3

# https://stackoverflow.com/q/69854335/2836621
# magick -size 1920x1080 xc:black -fill white -draw "circle 960,540 960,1040" -fill black -draw "circle 960,540 960,800" a.png

import cv2
import numpy as np
import numba as nb

def get_horizontal_adjacency(cells):
    adjacency_horizontal = np.zeros(cells.shape, dtype=int)
    for y in range(cells.shape[0]):
        span = 0
        for x in reversed(range(cells.shape[1])):
            if cells[y, x] > 0:
                span += 1
            else:
                span = 0
            adjacency_horizontal[y, x] = span
    return adjacency_horizontal

@nb.jit('void(uint8[:,::1], int32[:,::1])',parallel=True)
def nb_get_horizontal_adjacency(cells, result):
    for y in nb.prange(cells.shape[0]):
        span = 0
        for x in range(cells.shape[1]-1,-1,-1):
            if cells[y, x] > 0:
                span += 1
            else:
                span = 0
            result[y, x] = span
    return 

# Load image
im = cv2.imread('a.png', cv2.IMREAD_GRAYSCALE)

%timeit get_horizontal_adjacency(im)

result = np.zeros((im.shape[0],im.shape[1]),dtype=np.int32)
%timeit nb_get_horizontal_adjacency(im, result)

时间安排很好,如果运行正常,速度会提高 4000 倍:

In [15]: %timeit nb_get_horizontal_adjacency(im, result)
695 µs ± 9.12 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [17]: %timeit get_horizontal_adjacency(im)
2.78 s ± 44.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

输入

输入图像以 1080p 尺寸创建,即 1920x1080,ImageMagick 使用:

magick -size 1920x1080 xc:black -fill white -draw "circle 960,540 960,1040" -fill black -draw "circle 960,540 960,800" a.png

输出(对比度调整)

【讨论】:

  • for x in range(cells.shape[1]-1,0,-1): 必须是 range(cells.shape[1]-1,-1,-1)。如果不是,最后一个 x 将是 1 而不是 0
  • @LukasWeber 感谢您的更正 - 我已经更新了答案。有趣的是,这正是我没有彻底检查过的部分!
  • 你能解释一下为什么 nb.prange() 只用在第一个 for 循环中吗?
  • Numba 在您的 CPU 内核上并行化(即分布)任何使用 prange 编写的循环。如果您有 4 个 CPU 内核,它将在每个内核上执行 1/4 的循环。如果你然后在一个内部循环中再做一个prange,你实际上并没有 16 个内核,所以(我认为)进一步拆分事情没有什么意义。我可能是错的并且没有计时,但总的来说,我倾向于使最外层循环并行化,并假设尝试在内部循环中进一步拆分事情几乎没有什么可做的。如果那是错误的,请有人告诉我!!!
  • 我想将 uint8[:, ::1, :] 用于多波段图像还是 uint8[:, :, ::1]?
【解决方案2】:

正如 cmets 中所述,这是一个完美的示例,通过 Cython 或 Numba 重写函数更容易。由于 Mark 已经提供了 Numba 解决方案,让我提供一个 Cython 解决方案。首先,让我们在我的机器上计时他的解决方案,以便进行公平的比较:

In [5]: %timeit nb_get_horizontal_adjacency(im, result)
836 µs ± 36 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

假设图像 im 是 np.ndarray 和 dtype=np.uint8,并行化 Cython 解决方案如下所示:

In [6]: %%cython -f -a -c=-O3 -c=-march=native -c=-fopenmp --link-args=-fopenmp

from cython import boundscheck, wraparound, initializedcheck
from libc.stdint cimport uint8_t, uint32_t
from cython.parallel cimport prange
import numpy as np

@boundscheck(False)
@wraparound(False)
@initializedcheck(False)
def cy_get_horizontal_adjacency(uint8_t[:, ::1] cells):
    cdef int nrows = cells.shape[0]
    cdef int ncols = cells.shape[1]
    cdef uint32_t[:, ::1] adjacency_horizontal = np.zeros((nrows, ncols), dtype=np.uint32)
    cdef int x, y, span
    for y in prange(nrows, nogil=True, schedule="static"):
        span = 0
        for x in reversed(range(ncols)):
            if cells[y, x] > 0:
                span += 1
            else:
                span = 0
            adjacency_horizontal[y, x] = span
    return np.array(adjacency_horizontal, copy=False)

在我的机器上,这几乎快了两倍:

In [7]: %timeit cy_get_horizontal_adjacency(im)
431 µs ± 4.38 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

【讨论】:

  • 酷 - 干得好!
  • 你好乔尼!那是我第一次使用 cython。使用我编译的脚本,您的方法比 Numba 慢一些(请参阅编辑后的问题)。如果我正确编译了所有内容,您可以看看吗? github.com/lukasalexanderweber/…
  • 您应该设置正确的编译器标志。在 Windows 上,# distutils: extra_compile_args=/Ox /arch:AVX2 /openmp 应该位于 *.pyx 文件的顶部。但是,根据我的经验,与 gcc/clang 相比,MSVC 编译器在 SIMD 自动矢量化方面相当糟糕(这就是 gcc/clang 编译器的 -march=native 标志的目的)。因此,如果 Numba 解决方案在 Windows 上更快,我不会感到惊讶。
猜你喜欢
  • 2014-04-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多