高效计算不规则间距点密度的方法

50
我试图生成地图叠加图像,以帮助识别热点区域,即在地图上具有高密度数据点的区域。我尝试过的方法都不够快。 注意:我忘记提到算法应该在低缩放和高缩放情况下(或低和高数据点密度下)都能很好地工作。
我查看了numpy、pyplot和scipy库,找到的最接近的是numpy.histogram2d。如下图所示,histogram2d的输出相当粗糙。(每个图像包含覆盖热力图的点,以便更好地理解)
第二次尝试是迭代所有数据点,然后根据距离计算热点值。这产生了一个外观更好的图像,但在我的应用程序中速度太慢。由于它是O(n),对于100个点可以正常工作,但使用我的实际数据集(30000个点)时会崩溃。
我的最后一次尝试是将数据存储在KDTree中,并使用最近的5个点来计算热点值。这个算法是O(1),因此对于大型数据集更快。它仍然不够快,生成一个256x256位图需要约20秒时间,我希望这个过程在大约1秒钟内完成。
编辑
6502提供的boxsum平滑解决方案在所有缩放级别下都很好,并且比我的原始方法快得多。
Luke和Neil G建议的高斯滤波器解决方案是最快的。
您可以在下面看到所有四种方法,总共使用1000个数据点,在3倍缩放时可见约60个点。

enter image description here

这里是生成我最初的3个尝试、由6502提供的boxsum平滑解决方案以及Luke建议的高斯滤波器(改进了处理边缘并允许放大)的完整代码:

import matplotlib
import numpy as np
from matplotlib.mlab import griddata
import matplotlib.cm as cm
import matplotlib.pyplot as plt
import math
from scipy.spatial import KDTree
import time
import scipy.ndimage as ndi


def grid_density_kdtree(xl, yl, xi, yi, dfactor):
    zz = np.empty([len(xi),len(yi)], dtype=np.uint8)
    zipped = zip(xl, yl)
    kdtree = KDTree(zipped)
    for xci in range(0, len(xi)):
        xc = xi[xci]
        for yci in range(0, len(yi)):
            yc = yi[yci]
            density = 0.
            retvalset = kdtree.query((xc,yc), k=5)
            for dist in retvalset[0]:
                density = density + math.exp(-dfactor * pow(dist, 2)) / 5
            zz[yci][xci] = min(density, 1.0) * 255
    return zz

def grid_density(xl, yl, xi, yi):
    ximin, ximax = min(xi), max(xi)
    yimin, yimax = min(yi), max(yi)
    xxi,yyi = np.meshgrid(xi,yi)
    #zz = np.empty_like(xxi)
    zz = np.empty([len(xi),len(yi)])
    for xci in range(0, len(xi)):
        xc = xi[xci]
        for yci in range(0, len(yi)):
            yc = yi[yci]
            density = 0.
            for i in range(0,len(xl)):
                xd = math.fabs(xl[i] - xc)
                yd = math.fabs(yl[i] - yc)
                if xd < 1 and yd < 1:
                    dist = math.sqrt(math.pow(xd, 2) + math.pow(yd, 2))
                    density = density + math.exp(-5.0 * pow(dist, 2))
            zz[yci][xci] = density
    return zz

def boxsum(img, w, h, r):
    st = [0] * (w+1) * (h+1)
    for x in xrange(w):
        st[x+1] = st[x] + img[x]
    for y in xrange(h):
        st[(y+1)*(w+1)] = st[y*(w+1)] + img[y*w]
        for x in xrange(w):
            st[(y+1)*(w+1)+(x+1)] = st[(y+1)*(w+1)+x] + st[y*(w+1)+(x+1)] - st[y*(w+1)+x] + img[y*w+x]
    for y in xrange(h):
        y0 = max(0, y - r)
        y1 = min(h, y + r + 1)
        for x in xrange(w):
            x0 = max(0, x - r)
            x1 = min(w, x + r + 1)
            img[y*w+x] = st[y0*(w+1)+x0] + st[y1*(w+1)+x1] - st[y1*(w+1)+x0] - st[y0*(w+1)+x1]

def grid_density_boxsum(x0, y0, x1, y1, w, h, data):
    kx = (w - 1) / (x1 - x0)
    ky = (h - 1) / (y1 - y0)
    r = 15
    border = r * 2
    imgw = (w + 2 * border)
    imgh = (h + 2 * border)
    img = [0] * (imgw * imgh)
    for x, y in data:
        ix = int((x - x0) * kx) + border
        iy = int((y - y0) * ky) + border
        if 0 <= ix < imgw and 0 <= iy < imgh:
            img[iy * imgw + ix] += 1
    for p in xrange(4):
        boxsum(img, imgw, imgh, r)
    a = np.array(img).reshape(imgh,imgw)
    b = a[border:(border+h),border:(border+w)]
    return b

def grid_density_gaussian_filter(x0, y0, x1, y1, w, h, data):
    kx = (w - 1) / (x1 - x0)
    ky = (h - 1) / (y1 - y0)
    r = 20
    border = r
    imgw = (w + 2 * border)
    imgh = (h + 2 * border)
    img = np.zeros((imgh,imgw))
    for x, y in data:
        ix = int((x - x0) * kx) + border
        iy = int((y - y0) * ky) + border
        if 0 <= ix < imgw and 0 <= iy < imgh:
            img[iy][ix] += 1
    return ndi.gaussian_filter(img, (r,r))  ## gaussian convolution

def generate_graph():    
    n = 1000
    # data points range
    data_ymin = -2.
    data_ymax = 2.
    data_xmin = -2.
    data_xmax = 2.
    # view area range
    view_ymin = -.5
    view_ymax = .5
    view_xmin = -.5
    view_xmax = .5
    # generate data
    xl = np.random.uniform(data_xmin, data_xmax, n)    
    yl = np.random.uniform(data_ymin, data_ymax, n)
    zl = np.random.uniform(0, 1, n)

    # get visible data points
    xlvis = []
    ylvis = []
    for i in range(0,len(xl)):
        if view_xmin < xl[i] < view_xmax and view_ymin < yl[i] < view_ymax:
            xlvis.append(xl[i])
            ylvis.append(yl[i])

    fig = plt.figure()


    # plot histogram
    plt1 = fig.add_subplot(221)
    plt1.set_axis_off()
    t0 = time.clock()
    zd, xe, ye = np.histogram2d(yl, xl, bins=10, range=[[view_ymin, view_ymax],[view_xmin, view_xmax]], normed=True)
    plt.title('numpy.histogram2d - '+str(time.clock()-t0)+"sec")
    plt.imshow(zd, origin='lower', extent=[view_xmin, view_xmax, view_ymin, view_ymax])
    plt.scatter(xlvis, ylvis)


    # plot density calculated with kdtree
    plt2 = fig.add_subplot(222)
    plt2.set_axis_off()
    xi = np.linspace(view_xmin, view_xmax, 256)
    yi = np.linspace(view_ymin, view_ymax, 256)
    t0 = time.clock()
    zd = grid_density_kdtree(xl, yl, xi, yi, 70)
    plt.title('function of 5 nearest using kdtree\n'+str(time.clock()-t0)+"sec")
    cmap=cm.jet
    A = (cmap(zd/256.0)*255).astype(np.uint8)
    #A[:,:,3] = zd  
    plt.imshow(A , origin='lower', extent=[view_xmin, view_xmax, view_ymin, view_ymax])
    plt.scatter(xlvis, ylvis)

    # gaussian filter
    plt3 = fig.add_subplot(223)
    plt3.set_axis_off()
    t0 = time.clock()
    zd = grid_density_gaussian_filter(view_xmin, view_ymin, view_xmax, view_ymax, 256, 256, zip(xl, yl))
    plt.title('ndi.gaussian_filter - '+str(time.clock()-t0)+"sec")
    plt.imshow(zd , origin='lower', extent=[view_xmin, view_xmax, view_ymin, view_ymax])
    plt.scatter(xlvis, ylvis)

    # boxsum smoothing
    plt3 = fig.add_subplot(224)
    plt3.set_axis_off()
    t0 = time.clock()
    zd = grid_density_boxsum(view_xmin, view_ymin, view_xmax, view_ymax, 256, 256, zip(xl, yl))
    plt.title('boxsum smoothing - '+str(time.clock()-t0)+"sec")
    plt.imshow(zd, origin='lower', extent=[view_xmin, view_xmax, view_ymin, view_ymax])
    plt.scatter(xlvis, ylvis)

if __name__=='__main__':
    generate_graph()
    plt.show()

1
我曾经遇到过同样的问题。我的代码与你的片段非常相似。我的瓶颈在于for循环迭代。通过使用numpy重写代码后,我加速了100倍。例如:for i in np.arange(1e5): x[i] =+ 1应该比x =+ 1慢,因为后者是numpy,而前者并没有真正利用numpy的加速优势。 - ahelm
3
如果我没理解错的话,您是想说 O(n^2) 和 O(n),而不是 O(n) 和 O(1)?如果我理解有误,请纠正我。 - GeneralBecos
我注意到在ndi高斯滤波器版本中可能存在一些位置偏移:如果您看左边缘中间附近两个紧密垂直对齐的点,彩色区域的错位非常明显。 - 6502
从可视化结果来看,似乎boxsum平滑方法是最好的?例如对于高斯滤波器,图像顶部有一个强烈的热区,而周围只有几个点。 - Jim Raynor
6个回答

31

这种方法与之前的一些答案类似:为每个点递增一个像素,然后使用高斯滤波器平滑图像。在我的6年老笔记本上,一个256x256的图像运行大约需要350毫秒。

import numpy as np
import scipy.ndimage as ndi

data = np.random.rand(30000,2)           ## create random dataset
inds = (data * 255).astype('uint')       ## convert to indices

img = np.zeros((256,256))                ## blank image
for i in xrange(data.shape[0]):          ## draw pixels
    img[inds[i,0], inds[i,1]] += 1

img = ndi.gaussian_filter(img, (10,10))

2
我感到有必要指出,五年后,在for循环内部的缓慢部分可以使用numpy.histogram2d()更快地完成。 - Luke
在使用Python 2.7和numpy 1.12.1时,我在第img[data[i,0], data[i,1]] += 1行遇到了错误IndexError:only integers, slices (:), ellipsis (...), numpy.newaxis (None ) and integer or boolean arrays are valid indices - uzair_syed
1
NumPy最近加强了对索引的类型要求。问题已经得到修复。 - Luke

21

用C语言可以实时地执行此操作,而在纯Python中只需几分之一秒即可完成非常简单的实现,只需在屏幕空间中计算结果。

该算法包括以下步骤:

  1. 分配最终矩阵(例如256x256),其中所有元素均为0。
  2. 对于数据集中的每个点,将相应的单元格加1。
  3. 将矩阵中的每个单元格替换为该单元格中心的NxN框内矩阵值的总和。重复此步骤几次。
  4. 缩放结果并输出。

利用一个求和表格可以快速地计算盒子总和,并且与N无关。每次计算只需要扫描矩阵两次... 总复杂度是O(S + WHP),其中S是点数;W,H是输出宽度和高度,P是平滑处理的次数。

以下是纯Python实现的代码(也非常未优化);使用30000个点和256x256输出灰度图像,包括线性缩放到0..255和保存.pgm文件,计算时间为0.5秒(N = 5,4次循环)。

def boxsum(img, w, h, r):
    st = [0] * (w+1) * (h+1)
    for x in xrange(w):
        st[x+1] = st[x] + img[x]
    for y in xrange(h):
        st[(y+1)*(w+1)] = st[y*(w+1)] + img[y*w]
        for x in xrange(w):
            st[(y+1)*(w+1)+(x+1)] = st[(y+1)*(w+1)+x] + st[y*(w+1)+(x+1)] - st[y*(w+1)+x] + img[y*w+x]
    for y in xrange(h):
        y0 = max(0, y - r)
        y1 = min(h, y + r + 1)
        for x in xrange(w):
            x0 = max(0, x - r)
            x1 = min(w, x + r + 1)
            img[y*w+x] = st[y0*(w+1)+x0] + st[y1*(w+1)+x1] - st[y1*(w+1)+x0] - st[y0*(w+1)+x1]

def saveGraph(w, h, data):
    X = [x for x, y in data]
    Y = [y for x, y in data]
    x0, y0, x1, y1 = min(X), min(Y), max(X), max(Y)
    kx = (w - 1) / (x1 - x0)
    ky = (h - 1) / (y1 - y0)

    img = [0] * (w * h)
    for x, y in data:
        ix = int((x - x0) * kx)
        iy = int((y - y0) * ky)
        img[iy * w + ix] += 1

    for p in xrange(4):
        boxsum(img, w, h, 2)

    mx = max(img)
    k = 255.0 / mx

    out = open("result.pgm", "wb")
    out.write("P5\n%i %i 255\n" % (w, h))
    out.write("".join(map(chr, [int(v*k) for v in img])))
    out.close()

import random

data = [(random.random(), random.random())
        for i in xrange(30000)]

saveGraph(256, 256, data)

编辑

当然,在你的情况下,密度的定义取决于分辨率半径,或者当你击中一个点时密度是否为+inf,而不击中时则为零?

以下是使用上述程序构建的动画,只做了一些外观上的更改:

  1. 对于平均处理,使用sqrt(平方值的平均数)而不是sum
  2. 对结果进行着色
  3. 将结果拉伸以始终使用完整的颜色比例
  4. 在数据点处绘制抗锯齿黑点
  5. 通过从2到40递增半径来创建动画

使用此美化版本的以下动画的39个帧的总计算时间为PyPy 5.4秒,标准Python需要26秒。

输入图像描述


1
@Patros:问题是计算加权和需要对每个单元格进行一个NxN的循环。使用单个正方形时,没有内部循环,并且使用总和表每个单元格的计算为O(1)。通过多次传递近似加权和的结果,实际上在很少的传递后输出已经平滑了。一次传递后输出是“step”函数,两次后是“linear”,三次后是“quadratic”,以此类推... - 6502
@6502 啊,没错。迭代方法更具可扩展性,特别是如果你对相对较少的迭代次数得到的结果感到满意,而不是N^2。很好。 - patros
谢谢你的回答,我相信它在数据密集的情况下效果很好,但是在低密度或高缩放级别下,它并不能产生我想要的结果。我会更新上面的问题,明确指出这需要在高缩放级别下工作。 - Ivo Bosticky
经过第一轮的boxsum之后,图像中给定像素的值表示其邻域内(根据半径)包含的点数。但是,在多次传递后,除了按照第一次传递进行归一化外,我们如何保持这个“物理”含义呢? - floflo29
1
@floflo29:第一次通行是一个盒子总和,你也可以看作是一个带有正方形窗口的加权总和。第二遍是一个带有两倍盒子大小三角形形状的加权总和。第三遍是一个带有二次σ形钟形曲线的加权平均值,其支持面积为原始盒子的三倍。继续这样做,你会越来越接近一个带有高斯函数(具有无限支持)的加权总和。 - 6502
显示剩余5条评论

4

直方图

直方图的方式不是最快的,也无法区分点的任意小间隔和2 * sqrt(2) * b(其中b是条形图宽度)之间的差异。

即使您单独构建x轴和y轴的条形图(O(N)),您仍然必须执行一些卷积运算(每个方向的条形图数量),对于任何密集系统而言,这接近于N^2,对于稀疏系统来说更大(在稀疏系统中,ab >> N^2)。

从上面的代码来看,您似乎在grid_density()中有一个循环,该循环在x轴的bin数内运行y轴的bin数,这就是为什么您会获得O(N^2)性能的原因(尽管如果您已经按顺序排列了N个元素,您应该在不同数量的元素上绘制以查看,那么您每个周期只需要运行较少的代码)。

如果您想要实际的距离函数,那么您需要开始查看接触检测算法。

接触检测

天真的接触检测算法在RAM或CPU时间上都是O(N^2),但是有一种算法被归功于伦敦圣玛丽学院的Munjiza,它可以在线性时间和RAM中运行。如果您愿意,可以从他的书中了解并自己实现它。
事实上,我自己编写了这段代码。
我已经用Python封装了这个2D的C实现,但它还没有准备好投入生产(它仍然是单线程的等),但它将在您的数据集允许的情况下以接近O(N)的速度运行。您设置“元素大小”,它作为bin大小(代码将在另一个点的b内调用交互,并且有时在b 2 * sqrt(2) * b 之间),给它一个带有x和y属性的对象数组(本机Python列表),我的C模块将回调到您选择的Python函数,以运行匹配对元素的交互函数。它旨在运行接触力DEM模拟,但在这个问题上也可以很好地工作。
由于库中的其他部分尚未准备好,因此我还没有发布它,但我会给您提供我的当前源代码的压缩包,其中接触检测部分是可靠的。该代码采用LGPL许可证。
要使其工作,您需要安装Cython和C编译器,并且仅在*nix环境下进行过测试和工作。如果您使用Windows,则需要 mingw C编译器才能使Cython正常工作
一旦安装了Cython,构建/安装pynet应该只需要运行setup.py即可。
您感兴趣的函数是pynet.d2.run_contact_detection(py_elements, py_interaction_function, py_simulation_parameters)(如果您希望它不出现太多错误,请同时查看Element和SimulationParameters类 - 在archive-root/pynet/d2/__init__.py文件中查看类实现,它们是有用的数据持有者,具有有用的构造函数)。

当代码准备好更广泛地发布时,我会更新这个答案并提供一个公共的Mercurial存储库...


谢谢。你有最新的代码吗? - John Singh
抱歉,此代码从未被允许公开发布 :( - tehwalrus

0

你的解决方案还可以,但一个明显的问题是,尽管在它们中间有一个点,但你仍然得到了暗区域。

相反,我会在每个点上居中一个n维高斯分布,并在要显示的每个点上求和。为了将其减少到常见情况下的线性时间,请使用query_ball_point仅考虑几个标准偏差内的点。

如果您发现KDTree非常慢,为什么不使用稍大的阈值每五个像素调用一次query_ball_point?评估过多的高斯函数并不会对结果造成太大影响。


0
您可以使用二维可分离卷积(scipy.ndimage.convolve1d)和高斯核对原始图像进行处理。当图像大小为MxM,滤波器大小为P时,使用可分离滤波的复杂度为O(PM^2)。 "大O"复杂度无疑更大,但您可以利用numpy的高效数组操作来加速计算。

0

仅供参考,histogram2d函数应该可以很好地解决这个问题。你尝试过不同的bin大小吗?你最初的histogram2d图似乎只使用了默认的bin大小...但是没有理由期望默认大小给你想要的表示。话虽如此,其他解决方案也很令人印象深刻。


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