簡體   English   中英

將二維點雲雙線性加權到網格上的最快方法

[英]Fastest method of bilinear weighting of a 2D point cloud onto a grid

給定一個點(0.8, 0.6)和強度(3) ,可以將單個點雙線性“反向插值”到具有 integer 索引(0,0) -> (1,1)的 2x2 網格上。

點和網格的方向是第一個條目向下增加,第二個條目向右增加。

在這樣的網格上,上面坐標的權重變為:

  0.08 |  0.12  
----------------
  0.32 |  0.48

我們可以乘以與坐標相關的強度,得到一個 2x2 網格雙線性加權強度:

  0.24 |  0.36 
----------------
  0.96 |  1.44

並且可以像這樣繪制:

單雙線性加權

對於幾個點,可以將它們加權到同一個數組上(下面的完整代碼):

points = np.array([(0.8, 0.6), (2.2, 2.6),(5, 1), (3, 4.2), (8.5, 8.2)])
intens = np.array([3, 3, 1, 1, 2])
image, weight = bilinear(points, intens)

加權幾個點

對於我的工作,我需要weightsintensity*weights為 output arrays。 我需要在非常大(數百萬)的坐標上執行上述操作,其中坐標的值從 0.0 到 4095.0。 我在下面編寫了一個 numpy 例程,雖然它在 100_000 個點上相當快(1.25 秒),但我更希望它更快,因為我需要在我擁有的 10_000_000 個數據點上多次調用它。

我考慮對 numpy 代碼進行矢量化而不是 for 循環,但隨后我為每個點生成一個 4096x4096 的大部分空數組,然后我將對其求和。 那將需要 1000 TB 的內存。

我也嘗試過在 cupy 中實現一個簡單的實現,但是由於我使用了 for 循環,它變得太慢了。

在我的代碼中,我為每個點生成一個 2x2 加權數組,將數組乘以強度,然后通過切片將它們添加到主 arrays 中。 有沒有更好的辦法?

import numpy as np

def bilinear(points, intensity):
    """Bilinear weighting of points onto a grid.
    Extent of grid given by min and max of points in each dimension
    points should have shape (N, 2)
    intensity should have shape (N,)
    """
    floor = np.floor(points)
    ceil = floor + 1
    floored_indices = np.array(floor, dtype=int)
    low0, low1 = floored_indices.min(0)
    high0, high1 = floored_indices.max(0)
    floored_indices = floored_indices - (low0, low1)
    shape = (high0 - low0 + 2, high1-low1 + 2)
    
    weights_arr = np.zeros(shape, dtype=float)
    int_arr = np.zeros(shape, dtype=float)
    
    upper_diff = ceil - points
    lower_diff = points - floor
    w1 = np.prod((upper_diff), axis=1)
    w2 = upper_diff[:,0]*lower_diff[:,1]
    w3 = lower_diff[:,0]*upper_diff[:,1]
    w4 = np.prod((lower_diff), axis=1)
    
    for i, index in enumerate(floored_indices):
        s = np.s_[index[0]:index[0]+2, index[1]:index[1]+2]
        weights = np.array([[w1[i], w2[i]], [w3[i], w4[i]]])
        weights_arr[s] += weights
        int_arr[s] += intensity[i]*weights
    return int_arr, weights_arr

rng = np.random.default_rng()
N_points = 10_000 #  use 10_000 so it is quick
image_shape = (256, 256) # Use 256 so it isn't so big

points = rng.random((N_points, 2)) * image_shape
intensity = rng.random(N_points)

image, weight = bilinear(points, intensity)

為了測試代碼,我還提供了以下繪圖代碼 - 僅用於少量(~10)點,否則散點將覆蓋整個圖像。

import matplotlib.pyplot as plt
floor = np.floor(points) - 0.5
lower, left = floor.min(0)
upper, right = (floor).max(0) + 2
extent = (left, right, upper, lower)

fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(6,3))
ax1.scatter(*points[:,::-1].T, c='red')
im1 = ax1.imshow(weight, clim=(image.min(), image.max()), extent=extent)
ax1.set(title='Weight', xlim=(left - 1, right + 1), ylim = (upper + 1, lower - 1))
colorbar(im1)

ax2.scatter(*points[:,::-1].T , c='red')
im2 = ax2.imshow(image, extent=extent)

ax2.set(title='Weight x Intensity', xlim=(left - 1, right + 1), ylim = (upper + 1, lower - 1))
colorbar(im2)
plt.tight_layout()
plt.show()

# If labeling the first point
# ax1.text(*points[0].T, f"({points[0,0]}, {points[0,1]})", va='bottom', ha='center', color='red')
# ax2.text(*points[0].T, f"({points[0,0]}, {points[0,1]}, {intens[0]})", va='bottom', ha='center', color='red')

您想使用np.add.at 見, https://numpy.org/doc/stable/reference/generated/numpy.ufunc.at.html

def bilinear_2(points, intensity):
    # Create empty matrices, starting from 0 to p.max
    w = np.zeros((points[:, 0].max().astype(int) + 2, points[:, 1].max().astype(int) + 2))
    i = np.zeros_like(w)
    
    # Calc weights
    floor = np.floor(points)
    ceil = floor + 1
    upper_diff = ceil - points
    lower_diff = points - floor
    w1 = upper_diff[:, 0] * upper_diff[:, 1]
    w2 = upper_diff[:, 0] * lower_diff[:, 1]
    w3 = lower_diff[:, 0] * upper_diff[:, 1]
    w4 = lower_diff[:, 0] * lower_diff[:, 1]
    
    # Get indices
    ix, iy = floor[:, 0].astype(int), floor[:, 1].astype(int)

    # Use np.add.at. See, https://numpy.org/doc/stable/reference/generated/numpy.ufunc.at.html
    np.add.at(w, (ix, iy), w1)
    np.add.at(w, (ix, iy+1), w2)
    np.add.at(w, (ix+1, iy), w3)
    np.add.at(w, (ix+1, iy+1), w4)

    np.add.at(i, (ix, iy), w1 * intensity)
    np.add.at(i, (ix, iy+1), w2 * intensity)
    np.add.at(i, (ix+1, iy), w3 * intensity)
    np.add.at(i, (ix+1, iy+1), w4 * intensity)
    
    # Clip (to accomodate image size to be the same as your bilinear function)
    iix, iiy = points[:, 0].min().astype(int), points[:, 1].min().astype(int)
    i, w = i[iix:, iiy:], w[iix:, iiy:]
    return i, w

# At 10_000 samples:
%time image, weight = bilinear(points, intensity)
%time image_2, weight_2 = bilinear_2(points, intensity)
>>>
CPU times: user 178 ms, sys: 3.73 ms, total: 182 ms
Wall time: 185 ms
CPU times: user 9.63 ms, sys: 601 µs, total: 10.2 ms
Wall time: 10 ms

# These tests passes
np.testing.assert_allclose(weight, weight_2)
np.testing.assert_allclose(image, image_2)

# At 100K samples
N_points = 100_000
image_shape = (256, 256)
points = rng.random((N_points, 2)) * image_shape
intensity = rng.random(N_points)

%time image_2, weight_2 = bilinear_2(points, intensity)

CPU times: user 115 ms, sys: 66 ms, total: 181 ms
Wall time: 181 ms


# At 10M samples
N_points = 10_000_000
image_shape = (256, 256)
points = rng.random((N_points, 2)) * image_shape
intensity = rng.random(N_points)

%time image_2, weight_2 = bilinear_2(points, intensity)

CPU times: user 8.23 s, sys: 656 ms, total: 8.88 s
Wall time: 9.31 s

除此之外,這種方法是不可能的。 因為 integer 數組索引不會增量更新。

例如,

a = np.zeros(5)
a[np.array((1,1,2))] += 1
a
>>> array([0., 1., 1., 0., 0.])

但;

a = np.zeros(5)
np.add.at(a, ([1,1,2]), 1)
a
>>> array([0., 2., 1., 0., 0.])

感謝@armamut 的好答案,它啟發了我看了一下,我發現np.bincount ,它也在 cupy 中實現。 事實證明 bincount 實現速度更快,而 cupy 實現速度非常快,后者可能會進一步改進。 因為我不得不處理幾個元組才能讓它工作。

# Timings
points = np.random.random((10_000_000, 2)) * (256, 256)
intens = np.random.random((10_000_000))

pcupy = cp.asarray(points)
icupy = cp.asarray(intens)
%time bilinear_bincount_cupy(pcupy, icupy)
%time bilinear_bincount_numpy(points, intens)
%time bilinear_2(points, intens)
Wall time: 456 ms
Wall time: 2.57 s
Wall time: 5.37 s

numpy 實現:

def bilinear_bincount_numpy(points, intensities):
    """Bilinear weighting of points onto a grid.
    Extent of grid given by min and max of points in each dimension
    points should have shape (N, 2)
    intensity should have shape (N,)
    """
    floor = np.floor(points)
    ceil = floor + 1
    floored_indices = np.array(floor, dtype=int)
    low0, low1 = floored_indices.min(0)
    high0, high1 = floored_indices.max(0)
    floored_indices = floored_indices - (low0, low1)
    shape = (high0 - low0 + 2, high1-low1 + 2)

    upper_diff = ceil - points
    lower_diff = points - floor

    w1 = np.prod((upper_diff), axis=1)
    w2 = upper_diff[:,0]*lower_diff[:,1]
    w3 = lower_diff[:,0]*upper_diff[:,1]
    w4 = np.prod((lower_diff), axis=1)

    shifts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
    indices = floored_indices[:, None] + shifts
    indices = (indices * (shape[1], 1)).sum(-1)
    weights = np.array([w1, w2, w3, w4]).T

    weight_bins = np.bincount(indices.flatten(), weights=weights.flatten())
    intens_bins = np.bincount(indices.flatten(), weights=(intensities[:, None]*weights).flatten())

    all_weight_bins = np.zeros(np.prod(shape))
    all_intens_bins = np.zeros(np.prod(shape))

    all_weight_bins[:len(weight_bins)] = weight_bins
    all_intens_bins[:len(weight_bins)] = intens_bins

    weight_image = all_weight_bins.reshape(shape)
    intens_image = all_intens_bins.reshape(shape)
    return intens_image, weight_image

和 cupy 的實現:

def bilinear_bincount_cupy(points, intensities):
    """Bilinear weighting of points onto a grid.
    Extent of grid given by min and max of points in each dimension
    points should be a cupy array of shape (N, 2)
    intensity should be a cupy array of shape (N,)
    """
    floor = cp.floor(points)
    ceil = floor + 1
    floored_indices = cp.array(floor, dtype=int)
    low0, low1 = floored_indices.min(0)
    high0, high1 = floored_indices.max(0)
    floored_indices = floored_indices - cp.array([low0, low1])
    shape = cp.array([high0 - low0 + 2, high1-low1 + 2])

    upper_diff = ceil - points
    lower_diff = points - floor

    w1 = upper_diff[:, 0] * upper_diff[:, 1]
    w2 = upper_diff[:, 0] * lower_diff[:, 1]
    w3 = lower_diff[:, 0] * upper_diff[:, 1]
    w4 = lower_diff[:, 0] * lower_diff[:, 1]

    shifts = cp.array([[0, 0], [0, 1], [1, 0], [1, 1]])
    indices = floored_indices[:, None] + shifts
    indices = (indices * cp.array([shape[1].item(), 1])).sum(-1)
    weights = cp.array([w1, w2, w3, w4]).T

    # These bins only fill up to the highest index - not to shape[0]*shape[1]
    weight_bins = cp.bincount(indices.flatten(), weights=weights.flatten())
    intens_bins = cp.bincount(indices.flatten(), weights=(intensities[:, None]*weights).flatten())
    
    # So we create a zeros array that is big enough
    all_weight_bins = cp.zeros(cp.prod(shape).item())
    all_intens_bins = cp.zeros_like(all_weight_bins)
    # And fill it here
    all_weight_bins[:len(weight_bins)] = weight_bins
    all_intens_bins[:len(weight_bins)] = intens_bins

    weight_image = all_weight_bins.reshape(shape.get())
    intens_image = all_intens_bins.reshape(shape.get())
    return intens_image, weight_image

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM