Générez des points dans plusieurs entités en tenant compte de la distance minimale

8

J'ai une fonction qui crée des éoliennes représentées par des points. Essentiellement, il utilise le code des points aléatoires à l'intérieur de l' outil polygones (fixe) , mais avec quelques légères modifications.

L'objectif est de créer des points aléatoires à l'intérieur des polygones en tenant compte de la distance minimale spécifiée. Cela fonctionne très bien surtout avec des polygones qui ne sont pas proches les uns des autres (par exemple un seul polygone):

Exemple

Cependant, si le polygone est proche ou adjacent à un autre polygone (par exemple, comme indiqué ci-dessous), les points de chaque polygone peuvent être dans la distance minimale comme indiqué en rouge:

Exemple de problème

Comment pourrais-je changer le code pour que ces points en rouge ne soient pas proches les uns des autres d'un polygone voisin?

Idéalement, je souhaite que plusieurs points soient remplacés par un seul:

Résultat


Voici le code qui peut être reproduit dans la console Python , une couche polygonale doit être sélectionnée avec un CRS approprié avant d'exécuter la fonction:

import random
from PyQt4.QtCore import QVariant

def checkMinDistance(point, index, distance, points):
    if distance == 0:
        return True
    neighbors = index.nearestNeighbor(point, 1)
    if len(neighbors) == 0:
        return True
    if neighbors[0] in points:
        np = points[neighbors[0]]
        if np.sqrDist(point) < (distance * distance):
            return False
    return True

def generate_wind_turbines(spacing):
    layer = iface.activeLayer()
    crs = layer.crs()
    # Memory layer
    memory_lyr = QgsVectorLayer("Point?crs=epsg:" + unicode(crs.postgisSrid()) + "&index=yes", "Wind turbines for " + str(layer.name()), "memory")
    QgsMapLayerRegistry.instance().addMapLayer(memory_lyr)
    memory_lyr.startEditing()
    provider = memory_lyr.dataProvider()
    provider.addAttributes([QgsField("ID", QVariant.Int)])
    # Variables
    point_density = 0.0001
    fid = 1
    distance_area = QgsDistanceArea()
    # List of features
    fts = []
    # Create points
    for f in layer.getFeatures():
        fGeom = QgsGeometry(f.geometry())
        bbox = fGeom.boundingBox()
        pointCount = int(round(point_density * distance_area.measure(fGeom)))
        index = QgsSpatialIndex()
        points = dict()
        nPoints = 0
        fid += 1
        nIterations = 0
        maxIterations = pointCount * 200
        random.seed()
        while nIterations < maxIterations and nPoints < pointCount:
            rx = bbox.xMinimum() + bbox.width() * random.random()
            ry = bbox.yMinimum() + bbox.height() * random.random()
            pnt = QgsPoint(rx, ry)
            geom = QgsGeometry.fromPoint(pnt)
            if geom.within(fGeom) and checkMinDistance(pnt, index, spacing, points):
                f = QgsFeature(nPoints)
                f.setAttributes([fid])
                f.setGeometry(geom)
                fts.append(f)
                index.insertFeature(f)
                points[nPoints] = pnt
                nPoints += 1
            nIterations += 1
    provider.addFeatures(fts)
    memory_lyr.updateFields()
    memory_lyr.commitChanges()

generate_wind_turbines(500)

Éditer:

La dissolution et / ou la conversion des polygones en parties uniques ne semble pas beaucoup aider car les points générés semblent toujours se situer dans la distance minimale.

Testé sur QGIS 2.18.3 .

Joseph
la source
1
si c'est une option: Avez-vous essayé d'utiliser un polygone en plusieurs parties comme polygone d'entrée?
LaughU
@LaughU - Je viens de le tester avec un polygone en plusieurs parties mais aucun point n'est généré. Ce serait une option pour convertir les entités à partie unique en plusieurs parties si le code peut être modifié pour fonctionner avec des polygones en plusieurs parties.
Joseph
Avez-vous pensé à dissoudre les polygones, puis à créer plusieurs parties en une seule partie dans une couche temporaire que vous utilisez pour la génération de points? Vous pouvez également utiliser un tampon négatif pour le calque temporaire pour éviter les chevauchements de symboles sur les bords.
Mat
@Matte - Merci, j'ai déjà essayé de dissoudre tous les polygones. Je l'ai réessayé et converti en pièce unique (je ne sais pas si cela fait quelque chose car il s'agissait déjà d'une seule entité) mais certains points près des bords sont à l'intérieur de points dans les autres polygones. Je voudrais éviter d'utiliser des tampons négatifs car je veux permettre aux points d'être près des bords :)
Joseph

Réponses:

5

Vous devez changer deux choses pour que cela fonctionne. Cependant, vous n'obtiendrez pas le maximum d'éoliennes par zone. Pour cela, vous devrez exécuter quelques itérations pour chaque valeur et obtenir le nombre maximal de points.

J'ai déplacé le index = QgsSpatialIndex()et points = dict()à l'extérieur de la boucle for. Le code ressemblerait à ceci:

import random
def generate_wind_turbines(spacing):
    layer = self.iface.activeLayer()
    crs = layer.crs()
    # Memory layer
    memory_lyr = QgsVectorLayer("Point?crs=epsg:" + unicode(crs.postgisSrid()) + "&index=yes", "Wind turbines for " + str(layer.name()), "memory")
    QgsMapLayerRegistry.instance().addMapLayer(memory_lyr)
    memory_lyr.startEditing()
    provider = memory_lyr.dataProvider()
    provider.addAttributes([QgsField("ID", QVariant.Int)])
    # Variables
    point_density = 0.0001
    fid = 1
    distance_area = QgsDistanceArea()
    # List of features
    fts = []
    # Create points
    points = dict() # changed from me 
    index = QgsSpatialIndex()# changend from me 
    nPoints = 0 # changed in the edit 
    pointCount = 0 # changed in the edit 

    for f in layer.getFeatures():
        fGeom = QgsGeometry(f.geometry())
        bbox = fGeom.boundingBox()
        # changed here as well 
        pointCount = int(round(point_density * distance_area.measure(fGeom))) + int(pointCount)
        fid += 1
        nIterations = 0
        maxIterations = pointCount * 200
        random.seed()
        while nIterations < maxIterations and nPoints < pointCount:
            rx = bbox.xMinimum() + bbox.width() * random.random()
            ry = bbox.yMinimum() + bbox.height() * random.random()
            pnt = QgsPoint(rx, ry)
            geom = QgsGeometry.fromPoint(pnt)
            if geom.within(fGeom) and checkMinDistance(pnt, index, spacing, points):
                f = QgsFeature(nPoints)
                f.setAttributes([fid])
                f.setGeometry(geom)
                fts.append(f)
                index.insertFeature(f)
                points[nPoints] = pnt
                nPoints += 1
            nIterations += 1
    provider.addFeatures(fts)
    memory_lyr.updateFields()
    memory_lyr.commitChanges()

def checkMinDistance( point, index, distance, points):
    if distance == 0:
        return True
    neighbors = index.nearestNeighbor(point, 1)
    if len(neighbors) == 0:
        return True
    if neighbors[0] in points:
        np = points[neighbors[0]]
        if np.sqrDist(point) < (distance * distance):
            return False
    return True

ÉDITER:

Joseph avait raison. mes changements ne fonctionnaient que pour une zone vraiment petite. J'ai testé et trouvé une nouvelle solution en déplaçant deux variables hors de la boucle for et en changeant la pointCountvariable.

Je l'ai testé à 500m et voici le résultat (deux essais différents):

entrez la description de l'image ici

Rire
la source
1
Bien, merci de l'avoir remarqué! Je suis sûr que cela améliore considérablement l'efficacité =)
Joseph
1
@Joseph, tu avais raison. Je l'ai testé avec de petites zones qui fonctionnaient. J'ai ajouté quelques changements mineurs et cela fonctionne pour moi maintenant. Faites-moi savoir si elle doit encore être améliorée :)
LaughU
1
Intéressant! Je vais tester cela demain et faire un rapport mais le résultat semble vraiment bon;)
Joseph
1
Ouais, c'est bon! Votre script est également plus rapide et fournit toujours les résultats que je cherchais. Vous accordera éventuellement la prime, j'espère que cela ne vous dérange pas, mais j'ai légèrement modifié votre code;)
Joseph
1
@Joseph Non, cela ne me dérange pas;) Je testais avec le code et j'ai oublié de nettoyer le gâchis alias espaces blancs
LaughU
4

Une méthode pourrait être de créer une autre fonction qui effectue les opérations suivantes:

  1. Générez des tampons avec le même rayon que l'espacement autour de tous les points et stockez-les dans une couche polygonale.
  2. Créez une liste contenant l'id de tous les tampons qui se croisent.
  3. Supprime les tampons utilisant cette liste d'ID.
  4. Générez le centroïde des tampons restants et stockez-les dans une couche de points.

Voici la fonction que j'ai utilisée:

def wind_turbine_spacing_checker(layer, spacing):
    # Create buffers for points
    poly_layer =  QgsVectorLayer("Polygon?crs=epsg:27700", 'Buffers' , "memory")
    pr = poly_layer.dataProvider() 
    pr.addAttributes([QgsField("ID", QVariant.Int)])
    feat_list = []
    for f in layer.getFeatures():
        poly = QgsFeature()
        f_buffer = f.geometry().buffer((spacing / 2), 99)
        f_poly = poly.setGeometry(QgsGeometry.fromPolygon(f_buffer.asPolygon()))
        poly.setAttributes([1])
        poly.setGeometry(f_buffer)
        feat_list.append(poly)

    pr.addFeatures(feat_list)
    poly_layer.updateExtents()
    poly_layer.updateFields()
    QgsMapLayerRegistry.instance().addMapLayers([poly_layer])

    # Get pairs of intersecting buffer features
    features = [feat for feat in poly_layer.getFeatures()]
    ids = []
    for feat in poly_layer.getFeatures():
        for geat in features:
            if feat.id() != geat.id():
                if geat.geometry().intersects(feat.geometry()):
                    ids.append([feat.id(), geat.id()])

    # Set/sort list and get id of intersecting feature(s)
    for x in ids:
        x.sort()

    ids_sort = set(tuple(x) for x in ids)
    ids_list = [list(x) for x in ids_sort]
    ids_firstItem = [item[0] for item in ids_list]
    final_list = list(set(ids_firstItem))

    # Use ids from final_list to remove intersecting buffer features
    with edit(poly_layer):
        poly_layer.deleteFeatures(final_list)

    # Create new point layer and get centroids from buffers
    # (using final_list to delete the original features may not delete those points where the buffer interesects
    # so best to obtain the centroid of the buffers and output this as a new file)
    result_layer = QgsVectorLayer('Point?crs=epsg:27700&field=id:string', 'Result' , 'memory')
    result_layer.startEditing()
    for feat in poly_layer.getFeatures():
        centroid = feat.geometry().centroid()
        name = feat.attribute("ID")
        centroid_feature = QgsFeature(poly_layer.fields())
        centroid_feature.setGeometry(centroid)
        centroid_feature['ID'] = name
        result_layer.addFeature(centroid_feature)

    result_layer.commitChanges()
    QgsMapLayerRegistry.instance().addMapLayer(result_layer)

La fonction peut être exécutée immédiatement à la fin de la generate_wind_turbines()fonction en utilisant:

...
memory_lyr.commitChanges()
wind_turbine_spacing_checker(memory_lyr, spacing)

Cela donne les résultats comme indiqué dans l'image de la question. Probablement pas la solution la plus efficace, mais elle semble fonctionner.


Quelques exemples où les points rouges sont ceux générés initialement et les points affichés comme des turbines avec une limite sont le résultat final:

  • generate_wind_turbines(500)

    Scénario 1


  • generate_wind_turbines(1000)

    Scénario 2

Joseph
la source