Crear un polígono complejo a partir de la capa de puntos utilizando solo puntos de límite en ArcGIS Desktop

11

Necesito convertir una capa de puntos en un polígono, utilizando los puntos de límite de una cuadrícula compleja para definir los bordes del polígono.

Necesito incorporar esto en un marco de ModelBuilder en ArcGIS Desktop 10.3. Será necesario iterar el proceso (si es posible) debido a la gran cantidad de datos entrantes.

La capa de puntos está cuadriculada sobre un segmento de río, y necesito determinar los puntos de límite de los ríos y conectarlos para crear una capa de polígono del segmento de río.

El casco convexo no parece funcionar con la forma en que los ríos serpentean, necesito un límite limpio y apretado, no una contención como lo hace el casco convexo. Tengo capas solo para los puntos límite, pero no sé cómo conectarlas para llegar a un polígono.

Ejemplo de proceso teórico.

A.Wittenberg
fuente
1
Lo que desea es un casco cóncavo , que no está disponible de forma nativa en ArcGIS, a diferencia de los cascos convexos. Si el espaciado de puntos es bastante pequeño, puede usar Distancia euclidiana> Reclasificar> Expandir> De ráster a polígono o Agregar puntos .
dmahr
2
Crea TIN usando puntos. Delinear TIN (solo fuera del límite) usando una distancia razonable. Convierta TIN en triángulos y elimine los que cree que no son correctos. Fusionar triángulos juntos.
FelixIP
Gracias, he comenzado a trabajar con estos y probarlos.
A.Wittenberg
Este sitio web parece discutir bibliotecas de Python que son útiles para extraer formas de puntos. blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python No he probado el código, así que no sé si todas las bibliotecas vienen con la instalación de Python o no. De todos modos, parece prometedor.
Richard Fairhurst
Expansión en el método de Felix, creo: mappingcenter.esri.com/index.cfm?fa=ask.answers&q=1661 Además, ET GeoWizards tiene una herramienta para esto. Observo que el Estimador de casco COncave está vinculado en varias respuestas, pero todos los enlaces están rotos (supongo que después de la última reorganización web de Esri) y no puedo encontrar un enlace actualizado.
Chris W

Respuestas:

21

Este hilo de GeoNet tuvo una larga discusión sobre el tema de los cascos convexos / cóncavos y muchas imágenes, enlaces y archivos adjuntos. Desafortunadamente, todas las imágenes, enlaces y archivos adjuntos se rompieron cuando el viejo foro y galería de Esri fue reemplazado por Geonet o eliminado.

Aquí están mis variaciones en el guión del Estimador de casco cóncavo que Bruce Harold de Esri creó. Creo que mi versión hizo varias mejoras.

No veo una forma de adjuntar el archivo comprimido de la herramienta aquí, así que he creado una publicación de blog con la versión comprimida de la herramienta aquí . Aquí hay una foto de la interfaz.

Casco convexo por interfaz de caso

Aquí hay una imagen de algunas salidas (no recuerdo el factor k para esta imagen). k indica el número mínimo de puntos vecinos buscados para cada punto límite del casco. Los valores más altos de k dan como resultado límites más suaves. Cuando los datos de entrada están dispersos de manera desigual, ningún valor de k puede dar como resultado un casco cerrado.

ejemplo

Aquí está el código:

# Author: ESRI
# Date:   August 2010
#
# Purpose: This script creates a concave hull polygon FC using a k-nearest neighbours approach
#          modified from that of A. Moreira and M. Y. Santos, University of Minho, Portugal.
#          It identifies a polygon which is the region occupied by an arbitrary set of points
#          by considering at least "k" nearest neighbouring points (30 >= k >= 3) amongst the set.
#          If input points have uneven spatial density then any value of k may not connect the
#          point "clusters" and outliers will be excluded from the polygon.  Pre-processing into
#          selection sets identifying clusters will allow finding hulls one at a time.  If the
#          found polygon does not enclose the input point features, higher values of k are tried
#          up to a maximum of 30.
#
# Author: Richard Fairhurst
# Date:   February 2012
#
# Update:  The script was enhanced by Richard Fairhurst to include an optional case field parameter.
#          The case field can be any numeric, string, or date field in the point input and is
#          used to sort the points and generate separate polygons for each case value in the output.
#          If the Case field is left blank the script will work on all input points as it did
#          in the original script.
#
#          A field named "POINT_CNT" is added to the output feature(s) to indicate the number of
#          unique point locations used to create the output polygon(s).
#
#          A field named "ENCLOSED" is added to the output feature(s) to indicates if all of the
#          input points were enclosed by the output polygon(s). An ENCLOSED value of 1 means all
#          points were enclosed. When the ENCLOSED value is 0 and Area and Perimeter are greater
#          than 0, either all points are touching the hull boundary or one or more outlier points
#          have been excluded from the output hull. Use selection sets or preprocess input data
#          to find enclosing hulls. When a feature with an ENCLOSED value of 0 and Empty or Null
#          geometry is created (Area and Perimeter are either 0 or Null) insufficient input points
#          were provided to create an actual polygon.
try:

    import arcpy
    import itertools
    import math
    import os
    import sys
    import traceback
    import string

    arcpy.overwriteOutput = True

    #Functions that consolidate reuable actions
    #

    #Function to return an OID list for k nearest eligible neighbours of a feature
    def kNeighbours(k,oid,pDict,excludeList=[]):
        hypotList = [math.hypot(pDict[oid][0]-pDict[id][0],pDict[oid][5]-pDict[id][6]) for id in pDict.keys() if id <> oid and id not in excludeList]
        hypotList.sort()
        hypotList = hypotList[0:k]
        oidList = [id for id in pDict.keys() if math.hypot(pDict[oid][0]-pDict[id][0],pDict[oid][7]-pDict[id][8]) in hypotList and id <> oid and id not in excludeList]
        return oidList

    #Function to rotate a point about another point, returning a list [X,Y]
    def RotateXY(x,y,xc=0,yc=0,angle=0):
        x = x - xc
        y = y - yc
        xr = (x * math.cos(angle)) - (y * math.sin(angle)) + xc
        yr = (x * math.sin(angle)) + (y * math.cos(angle)) + yc
        return [xr,yr]

    #Function finding the feature OID at the rightmost angle from an origin OID, with respect to an input angle
    def Rightmost(oid,angle,pDict,oidList):
        origxyList = [pDict[id] for id in pDict.keys() if id in oidList]
        rotxyList = []
        for p in range(len(origxyList)):
            rotxyList.append(RotateXY(origxyList[p][0],origxyList[p][9],pDict[oid][0],pDict[oid][10],angle))
        minATAN = min([math.atan2((xy[1]-pDict[oid][11]),(xy[0]-pDict[oid][0])) for xy in rotxyList])
        rightmostIndex = rotxyList.index([xy for xy in rotxyList if math.atan2((xy[1]-pDict[oid][1]),(xy[0]-pDict[oid][0])) == minATAN][0])
        return oidList[rightmostIndex]

    #Function to detect single-part polyline self-intersection    
    def selfIntersects(polyline):
        lList = []
        selfIntersects = False
        for n in range(0, len(line.getPart(0))-1):
            lList.append(arcpy.Polyline(arcpy.Array([line.getPart(0)[n],line.getPart(0)[n+1]])))
        for pair in itertools.product(lList, repeat=2): 
            if pair[0].crosses(pair[1]):
                selfIntersects = True
                break
        return selfIntersects

    #Function to construct the Hull
    def createHull(pDict, outCaseField, lastValue, kStart, dictCount, includeNull):
        #Value of k must result in enclosing all data points; create condition flag
        enclosesPoints = False
        notNullGeometry = False
        k = kStart

        if dictCount > 1:
            pList = [arcpy.Point(xy[0],xy[1]) for xy in pDict.values()]
            mPoint = arcpy.Multipoint(arcpy.Array(pList),sR)
            minY = min([xy[1] for xy in pDict.values()])


            while not enclosesPoints and k <= 30:
                arcpy.AddMessage("Finding hull for k = " + str(k))
                #Find start point (lowest Y value)
                startOID = [id for id in pDict.keys() if pDict[id][1] == minY][0]
                #Select the next point (rightmost turn from horizontal, from start point)
                kOIDList = kNeighbours(k,startOID,pDict,[])
                minATAN = min([math.atan2(pDict[id][14]-pDict[startOID][15],pDict[id][0]-pDict[startOID][0]) for id in kOIDList])
                nextOID = [id for id in kOIDList if math.atan2(pDict[id][1]-pDict[startOID][1],pDict[id][0]-pDict[startOID][0]) == minATAN][0]
                #Initialise the boundary array
                bArray = arcpy.Array(arcpy.Point(pDict[startOID][0],pDict[startOID][18]))
                bArray.add(arcpy.Point(pDict[nextOID][0],pDict[nextOID][19]))
                #Initialise current segment lists
                currentOID = nextOID
                prevOID = startOID
                #Initialise list to be excluded from candidate consideration (start point handled additionally later)
                excludeList = [startOID,nextOID]
                #Build the boundary array - taking the closest rightmost point that does not cause a self-intersection.
                steps = 2
                while currentOID <> startOID and len(pDict) <> len(excludeList):
                    try:
                        angle = math.atan2((pDict[currentOID][20]- pDict[prevOID][21]),(pDict[currentOID][0]- pDict[prevOID][0]))
                        oidList = kNeighbours(k,currentOID,pDict,excludeList)
                        nextOID = Rightmost(currentOID,0-angle,pDict,oidList)
                        pcArray = arcpy.Array([arcpy.Point(pDict[currentOID][0],pDict[currentOID][22]),\
                                            arcpy.Point(pDict[nextOID][0],pDict[nextOID][23])])
                        while arcpy.Polyline(bArray,sR).crosses(arcpy.Polyline(pcArray,sR)) and len(oidList) > 0:
                            #arcpy.AddMessage("Rightmost point from " + str(currentOID) + " : " + str(nextOID) + " causes self intersection - selecting again")
                            excludeList.append(nextOID)
                            oidList.remove(nextOID)
                            oidList = kNeighbours(k,currentOID,pDict,excludeList)
                            if len(oidList) > 0:
                                nextOID = Rightmost(currentOID,0-angle,pDict,oidList)
                                #arcpy.AddMessage("nextOID candidate: " + str(nextOID))
                                pcArray = arcpy.Array([arcpy.Point(pDict[currentOID][0],pDict[currentOID][24]),\
                                                    arcpy.Point(pDict[nextOID][0],pDict[nextOID][25])])
                        bArray.add(arcpy.Point(pDict[nextOID][0],pDict[nextOID][26]))
                        prevOID = currentOID
                        currentOID = nextOID
                        excludeList.append(currentOID)
                        #arcpy.AddMessage("CurrentOID = " + str(currentOID))
                        steps+=1
                        if steps == 4:
                            excludeList.remove(startOID)
                    except ValueError:
                        arcpy.AddMessage("Zero reachable nearest neighbours at " + str(pDict[currentOID]) + " , expanding search")
                        break
                #Close the boundary and test for enclosure
                bArray.add(arcpy.Point(pDict[startOID][0],pDict[startOID][27]))
                pPoly = arcpy.Polygon(bArray,sR)
                if pPoly.length == 0:
                    break
                else:
                    notNullGeometry = True
                if mPoint.within(arcpy.Polygon(bArray,sR)):
                    enclosesPoints = True
                else:
                    arcpy.AddMessage("Hull does not enclose data, incrementing k")
                    k+=1
            #
            if not mPoint.within(arcpy.Polygon(bArray,sR)):
                arcpy.AddWarning("Hull does not enclose data - probable cause is outlier points")

        #Insert the Polygons
        if (notNullGeometry and includeNull == False) or includeNull:
            rows = arcpy.InsertCursor(outFC)
            row = rows.newRow()
            if outCaseField > " " :
                row.setValue(outCaseField, lastValue)
            row.setValue("POINT_CNT", dictCount)
            if notNullGeometry:
                row.shape = arcpy.Polygon(bArray,sR)
                row.setValue("ENCLOSED", enclosesPoints)
            else:
                row.setValue("ENCLOSED", -1)
            rows.insertRow(row)
            del row
            del rows
        elif outCaseField > " ":
            arcpy.AddMessage("\nExcluded Null Geometry for case value " + str(lastValue) + "!")
        else:
            arcpy.AddMessage("\nExcluded Null Geometry!")

    # Main Body of the program.
    #
    #

    #Get the input feature class or layer
    inPoints = arcpy.GetParameterAsText(0)
    inDesc = arcpy.Describe(inPoints)
    inPath = os.path.dirname(inDesc.CatalogPath)
    sR = inDesc.spatialReference

    #Get k
    k = arcpy.GetParameter(1)
    kStart = k

    #Get output Feature Class
    outFC = arcpy.GetParameterAsText(2)
    outPath = os.path.dirname(outFC)
    outName = os.path.basename(outFC)

    #Get case field and ensure it is valid
    caseField = arcpy.GetParameterAsText(3)
    if caseField > " ":
        fields = inDesc.fields
        for field in fields:
            # Check the case field type
            if field.name == caseField:
                caseFieldType = field.type
                if caseFieldType not in ["SmallInteger", "Integer", "Single", "Double", "String", "Date"]:
                    arcpy.AddMessage("\nThe Case Field named " + caseField + " is not a valid case field type!  The Case Field will be ignored!\n")
                    caseField = " "
                else:
                    if caseFieldType in ["SmallInteger", "Integer", "Single", "Double"]:
                        caseFieldLength = 0
                        caseFieldScale = field.scale
                        caseFieldPrecision = field.precision
                    elif caseFieldType == "String":
                        caseFieldLength = field.length
                        caseFieldScale = 0
                        caseFieldPrecision = 0
                    else:
                        caseFieldLength = 0
                        caseFieldScale = 0
                        caseFieldPrecision = 0

    #Define an output case field name that is compliant with the output feature class
    outCaseField = str.upper(str(caseField))
    if outCaseField == "ENCLOSED":
        outCaseField = "ENCLOSED1"
    if outCaseField == "POINT_CNT":
        outCaseField = "POINT_CNT1"
    if outFC.split(".")[-1] in ("shp","dbf"):
        outCaseField = outCaseField[0,10] #field names in the output are limited to 10 charaters!

    #Get Include Null Geometry Feature flag
    if arcpy.GetParameterAsText(4) == "true":
        includeNull = True
    else:
        includeNull = False

    #Some housekeeping
    inDesc = arcpy.Describe(inPoints)
    sR = inDesc.spatialReference
    arcpy.env.OutputCoordinateSystem = sR
    oidName = str(inDesc.OIDFieldName)
    if inDesc.dataType == "FeatureClass":
        inPoints = arcpy.MakeFeatureLayer_management(inPoints)

    #Create the output
    arcpy.AddMessage("\nCreating Feature Class...")
    outFC = arcpy.CreateFeatureclass_management(outPath,outName,"POLYGON","#","#","#",sR).getOutput(0)
    if caseField > " ":
        if caseFieldType in ["SmallInteger", "Integer", "Single", "Double"]:
            arcpy.AddField_management(outFC, outCaseField, caseFieldType, str(caseFieldScale), str(caseFieldPrecision))
        elif caseFieldType == "String":
            arcpy.AddField_management(outFC, outCaseField, caseFieldType, "", "", str(caseFieldLength))
        else:
            arcpy.AddField_management(outFC, outCaseField, caseFieldType)
    arcpy.AddField_management(outFC, "POINT_CNT", "Long")
    arcpy.AddField_management(outFC, "ENCLOSED", "SmallInteger")

    #Build required data structures
    arcpy.AddMessage("\nCreating data structures...")
    rowCount = 0
    caseCount = 0
    dictCount = 0
    pDict = {} #dictionary keyed on oid with [X,Y] list values, no duplicate points
    if caseField > " ":
        for p in arcpy.SearchCursor(inPoints, "", "", "", caseField + " ASCENDING"):
            rowCount += 1
            if rowCount == 1:
                #Initialize lastValue variable when processing the first record.
                lastValue = p.getValue(caseField)
            if lastValue == p.getValue(caseField):
                #Continue processing the current point subset.
                if [p.shape.firstPoint.X,p.shape.firstPoint.Y] not in pDict.values():
                    pDict[p.getValue(inDesc.OIDFieldName)] = [p.shape.firstPoint.X,p.shape.firstPoint.Y]
                    dictCount += 1
            else:
                #Create a hull prior to processing the next case field subset.
                createHull(pDict, outCaseField, lastValue, kStart, dictCount, includeNull)
                if outCaseField > " ":
                    caseCount += 1
                #Reset variables for processing the next point subset.
                pDict = {}
                pDict[p.getValue(inDesc.OIDFieldName)] = [p.shape.firstPoint.X,p.shape.firstPoint.Y]
                lastValue = p.getValue(caseField)
                dictCount = 1
    else:
        for p in arcpy.SearchCursor(inPoints):
            rowCount += 1
            if [p.shape.firstPoint.X,p.shape.firstPoint.Y] not in pDict.values():
                pDict[p.getValue(inDesc.OIDFieldName)] = [p.shape.firstPoint.X,p.shape.firstPoint.Y]
                dictCount += 1
                lastValue = 0
    #Final create hull call and wrap up of the program's massaging
    createHull(pDict, outCaseField, lastValue, kStart, dictCount, includeNull)
    if outCaseField > " ":
        caseCount += 1
    arcpy.AddMessage("\n" + str(rowCount) + " points processed.  " + str(caseCount) + " case value(s) processed.")
    if caseField == " " and arcpy.GetParameterAsText(3) > " ":
        arcpy.AddMessage("\nThe Case Field named " + arcpy.GetParameterAsText(3) + " was not a valid field type and was ignored!")
    arcpy.AddMessage("\nFinished")


#Error handling    
except:
    tb = sys.exc_info()[2]
    tbinfo = traceback.format_tb(tb)[0]
    pymsg = "PYTHON ERRORS:\nTraceback Info:\n" + tbinfo + "\nError Info:\n    " + \
            str(sys.exc_type)+ ": " + str(sys.exc_value) + "\n"
    arcpy.AddError(pymsg)

    msgs = "GP ERRORS:\n" + arcpy.GetMessages(2) + "\n"
    arcpy.AddError(msgs)

Aquí hay imágenes que acabo de procesar en un conjunto de puntos de dirección para tres subdivisiones. A modo de comparación, se muestran las parcelas originales. El factor k inicial para esta ejecución de la herramienta se estableció en 3, pero la herramienta repitió cada punto establecido en al menos un factor k de 6 antes de crear cada polígono (se usó un factor k de 9 para uno de ellos). La herramienta creó la nueva clase de entidad de casco y los 3 cascos en menos de 35 segundos. La presencia de puntos distribuidos de manera regular que llenan el interior del casco realmente ayuda a crear un contorno de casco más preciso que simplemente usar el conjunto de puntos que deberían definir el contorno.

Paquetes originales y puntos de dirección

Cascos cóncavos creados a partir de puntos de dirección

Superposición de cascos cóncavos en parcelas originales

Richard Fairhurst
fuente
¡Gracias por la versión actualizada / mejorada! Es posible que desee buscar la pregunta más votada para los cascos cóncavos de ArcGIS aquí y publicar su respuesta allí también. Como mencioné en un comentario anterior, varias preguntas hacen referencia a ese viejo enlace roto y tener esta respuesta como reemplazo sería útil. Alternativamente, usted (o alguien) podría comentar esas preguntas y vincularlas a esta.
Chris W
¡Esto es excelente! Pero tengo otra pregunta. Siguiendo mi sistema fluvial como se plantea en la pregunta, ¿esta herramienta tiene una manera de dar cuenta de una isla en medio de un río que desea omitir?
A.Wittenberg
No, no tiene forma de formar un casco con un agujero. Además de dibujar el agujero por separado, puede agregar puntos para llenar la región que desea mantener como un agujero y asignarlos con un atributo "agujero" (cada agujero debería ser único para evitar unirse con otros agujeros no relacionados). Entonces se formaría un casco para definir el agujero como un polígono separado. Podrías crear ríos y agujeros al mismo tiempo. Luego copie la capa y asigne la copia con una consulta de definición para mostrar solo los polígonos de agujeros. Luego use esos agujeros como entidades de borrado contra toda la capa.
Richard Fairhurst