Membuat Polygon Kompleks dari Point Layer menggunakan hanya Boundary Points di ArcGIS Desktop

11

Saya perlu mengonversi lapisan titik menjadi poligon, menggunakan titik batas dari kisi kompleks untuk menentukan tepi poligon.

Saya perlu memasukkan ini ke dalam kerangka ModelBuilder di ArcGIS Desktop 10.3. Iterasi proses akan diperlukan (jika mungkin) karena banyak data yang masuk.

Lapisan titik disambungkan ke segmen sungai, dan saya perlu menentukan titik batas sungai, dan menghubungkannya untuk membuat lapisan poligon segmen sungai.

Convex hull tampaknya tidak bekerja dengan cara sungai berliku-liku, saya perlu batas ketat bersih, bukan penahanan seperti convex hull. Saya memiliki layer hanya untuk titik batas, tetapi saya tidak tahu bagaimana menghubungkan mereka untuk mendapatkan poligon.

Contoh proses teoretis

A.Wittenberg
sumber
1
Yang Anda inginkan adalah lambung cekung , yang tidak tersedia secara asli di ArcGIS, tidak seperti lambung cembung. Jika jarak titik Anda cukup kecil, Anda bisa menggunakan Euclidean Distance> Reklasifikasi> Expand> Raster ke Polygon atau Aggregate Points .
dmahr
2
Buat TIN menggunakan poin. Gambarkan TIN (hanya batas luar) dengan menggunakan jarak yang masuk akal. Konversikan TIN ke segitiga dan singkirkan yang menurut Anda tidak benar. Gabungkan segitiga bersama.
FelixIP
Terima kasih, saya sudah mulai mengerjakan ini dan mengujinya.
A.Wittenberg
Situs web ini tampaknya membahas pustaka Python yang berguna dalam mengekstraksi bentuk dari titik. blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python Saya belum mencoba kode, jadi saya tidak tahu apakah semua perpustakaan datang dengan instalasi Python atau tidak. Bagaimanapun, itu terlihat menjanjikan.
Richard Fairhurst
Perluasan pada metode Felix, saya pikir: mappingcenter.esri.com/index.cfm?fa=ask.answers&q=1661 Juga, ET GeoWizards memiliki alat untuk ini. Saya perhatikan Pengukur Hull COncave ditautkan dalam beberapa jawaban, tetapi semua tautan rusak (Saya berasumsi setelah perombakan web terbaru Esri) dan saya tidak dapat menemukan tautan yang diperbarui.
Chris W

Jawaban:

21

Utas GeoNet ini telah berdiskusi panjang tentang masalah cembung / cekung lambung dan banyak gambar, tautan, dan lampiran. Sayangnya, semua gambar, tautan, dan lampiran rusak ketika forum dan galeri lama untuk Esri digantikan oleh Geonet atau dihapus.

Berikut adalah variasi saya pada skrip Pengukur Cekung Hull yang dibuat Bruce Harold dari Esri. Saya pikir versi saya membuat beberapa perbaikan.

Saya tidak melihat cara untuk melampirkan file alat zip di sini, jadi saya telah membuat posting blog dengan versi zip dari alat di sini . Ini adalah gambar antarmuka.

Convex Hull Dengan Antarmuka Kasus

Berikut ini adalah gambar dari beberapa output (saya tidak ingat faktor k untuk gambar ini). k menunjukkan jumlah minimum titik tetangga yang dicari untuk setiap titik batas lambung. Nilai k yang lebih tinggi menghasilkan batas yang lebih halus. Jika data input tidak terdispersi secara merata, nilai k tidak dapat menghasilkan lambung tertutup.

contoh

Ini kodenya:

# 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)

Berikut adalah gambar yang baru saja saya proses pada satu set titik alamat untuk tiga Subdivisi. Sebagai perbandingan, paket asli ditampilkan. Faktor k awal untuk menjalankan alat ini diatur ke 3, tetapi alat iterasi setiap titik diatur setidaknya faktor ak 6 sebelum membuat setiap poligon (faktor ak 9 digunakan untuk salah satu dari mereka). Alat ini menciptakan kelas fitur lambung baru dan ketiga lambung kapal dalam waktu kurang dari 35 detik. Kehadiran titik-titik yang agak terdistribusi secara teratur yang mengisi interior lambung sebenarnya membantu menciptakan garis lambung yang lebih akurat daripada hanya menggunakan set titik yang seharusnya menentukan garis besar.

Paket Asli dan Poin Alamat

Hull Cekung Dibuat Dari Titik Alamat

Hamparan Lambung Cekung pada Paket Asli

Richard Fairhurst
sumber
Terima kasih untuk versi yang diperbarui / ditingkatkan! Anda mungkin ingin mencari pertanyaan dengan suara tertinggi untuk lambung cekung ArcGIS di sini dan memposting jawaban Anda di sana juga. Seperti yang saya sebutkan di komentar sebelumnya, beberapa pertanyaan referensi bahwa tautan lama yang rusak dan memiliki jawaban ini sebagai pengganti akan sangat membantu. Atau Anda (atau seseorang) dapat mengomentari pertanyaan-pertanyaan itu dan menghubungkannya dengan yang ini.
Chris W
Ini luar biasa! Tapi saya punya pertanyaan lain. Mengikuti sistem sungai saya seperti yang diajukan dalam pertanyaan, apakah alat ini memiliki cara untuk menghitung sebuah pulau di tengah sungai yang ingin Anda hilangkan?
A.Wittenberg
Tidak, itu tidak memiliki cara untuk membentuk lambung dengan lubang di dalamnya. Selain menggambar lubang secara terpisah, Anda bisa menambahkan poin untuk mengisi wilayah yang ingin Anda simpan sebagai lubang dan menetapkannya dengan atribut "lubang" (setiap lubang harus unik untuk menghindari bergabung dengan lubang yang tidak terkait lainnya). Lambung kemudian akan dibentuk untuk mendefinisikan lubang sebagai poligon terpisah. Anda bisa membuat sungai dan lubang secara bersamaan. Kemudian salin layer dan tetapkan salinan dengan kueri definisi untuk hanya memperlihatkan poligon lubang. Kemudian gunakan lubang itu sebagai fitur Erase terhadap seluruh lapisan.
Richard Fairhurst