ArcGIS Desktop에서 경계 점만 사용하여 점 레이어에서 복잡한 다각형 만들기


11

복잡한 그리드의 경계 점을 사용하여 다각형의 가장자리를 정의하여 점 레이어를 다각형으로 변환해야합니다.

이것을 ArcGIS Desktop 10.3의 ModelBuilder 프레임 워크에 통합해야합니다. 들어오는 데이터가 많기 때문에 프로세스를 반복해야합니다 (가능한 경우).

포인트 레이어는 강 세그먼트 위에 그리드로 표시되어 강 경계 포인트를 결정하고 연결하여 강 세그먼트의 다각형 레이어를 만들어야합니다.

볼록 껍질은 강이 구불 구불 한 방식으로 작동하지 않는 것 같습니다. 볼록 껍질처럼 봉쇄가 아닌 깨끗한 경계가 필요합니다. 경계 점에 대한 레이어가 있지만 다각형에 연결하기 위해 레이어를 연결하는 방법을 모르겠습니다.

이론적 과정의 예


1
당신이 원하는 것은입니다 오목 선체 볼록 선체와는 달리,는 ArcGIS에서 기본적으로 사용할 수 없습니다. 포인트 간격이 상당히 작은 경우 유클리드 거리> 재 분류> 확장> 래스터를 다각형 또는 집계 포인트로 사용할 수 있습니다.
— dmahr

2
포인트를 사용하여 TIN을 만듭니다. 적당한 거리를 사용하여 TIN (외부 경계 만)을 묘사하십시오. TIN을 삼각형으로 변환하고 올바르지 않다고 생각되는 것을 제거하십시오. 삼각형을 함께 병합하십시오.
— FelixIP

고맙습니다, 나는 이것들을 연구하고 테스트하기 시작했습니다.
— A.Wittenberg

이 웹 사이트는 포인트에서 모양을 추출하는 데 유용한 Python 라이브러리에 대해 설명합니다. blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python 코드를 시도하지 않았으므로 모든 라이브러리가 Python 설치와 함께 제공되는지 여부를 모르겠습니다. 어쨌든 유망 해 보입니다.
— Richard Fairhurst 2016 년

Felix의 방법에 대한 확장은 다음과 같습니다. mappingcenter.esri.com/index.cfm?fa=ask.answers&q=1661 또한 ET GeoWizards 에는이를위한 도구가 있습니다. COncave Hull Estimator가 여러 답변으로 연결되어 있지만 링크가 모두 끊어졌으며 (Esri의 최신 웹 개편 후에 가정) 업데이트 된 링크를 찾을 수 없습니다.
— Chris W

답변:


21

이 GeoNet 스레드 는 볼록 / 오목 선체와 많은 그림, 링크 및 첨부 파일 주제에 대해 긴 토론을했습니다. 불행히도 Esri의 이전 포럼 및 갤러리가 Geonet으로 교체되거나 중단되었을 때 모든 그림, 링크 및 첨부 파일이 손상되었습니다.

Esri의 Bruce Harold가 만든 Concave Hull Estimator 스크립트에 대한 변형입니다. 내 버전이 몇 가지 개선 된 것으로 생각합니다.

압축 도구 파일을 여기에 첨부하는 방법이 보이지 않으므로 압축 버전의 도구 로 블로그 게시물을 작성했습니다 . 다음은 인터페이스 사진입니다.

케이스 인터페이스 별 볼록 껍질

다음은 일부 출력 그림입니다 (이 그림의 k 요소는 기억 나지 않습니다). k는 각 선체 경계 지점에 대해 검색된 최소 인접 지점 수를 나타냅니다. k 값이 클수록 경계가 더 매끄 럽습니다. 입력 데이터가 고르지 않게 분산되어있는 경우 k 값은 둘러싸는 선체를 초래할 수 없습니다.

예

코드는 다음과 같습니다.

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

다음은 세 개의 세분화를 위해 일련의 주소 지점에서 처리 한 그림입니다. 비교를 위해 원래 소포가 표시됩니다. 이 도구 실행의 시작 k 계수는 3으로 설정되었지만 도구는 각 다각형을 만들기 전에 각 점 세트를 최소 ak 요소 6으로 반복했습니다 (ak 요소 9는 그 중 하나에 사용됨). 이 도구는 35 초 안에 새로운 선체 피처 클래스와 3 개의 선체를 모두 만들었습니다. 선체 내부를 채우는 다소 규칙적으로 분포 된 점이 있으면 실제로 윤곽을 정의해야하는 점 세트를 사용하는 것보다 더 정확한 선체 윤곽선을 만드는 데 도움이됩니다.

원본 소포 및 주소

주소 지점에서 생성 된 오목 선체

원본 소포에 오목 선체 오버레이


업데이트 / 개선 된 버전에 감사드립니다! ArcGIS 오목 선체에 대해 가장 많이 투표 된 질문을 검색하고 답변을 게시 할 수도 있습니다. 이전 의견에서 언급했듯이 몇 가지 질문은 오래된 깨진 링크 와이 답변을 대체품으로 사용하는 것이 도움이 될 것이라고 언급합니다. 또는 귀하 (또는 누군가)가 해당 질문에 대해 의견을 제시하고이 질문에 연결할 수 있습니다.
— Chris W

이것은 우수하다! 그러나 다른 질문이 있습니다. 질문에 제기 된 내 강 시스템을 따라이 도구를 사용하여 강 중간에있는 섬을 설명 할 수 있습니까?
— A.Wittenberg

아니요, 구멍이있는 선체를 형성하는 방법이 없습니다. 구멍을 개별적으로 그리는 것 외에도 구멍으로 유지하려는 영역을 채우는 점을 추가하고 "구멍"속성으로 할당 할 수 있습니다 (각 구멍은 다른 관련없는 구멍과 연결되지 않도록 고유해야 함). 구멍을 별도의 다각형으로 정의하기 위해 선체가 형성됩니다. 강과 구멍을 동시에 만들 수 있습니다. 그런 다음 레이어를 복사하고 구멍 폴리곤 만 표시하도록 정의 쿼리로 복사본을 할당하십시오. 그런 다음 해당 구멍을 전체 레이어에 대한 지우기 기능으로 사용하십시오.
— Richard Fairhurst 2016 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.