web-dev-qa-db-fra.com

Méthode efficace de calcul de la densité de points irrégulièrement espacés

Je tente de générer des images de superposition de carte qui aideraient à identifier les points chauds, c'est-à-dire des zones sur la carte qui ont une densité élevée de points de données. Aucune des approches que j'ai essayées n'est assez rapide pour mes besoins. Remarque: J'ai oublié de mentionner que l'algorithme devrait bien fonctionner sous des scénarios de zoom bas et élevés (ou densité de points de données faible et élevée).

J'ai regardé à travers des bibliothèques engendrées, pyplot et scipes, et le plus proche que je puisse trouver, a été numpy.histogramme2D. Comme vous pouvez le constater dans l'image ci-dessous, la sortie histogramme2D est plutôt brute. (Chaque image comprend des points superposant la chaleur pour une meilleure compréhension)

enter image description here Ma deuxième tentative était de itérer sur tous les points de données, puis de calculer la valeur de point chaud en fonction de la distance. Cela a produit une meilleure image, mais il est trop lent à utiliser dans mon application. Étant donné que c'est O (n), cela fonctionne bien avec 100 points, mais souffle lorsque j'utilise mon jeu de données réel de 30000 points.

Ma dernière tentative était de stocker les données dans une KDTREE et d'utiliser les 5 points les plus proches pour calculer la valeur de point chaud. Cet algorithme est O (1), tant plus rapidement avec un ensemble de données volumineux. Il n'est toujours pas assez rapide, il faut environ 20 secondes pour générer un bitmap de 256x256 et j'aimerais que cela se produise dans environ une seconde fois.

éditer

La solution de lissage BoxSUM fournie par 6502 fonctionne bien à tous les niveaux de zoom et est beaucoup plus rapide que mes méthodes d'origine.

La solution de filtre gaussien suggérée par Luke et Neil G est la plus rapide.

Vous pouvez voir les quatre approches ci-dessous, en utilisant 1000 points de données au total, à un zoom 3X, il y a environ 60 points visibles.

enter image description here

Code complet qui génère mes 3 tentatives d'origine, la solution de lissage de la boîte fournie par 6502 et le filtre gaussien suggéré par Luke (amélioré pour gérer les bords mieux et permettre le zoom avant) est ici:

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()
44
Ivo Bosticky

Cette approche est le long des lignes de certaines réponses précédentes: incrémentez un pixel pour chaque point, puis lisse l'image avec un filtre gaussien. Une image 256x256 fonctionne vers environ 350 ms sur mon ordinateur portable de 6 ans.

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))
29
Luke

Une implémentation très simple qui pourrait être faite (avec c) en temps réel et qui ne prend que des fractions d'une seconde en pure python consiste simplement à calculer le résultat dans l'espace d'écran.

L'algorithme est

  1. Allouer la matrice finale (E.G. 256x256) avec tous les zéros
  2. Pour chaque point dans l'incrément de jeu de données, la cellule correspondante
  3. Remplacez chaque cellule dans la matrice avec la somme des valeurs de la matrice dans une boîte NXN centrée sur la cellule. Répétez cette étape plusieurs fois.
  4. Résultat d'échelle et sortie

Le calcul de la boîte peut être rendu très rapide et indépendant sur N en utilisant une table de somme. Chaque calcul nécessite simplement deux balayages de la matrice ... La complexité totale est O (S + W H p) où S est le nombre de points; W, H sont la largeur et la hauteur de la sortie et P est le nombre de passes de lissage.

Vous trouverez ci-dessous le code pour un pur python=== (également très optimisé); avec 30000 points et une image en niveaux de gris de taille 256x256, le calcul est de 0,5 secondé comprenant une échelle linéaire à 0..255 et la sauvegarde d'un fichier .pgm (n = 5, 4 passes).

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)

Éditer

Bien sûr, la définition même de la densité dans votre cas dépend d'un rayon de résolution ou est la densité juste + inf fois lorsque vous frappez un point et zéro quand vous ne le faites pas?

Ce qui suit est une animation construite avec le programme ci-dessus avec quelques modifications cosmétiques:

  1. utilisé sqrt(average of squared values) au lieu de sum pour le passage de la moyenne
  2. codé couleur les résultats
  3. étirement du résultat pour toujours utiliser l'échelle de couleur complète
  4. des points noirs antialiasés dessinés où les points de données sont
  5. fait une animation en incrémentant le rayon de 2 à 40

La durée totale de calcul des 39 images de l'animation suivante avec cette version cosmétique est de 5,4 secondes avec pypy et 26 secondes avec python standard.

enter image description here

20
6502

histogrammes

La manière histogramme n'est pas la plus rapide et ne peut pas dire la différence entre une séparation arbitrairement petite des points et 2 * sqrt(2) * b (où b est la largeur de la corbeille).

Même si vous construisez les bacs x et les bacs y séparément (O (n)), vous devez toujours effectuer une certaine convolution AB (nombre de bacs à chaque sens), qui est proche de N ^ 2 pour tout système dense, et même plus grand pour un (puits, ab >> n ^ 2 dans un système de plaide.)

En regardant le code ci-dessus, vous semblez avoir une boucle dans grid_density() qui fonctionne sur le nombre de bacs dans Y à l'intérieur d'une boucle du nombre de bacs dans x, c'est pourquoi vous obtenez O (n ^ 2) Performances (bien que si vous commandez déjà n, que vous devez tracer sur différents nombres d'éléments à voir, vous allez simplement avoir à devoir exécuter moins de code par cycle).

Si vous souhaitez une fonction de distance réelle, vous devez commencer à regarder des algorithmes de détection de contact.

Détection de contact

Les algorithmes de détection des contacts naïfs entrent dans O (n ^ 2) dans l'un ou l'autre RAM ou le temps de la CPU, mais il y a un algorithme, à tort ou à tort attribué à Munjiza au Saint-Mary's College London, qui fonctionne en temps linéaire et en bélier.

vous pouvez en lire à ce sujet et la mettre en œuvre vous-même de - son livre , si vous le souhaitez.

J'ai écrit ce code moi-même, en fait

J'ai écrit une implémentation C enveloppée par Python en 2D, qui n'est pas vraiment prête pour la production (elle est encore unique filetée, etc.), mais elle s'exécutera aussi près de O(N) = Au fur et à mesure que votre jeu de données permettra. Vous définissez la "taille de l'élément", qui agit comme une taille de poubelle (le code appellera des interactions sur tout dans b d'un autre point, et parfois entre b et 2 * sqrt(2) * b), donnez-lui un tableau (native python) d'objets avec une propriété X et Y et mon module C rappellera à un python Fonction de votre choix Pour exécuter une fonction d'interaction pour des paires d'éléments appariés. Il est conçu pour la confrontation de contacts de contact des simulations de DEM, mais cela fonctionnera également sur ce problème.

Comme je ne l'ai pas encore publié, car les autres bits de la bibliothèque ne sont pas encore prêts, je vais devoir vous donner un zip de ma source actuelle, mais la partie de détection de contact est solide. Le code est lgpl'd.

Vous aurez besoin de CYTHON et de CA COMPILER pour le faire fonctionner, et cela n'a été testé que sur * NIX Environnemts, si vous êtes sous Windows, vous aurez besoin le compilateur Mingw C pour Cython pour travailler du tout .

Une fois que Cython installé, bâtiment/installation pynet doit être un cas d'exécution Setup.py.

La fonction que vous êtes intéressé est pynet.d2.run_contact_detection(py_elements, py_interaction_function, py_simulation_parameters) (et vous devez consulter l'élément de classes et les simulationsParameters au même niveau si vous souhaitez que la tâche ait moins d'erreurs - regardez dans le fichier à archive-root/pynet/d2/__init__.py Pour voir les implémentations de la classe, ils sont des détenteurs de données triviaux avec des constructeurs utiles.)

(Je mettrai à jour cette réponse avec un repo Mercurial public lorsque le code est prêt pour une version plus générale ...)

4
tehwalrus

Juste une note, le histogram2d La fonction devrait fonctionner correctement pour cela. Avez-vous joué avec différentes tailles de bacs? Votre initiale histogram2d L'intrigue semble simplement utiliser les tailles des bacs par défaut ... mais il n'y a aucune raison d'attendre que les tailles par défaut vous donnent la représentation souhaitée. Cela dit, beaucoup d'autres solutions sont également impressionnantes.

0
ely

Vous pouvez le faire avec une convolution 2D séparable (scipedimage.convolve1d) de votre image d'origine avec un noyau de forme gaussienne. Avec une taille d'image de MXM et une taille de filtre de P, la complexité est O (PM ^ 2) en utilisant un filtrage séparable. La complexité "Big-Oh" est sans doute plus grande, mais vous pouvez profiter des opérations de réseau efficaces de NUMPY qui devraient considérablement accélérer vos calculs.

0
benpro

Votre solution est correcte, mais un problème clair est que vous obtenez des régions sombres malgré un point au milieu d'eux.

Je voudrais plutôt centrer un gaussien N-dimensionnel sur chaque point et évaluer la somme sur chaque point que vous souhaitez afficher. Pour réduire le temps linéaire dans le cas commun, utilisez query_ball_point Pour considérer que des points dans quelques écarts types.

Si vous constatez que Kdtree est vraiment lent, pourquoi ne pas appeler query_ball_point Une fois tous les cinq pixels avec un seuil légèrement plus grand? Cela ne fait pas trop mal pour évaluer quelques gaussiens trop nombreux.

0
Neil G