इस जियोनेट थ्रेड में उत्तल / अवतल पतवार और कई चित्रों, लिंक और संलग्नक के विषय पर एक लंबी चर्चा थी। दुर्भाग्य से, सभी चित्रों, लिंक और अटैचमेंट को तोड़ दिया गया था जब एश्री के लिए पुराने फोरम और गैलरी को जियोनेट द्वारा बदल दिया गया था या नीचे ले जाया गया था।
यहाँ कॉन्सेव हल अनुमानक स्क्रिप्ट पर मेरी विविधताएं हैं जो एश्री के ब्रूस हेरोल्ड ने बनाई हैं। मुझे लगता है कि मेरे संस्करण ने कई सुधार किए।
मैं यहाँ ज़िप्ड टूल फ़ाइल को अटैच करने का तरीका नहीं देख रहा हूँ, इसलिए मैंने यहाँ टूल के ज़िप किए गए संस्करण के साथ एक ब्लॉग पोस्ट बनाई है । यहाँ इंटरफ़ेस की एक तस्वीर है।
यहाँ कुछ आउटपुट की एक तस्वीर है (मुझे इस तस्वीर के लिए 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 पर सेट किया गया था, लेकिन टूल ने प्रत्येक पॉलीगॉन बनाने से पहले कम से कम 6 ak फ़ैक्टर पर सेट किए गए प्रत्येक बिंदु को पुनरावृत्त कर दिया (उनमें से एक के लिए 9 का ak फ़ैक्टर उपयोग किया गया था)। इस टूल ने 35 सेकंड से कम समय में नए hull फीचर क्लास और सभी 3 hull बनाए। कुछ नियमित रूप से वितरित बिंदुओं की उपस्थिति जो पतवार के आंतरिक भाग को भरते हैं, वास्तव में रूपरेखा को परिभाषित करने के लिए आवश्यक बिंदुओं के सेट का उपयोग करने की तुलना में अधिक सटीक पतवार रूपरेखा बनाने में मदद करते हैं।