numpy:大量线段/点的快速有规律的平均间隔

时间:2018-09-13 11:12:31

标签: python numpy image-processing interpolation

我沿着一维线有许多(〜一百万个)不规则间隔的点P。这些标记线段,以便如果点是{0,x_a,x_b,x_c,x_d,...},则这些段从0-> x_a,x_a-> x_b,x_b-> x_c,x_c- > x_d等。对于每个片段,我也都有一个y值,我希望将其解释为颜色的深度。我需要将这条线绘制为图像,但是可能只有(比如说)1000个像素可以代表线的整个长度。当然,这些像素对应于沿着线的规则间隔,例如在0..X1,X1..X2,X2..X3等处,其中X1,X2,X3是规则间隔的。要计算每个像素的颜色,我需要取落在规则间隔的像素边界内的所有y值的平均值,并按该间隔内段的长度加权。可能还有一些像素在P中不包含任何值,它们只是采用了跨整个像素的线段所定义的颜色值。

似乎在图像分析中可能需要做很多事情。那么,该操作有一个名称吗?在numpy中,什么是最快的方法来计算这样一组规则的平均y值?我想这有点像插值法,只是我不想只取两个周围点的平均值,而是取规则间隔内所有点的加权平均值(加上一点重叠)。

[编辑-添加了最小的示例]

假设沿水平线有5个段,它们由[0,1.1,2.2,2.3,2.8,4]分隔(即,该线从0到4)。假设每个线段采用任意阴影值,例如,我们可以使用5个阴影值[0,0.88,0.55,0.11,0.44]-其中0为黑色,而1为白色。然后,如果我想使用4个像素进行绘制,则需要创建4个值,分别是0 ... 1、1 ... 2等,并且希望该计算为每个值返回以下值:

0 ... 1 = 0(第一行覆盖0-> 1.1)

1 ... 2 = 0.1 * 0 + 0.9 * 0.88(1 ... 1.1被第一行覆盖,其余被第二行覆盖)

2 ... 3 = 0.2 * 0.88,0.1 * 0.55 + 0.5 * 0.11 + 0.2 * 0.44(第二到第五条线段覆盖

3 ... 4 = 0.44(这由最后一个线段2.8-> 4覆盖)

如果我想将此数据拟合为2像素长的线,则2像素将具有以下值:

0 ... 2 = 1.1 / 2 * 0 + 0.9 / 2 * 0.88

2 ... 4 = 0.2 / 2 * 0.88 + 0.1 / 2 * 0.55 + 0.5 / 2 * 0.11 + 1.2 * 0.44

这似乎是沿着1d线进行下采样的“正确”方法。我正在寻找一种快速实现(最好是内置的)的功能,当我沿线有(例如)一百万个点,并且只有1000个(或大约)像素可以容纳它们时。

2 个答案:

答案 0 :(得分:3)

如您所料,有一个纯粹的numpy解决方案可以解决此问题。诀窍是巧妙地混合np.searchsortednp.add.reduceat来将常规网格放在原始的最近的bin上,以计算这些bin的总和:

import numpy as np

def distribute(x, y, n):
    """
    Down-samples/interpolates the y-values of each segment across a
    domain with `n` points. `x` represents segment endpoints, so should
    have one more element than `y`. 
    """
    y = np.asanyarray(y)
    x = np.asanyarray(x)

    new_x = np.linspace(x[0], x[-1], n + 1)
    # Find the insertion indices
    locs = np.searchsorted(x, new_x)[1:]
    # create a matrix of indices
    indices = np.zeros(2 * n, dtype=np.int)
    # Fill it in
    dloc = locs[:-1] - 1
    indices[2::2] = dloc
    indices[1::2] = locs

    # This is the sum of every original segment a new segment touches
    weighted = np.append(y * np.diff(x), 0)
    sums = np.add.reduceat(weighted, indices)[::2]

    # Now subtract the adjusted portions from the right end of the sums
    sums[:-1] -= (x[dloc + 1] - new_x[1:-1]) * y[dloc]
    # Now do the same for the left of each interval
    sums[1:] -= (new_x[1:-1] - x[dloc]) * y[dloc]

    return new_x, sums / np.diff(new_x)


seg = [0, 1.1, 2.2, 2.3, 2.8, 4]
color = [0, 0.88, 0.55, 0.11, 0.44]

seg, color = distribute(seg, color, 4)
print(seg, color)

结果是

[0. 1. 2. 3. 4.] [0.    0.792 0.374 0.44 ]

这正是您在手动计算中所期望的。

基准

我运行了以下基准测试,以确保EE_'s solution和我的都同意答案,并检查时间安排。我对其他解决方案进行了少许修改,使其具有与我相同的界面:

from scipy.interpolate import interp1d

def EE_(x, y, n):
    I = np.zeros_like(x)
    I[1:] = np.cumsum(np.diff(x) * y)
    f = interp1d(x, I, bounds_error=False, fill_value=(0, I[-1]))
    pix_x = np.linspace(x[0], x[-1], n + 1)
    pix_y = (f(pix_x[1:]) - f(pix_x[:-1])) / (pix_x[1:] - pix_x[:-1])
    return pix_x, pix_y

这是测试平台(方法MadPhysicist仅从上面的distribute函数重命名)。对于x,输入始终为1001个元素,对于y,输入始终为1000个元素。输出数字为5、10、100、1000、10000:

np.random.seed(0x1234ABCD)

x = np.cumsum(np.random.gamma(3.0, 0.2, size=1001))
y = np.random.uniform(0.0, 1.0, size=1000)

tests = (
    MadPhysicist,
    EE_,
)

for n in (5, 10, 100, 1000, 10000):
    print(f'N = {n}')
    results = {test.__name__: test(x, y, n) for test in tests}

    for name, (x_out, y_out) in results.items():
        print(f'{name}:\n\tx = {x_out}\n\ty = {y_out}')

    allsame = np.array([[np.allclose(x1, x2) and np.allclose(y1, y2)
                         for x2, y2 in results.values()]
                        for x1, y1 in results.values()])
    print()
    print(f'Result Match:\n{allsame}')

    from IPython import get_ipython
    magic = get_ipython().magic

    for test in tests:
        print(f'{test.__name__}({n}):\n\t', end='')
        magic(f'timeit {test.__name__}(x, y, n)')

我将跳过数据和协议打印输出(结果相同),并显示时间:

N = 5
MadPhysicist: 50.6 µs ± 349 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
EE_:           110 µs ± 568 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

N = 10
MadPhysicist: 50.5 µs ± 732 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
EE_:           111 µs ± 635 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)   

N = 100
MadPhysicist: 54.5 µs ± 284 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
EE_:           114 µs ± 215 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

N = 1000
MadPhysicist: 107 µs ± 5.73 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
EE_:          148 µs ± 5.11 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

N = 10000
MadPhysicist: 458 µs ± 2.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
EE_:          301 µs ± 4.57 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

您可以看到,在较小的输出尺寸下,numpy解决方案要快得多,这可能是因为开销占主导。但是,在更多的断点处,scipy解决方案变得更快。您必须比较不同的输入大小,才能真正了解时序的计算原理,而不仅仅是不同的输出大小。

答案 1 :(得分:2)

您仍然可以使用线性插值来完成此操作。尽管您的函数是分段恒定的,但您希望在很多小的间隔内获得平均值。从 a b 的时间间隔内某些函数 f x )的平均值只是其整数范围除以 a b 之间的差异。分段常数函数的积分将是分段线性函数。因此,假设您拥有数据:

x = [0, 1.1, 2.2, 2.3, 2.8, 4]
y = [0, 0.88, 0.55, 0.11, 0.44]

制作一个函数,该函数将以任何 x 值给出其积分。在这里,数组I将包含您给定的每个 x 值的积分值,函数f是它的线性内插值,它将在任何一点:

I = numpy.zeros_like(x)
I[1:] = numpy.cumsum(numpy.diff(x) * y)
f = scipy.interpolate.interp1d(x, I)

现在评估每个像素的平均值很容易:

pix_x = numpy.linspace(0, 4, 5)
pix_y = (f(pix_x[1:]) - f(pix_x[:-1])) / (pix_x[1:] - pix_x[:-1])

我们可以检查这些数组中的内容:

>>> pix_x
array([0., 1., 2., 3., 4.])
>>> pix_y
array([0.   , 0.792, 0.374, 0.44 ])

像素的阴影值现在位于pix_y中。这些应该与您在示例中上面给出的值完全匹配。

即使很多点,这也应该相当快:

def test(x, y):
    I = numpy.zeros_like(x)
    I[1:] = numpy.cumsum(numpy.diff(x) * y)
    f = scipy.interpolate.interp1d(x, I,
        bounds_error=False, fill_value=(0, I[-1]))
    pix_x = numpy.linspace(0, 1, 1001)
    pix_y = (f(pix_x[1:]) - f(pix_x[:-1])) / (pix_x[1:] - pix_x[:-1])
    return pix_y

timeit报告:

225 ms ± 37.6 ms per loop
在我的系统上,当x的大小为1000000(y的大小为999999)时。请注意,bounds_error=Falsefill_value=(0, I[-1])被传递到interp1d。这样做的效果是假设您的着色函数在 x 值范围之外为零。另外,interp1d不需要对输入值进行排序;在上面的测试中,我同时给出了 x y 作为0到1之间的统一随机数数组。但是,如果您确定它们是已排序的,则可以通过assume_sorted=True,您应该会提高速度:

20.2 ms ± 377 µs per loop