KNearestConcaveHull.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. """
  2. /***************************************************************************
  3. KNearestConcaveHull.py
  4. ----------------------
  5. Date : November 2014
  6. Copyright : (C) 2014 by Detlev Neumann
  7. Dr. Neumann Consulting - Geospatial Services
  8. Email : dneumann@geospatial-services.de
  9. ***************************************************************************/
  10. /***************************************************************************
  11. * *
  12. * This program is free software; you can redistribute it and/or modify *
  13. * it under the terms of the GNU General Public License as published by *
  14. * the Free Software Foundation; either version 2 of the License, or *
  15. * (at your option) any later version. *
  16. * *
  17. ***************************************************************************/
  18. """
  19. __author__ = 'Detlev Neumann'
  20. __date__ = 'November 2014'
  21. __copyright__ = '(C) 2014, Detlev Neumann'
  22. import os.path
  23. import math
  24. from qgis.PyQt.QtGui import QIcon
  25. from qgis.PyQt.QtCore import QVariant
  26. from qgis.core import (QgsApplication,
  27. QgsExpression,
  28. QgsFeature,
  29. QgsFeatureRequest,
  30. QgsFeatureSink,
  31. QgsField,
  32. QgsFields,
  33. QgsGeometry,
  34. QgsProcessing,
  35. QgsProcessingAlgorithm,
  36. QgsProcessingException,
  37. QgsProcessingParameterFeatureSink,
  38. QgsProcessingParameterFeatureSource,
  39. QgsProcessingParameterField,
  40. QgsProcessingParameterNumber,
  41. QgsPoint,
  42. QgsPointXY,
  43. QgsWkbTypes)
  44. from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
  45. class KNearestConcaveHull(QgisAlgorithm):
  46. KNEIGHBORS = 'KNEIGHBORS'
  47. INPUT = 'INPUT'
  48. OUTPUT = 'OUTPUT'
  49. FIELD = 'FIELD'
  50. def name(self):
  51. return 'knearestconcavehull'
  52. def displayName(self):
  53. return self.tr('Concave hull (k-nearest neighbor)')
  54. def shortDescription(self):
  55. return self.tr('Creates a concave hull using the k-nearest neighbor algorithm.')
  56. def icon(self):
  57. return QgsApplication.getThemeIcon("/algorithms/mAlgorithmConcaveHull.svg")
  58. def svgIconPath(self):
  59. return QgsApplication.iconPath("/algorithms/mAlgorithmConcaveHull.svg")
  60. def group(self):
  61. return self.tr('Vector geometry')
  62. def groupId(self):
  63. return 'vectorgeometry'
  64. def flags(self):
  65. return super().flags() | QgsProcessingAlgorithm.FlagDeprecated | QgsProcessingAlgorithm.FlagNotAvailableInStandaloneTool
  66. def __init__(self):
  67. super().__init__()
  68. def initAlgorithm(self, config=None):
  69. self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
  70. self.tr('Input layer')))
  71. self.addParameter(QgsProcessingParameterNumber(self.KNEIGHBORS,
  72. self.tr('Number of neighboring points to consider (a lower number is more concave, a higher number is smoother)'),
  73. QgsProcessingParameterNumber.Integer,
  74. defaultValue=3, minValue=3))
  75. self.addParameter(QgsProcessingParameterField(self.FIELD,
  76. self.tr('Field (set if creating concave hulls by class)'),
  77. parentLayerParameterName=self.INPUT, optional=True))
  78. self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Concave hull'),
  79. QgsProcessing.TypeVectorPolygon))
  80. def processAlgorithm(self, parameters, context, feedback):
  81. # Get variables from dialog
  82. source = self.parameterAsSource(parameters, self.INPUT, context)
  83. if source is None:
  84. raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))
  85. field_name = self.parameterAsString(parameters, self.FIELD, context)
  86. kneighbors = self.parameterAsInt(parameters, self.KNEIGHBORS, context)
  87. use_field = bool(field_name)
  88. field_index = -1
  89. fields = QgsFields()
  90. fields.append(QgsField('id', QVariant.Int, '', 20))
  91. current = 0
  92. # Get properties of the field the grouping is based on
  93. if use_field:
  94. field_index = source.fields().lookupField(field_name)
  95. if field_index >= 0:
  96. fields.append(source.fields()[field_index]) # Add a field with the name of the grouping field
  97. # Initialize writer
  98. (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
  99. fields, QgsWkbTypes.Polygon, source.sourceCrs())
  100. if sink is None:
  101. raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
  102. success = False
  103. fid = 0
  104. # Get unique values of grouping field
  105. unique_values = source.uniqueValues(field_index)
  106. total = 100.0 / float(source.featureCount() * len(unique_values))
  107. for unique in unique_values:
  108. points = []
  109. filter = QgsExpression.createFieldEqualityExpression(field_name, unique)
  110. request = QgsFeatureRequest().setFilterExpression(filter)
  111. request.setSubsetOfAttributes([])
  112. # Get features with the grouping attribute equal to the current grouping value
  113. features = source.getFeatures(request)
  114. for in_feature in features:
  115. if feedback.isCanceled():
  116. break
  117. # Add points or vertices of more complex geometry
  118. points.extend(extract_points(in_feature.geometry()))
  119. current += 1
  120. feedback.setProgress(int(current * total))
  121. # A minimum of 3 points is necessary to proceed
  122. if len(points) >= 3:
  123. out_feature = QgsFeature()
  124. the_hull = concave_hull(points, kneighbors)
  125. if the_hull:
  126. vertex = [QgsPointXY(point[0], point[1]) for point in the_hull]
  127. poly = QgsGeometry().fromPolygonXY([vertex])
  128. out_feature.setGeometry(poly)
  129. # Give the polygon the same attribute as the point grouping attribute
  130. out_feature.setAttributes([fid, unique])
  131. sink.addFeature(out_feature, QgsFeatureSink.FastInsert)
  132. success = True # at least one polygon created
  133. fid += 1
  134. if not success:
  135. raise QgsProcessingException('No hulls could be created. Most likely there were not at least three unique points in any of the groups.')
  136. else:
  137. # Field parameter provided but can't read from it
  138. raise QgsProcessingException('Unable to find grouping field')
  139. else:
  140. # Not grouped by field
  141. # Initialize writer
  142. (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
  143. fields, QgsWkbTypes.Polygon, source.sourceCrs())
  144. if sink is None:
  145. raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
  146. points = []
  147. request = QgsFeatureRequest()
  148. request.setSubsetOfAttributes([])
  149. features = source.getFeatures(request) # Get all features
  150. total = 100.0 / source.featureCount() if source.featureCount() else 0
  151. for in_feature in features:
  152. if feedback.isCanceled():
  153. break
  154. # Add points or vertices of more complex geometry
  155. points.extend(extract_points(in_feature.geometry()))
  156. current += 1
  157. feedback.setProgress(int(current * total))
  158. # A minimum of 3 points is necessary to proceed
  159. if len(points) >= 3:
  160. out_feature = QgsFeature()
  161. the_hull = concave_hull(points, kneighbors)
  162. if the_hull:
  163. vertex = [QgsPointXY(point[0], point[1]) for point in the_hull]
  164. poly = QgsGeometry().fromPolygonXY([vertex])
  165. out_feature.setGeometry(poly)
  166. out_feature.setAttributes([0])
  167. sink.addFeature(out_feature, QgsFeatureSink.FastInsert)
  168. else:
  169. # the_hull returns None only when there are less than three points after cleaning
  170. raise QgsProcessingException('At least three unique points are required to create a concave hull.')
  171. else:
  172. raise QgsProcessingException('At least three points are required to create a concave hull.')
  173. return {self.OUTPUT: dest_id}
  174. def clean_list(list_of_points):
  175. """
  176. Deletes duplicate points in list_of_points
  177. """
  178. return list(set(list_of_points))
  179. def find_min_y_point(list_of_points):
  180. """
  181. Returns that point of *list_of_points* having minimal y-coordinate
  182. :param list_of_points: list of tuples
  183. :return: tuple (x, y)
  184. """
  185. min_y_pt = list_of_points[0]
  186. for point in list_of_points[1:]:
  187. if point[1] < min_y_pt[1] or (point[1] == min_y_pt[1] and point[0] < min_y_pt[0]):
  188. min_y_pt = point
  189. return min_y_pt
  190. def add_point(vector, element):
  191. """
  192. Returns vector with the given element append to the right
  193. """
  194. vector.append(element)
  195. return vector
  196. def remove_point(vector, element):
  197. """
  198. Returns a copy of vector without the given element
  199. """
  200. vector.pop(vector.index(element))
  201. return vector
  202. def euclidean_distance(point1, point2):
  203. """
  204. Returns the euclidean distance of the 2 given points.
  205. :param point1: tuple (x, y)
  206. :param point2: tuple (x, y)
  207. :return: float
  208. """
  209. return math.sqrt(math.pow(point1[0] - point2[0], 2) + math.pow(point1[1] - point2[1], 2))
  210. def nearest_points(list_of_points, point, k):
  211. """
  212. Returns a list of the indices of the k closest neighbors from list_of_points to the specified point. The measure
  213. of proximity is the Euclidean distance. Internally, k becomes the minimum between the given value for k and the
  214. number of points in list_of_points
  215. :param list_of_points: list of tuples
  216. :param point: tuple (x, y)
  217. :param k: integer
  218. :return: list of k tuples
  219. """
  220. # build a list of tuples of distances between point *point* and every point in *list_of_points*, and
  221. # their respective index of list *list_of_distances*
  222. list_of_distances = []
  223. for index in range(len(list_of_points)):
  224. list_of_distances.append((euclidean_distance(list_of_points[index], point), index))
  225. # sort distances in ascending order
  226. list_of_distances.sort()
  227. # get the k nearest neighbors of point
  228. nearest_list = []
  229. for index in range(min(k, len(list_of_points))):
  230. nearest_list.append(list_of_points[list_of_distances[index][1]])
  231. return nearest_list
  232. def angle(from_point, to_point):
  233. """
  234. Returns the angle of the directed line segment, going from *from_point* to *to_point*, in radians. The angle is
  235. positive for segments with upward direction (north), otherwise negative (south). Values ranges from 0 at the
  236. right (east) to pi at the left side (west).
  237. :param from_point: tuple (x, y)
  238. :param to_point: tuple (x, y)
  239. :return: float
  240. """
  241. return math.atan2(to_point[1] - from_point[1], to_point[0] - from_point[0])
  242. def angle_difference(angle1, angle2):
  243. """
  244. Calculates the difference between the given angles in clockwise direction as radians.
  245. :param angle1: float
  246. :param angle2: float
  247. :return: float; between 0 and 2*Pi
  248. """
  249. if (angle1 > 0 and angle2 >= 0) and angle1 > angle2:
  250. return abs(angle1 - angle2)
  251. elif (angle1 >= 0 and angle2 > 0) and angle1 < angle2:
  252. return 2 * math.pi + angle1 - angle2
  253. elif (angle1 < 0 and angle2 <= 0) and angle1 < angle2:
  254. return 2 * math.pi + angle1 + abs(angle2)
  255. elif (angle1 <= 0 and angle2 < 0) and angle1 > angle2:
  256. return abs(angle1 - angle2)
  257. elif angle1 <= 0 < angle2:
  258. return 2 * math.pi + angle1 - angle2
  259. elif angle1 >= 0 >= angle2:
  260. return angle1 + abs(angle2)
  261. else:
  262. return 0
  263. def intersect(line1, line2):
  264. """
  265. Returns True if the two given line segments intersect each other, and False otherwise.
  266. :param line1: 2-tuple of tuple (x, y)
  267. :param line2: 2-tuple of tuple (x, y)
  268. :return: boolean
  269. """
  270. a1 = line1[1][1] - line1[0][1]
  271. b1 = line1[0][0] - line1[1][0]
  272. c1 = a1 * line1[0][0] + b1 * line1[0][1]
  273. a2 = line2[1][1] - line2[0][1]
  274. b2 = line2[0][0] - line2[1][0]
  275. c2 = a2 * line2[0][0] + b2 * line2[0][1]
  276. tmp = (a1 * b2 - a2 * b1)
  277. if tmp == 0:
  278. return False
  279. sx = (c1 * b2 - c2 * b1) / tmp
  280. if (sx > line1[0][0] and sx > line1[1][0]) or (sx > line2[0][0] and sx > line2[1][0]) or\
  281. (sx < line1[0][0] and sx < line1[1][0]) or (sx < line2[0][0] and sx < line2[1][0]):
  282. return False
  283. sy = (a1 * c2 - a2 * c1) / tmp
  284. if (sy > line1[0][1] and sy > line1[1][1]) or (sy > line2[0][1] and sy > line2[1][1]) or\
  285. (sy < line1[0][1] and sy < line1[1][1]) or (sy < line2[0][1] and sy < line2[1][1]):
  286. return False
  287. return True
  288. def point_in_polygon_q(point, list_of_points):
  289. """
  290. Return True if given point *point* is laying in the polygon described by the vertices *list_of_points*,
  291. otherwise False
  292. Based on the "Ray Casting Method" described by Joel Lawhead in this blog article:
  293. http://geospatialpython.com/2011/01/point-in-polygon.html
  294. """
  295. x = point[0]
  296. y = point[1]
  297. poly = [(pt[0], pt[1]) for pt in list_of_points]
  298. n = len(poly)
  299. inside = False
  300. p1x, p1y = poly[0]
  301. for i in range(n + 1):
  302. p2x, p2y = poly[i % n]
  303. if y > min(p1y, p2y):
  304. if y <= max(p1y, p2y):
  305. if x <= max(p1x, p2x):
  306. if p1y != p2y:
  307. xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
  308. if p1x == p2x or x <= xints:
  309. inside = not inside
  310. p1x, p1y = p2x, p2y
  311. return inside
  312. def extract_points(geom):
  313. """
  314. Generate list of QgsPoints from QgsGeometry *geom* ( can be point, line, or polygon )
  315. Code taken from fTools plugin
  316. :param geom: an arbitrary geometry feature
  317. :return: list of points
  318. """
  319. multi_geom = QgsGeometry()
  320. temp_geom = []
  321. # point geometry
  322. if geom.type() == 0:
  323. if geom.isMultipart():
  324. temp_geom = geom.asMultiPoint()
  325. else:
  326. temp_geom.append(geom.asPoint())
  327. # line geometry
  328. if geom.type() == 1:
  329. # if multipart feature explode to single part
  330. if geom.isMultipart():
  331. multi_geom = geom.asMultiPolyline()
  332. for i in multi_geom:
  333. temp_geom.extend(i)
  334. else:
  335. temp_geom = geom.asPolyline()
  336. # polygon geometry
  337. elif geom.type() == 2:
  338. # if multipart feature explode to single part
  339. if geom.isMultipart():
  340. multi_geom = geom.asMultiPolygon()
  341. # now single part polygons
  342. for i in multi_geom:
  343. # explode to line segments
  344. for j in i:
  345. temp_geom.extend(j)
  346. else:
  347. multi_geom = geom.asPolygon()
  348. # explode to line segments
  349. for i in multi_geom:
  350. temp_geom.extend(i)
  351. return temp_geom
  352. def sort_by_angle(list_of_points, last_point, last_angle):
  353. """
  354. returns the points in list_of_points in descending order of angle to the last segment of the envelope, measured
  355. in a clockwise direction. Thus, the rightmost of the neighboring points is always selected. The first point of
  356. this list will be the next point of the envelope.
  357. """
  358. def getkey(item):
  359. return angle_difference(last_angle, angle(last_point, item))
  360. vertex_list = sorted(list_of_points, key=getkey, reverse=True)
  361. return vertex_list
  362. def concave_hull(points_list, k):
  363. """
  364. Calculates a valid concave hull polygon containing all given points. The algorithm searches for that
  365. point in the neighborhood of k nearest neighbors which maximizes the rotation angle in clockwise direction
  366. without intersecting any previous line segments.
  367. This is an implementation of the algorithm described by Adriano Moreira and Maribel Yasmina Santos:
  368. CONCAVE HULL: A neighborhood_k-NEAREST NEIGHBORS APPROACH FOR THE COMPUTATION OF THE REGION OCCUPIED BY A SET OF POINTS.
  369. GRAPP 2007 - International Conference on Computer Graphics Theory and Applications; pp 61-68.
  370. :param points_list: list of tuples (x, y)
  371. :param k: integer
  372. :return: list of tuples (x, y)
  373. """
  374. # return an empty list if not enough points are given
  375. if k > len(points_list):
  376. k = len(points_list)
  377. # the number of nearest neighbors k must be greater than or equal to 3
  378. kk = max(k, 3)
  379. # delete duplicate points
  380. point_set = clean_list(points_list)
  381. # if point_set has less then 3 points no polygon can be created and an empty list will be returned
  382. if len(point_set) < 3:
  383. return None
  384. # if point_set has 3 points then these are already vertices of the hull. Append the first point to
  385. # close the hull polygon
  386. if len(point_set) == 3:
  387. return add_point(point_set, point_set[0])
  388. # make sure that k neighbors can be found
  389. kk = min(kk, len(point_set))
  390. # start with the point having the smallest y-coordinate (most southern point)
  391. first_point = find_min_y_point(point_set)
  392. # add this points as the first vertex of the hull
  393. hull = [first_point]
  394. # make the first vertex of the hull to the current point
  395. current_point = first_point
  396. # remove the point from the point_set, to prevent him being among the nearest points
  397. point_set = remove_point(point_set, first_point)
  398. previous_angle = math.pi
  399. # step counts the number of segments
  400. step = 2
  401. # as long as point_set is not empty or search is returning to the starting point
  402. while (current_point != first_point) or (step == 2) and (len(point_set) > 0):
  403. # after 3 iterations add the first point to point_set again, otherwise a hull cannot be closed
  404. if step == 5:
  405. point_set = add_point(point_set, first_point)
  406. # search the k nearest neighbors of the current point
  407. k_nearest_points = nearest_points(point_set, current_point, kk)
  408. # sort the candidates (neighbors) in descending order of right-hand turn. This way the algorithm progresses
  409. # in clockwise direction through as many points as possible
  410. c_points = sort_by_angle(k_nearest_points, current_point, previous_angle)
  411. its = True
  412. i = -1
  413. # search for the nearest point to which the connecting line does not intersect any existing segment
  414. while its is True and (i < len(c_points) - 1):
  415. i += 1
  416. if c_points[i] == first_point:
  417. last_point = 1
  418. else:
  419. last_point = 0
  420. j = 2
  421. its = False
  422. while its is False and (j < len(hull) - last_point):
  423. its = intersect((hull[step - 2], c_points[i]), (hull[step - 2 - j], hull[step - 1 - j]))
  424. j += 1
  425. # there is no candidate to which the connecting line does not intersect any existing segment, so the
  426. # for the next candidate fails. The algorithm starts again with an increased number of neighbors
  427. if its is True:
  428. return concave_hull(points_list, kk + 1)
  429. # the first point which complies with the requirements is added to the hull and gets the current point
  430. current_point = c_points[i]
  431. hull = add_point(hull, current_point)
  432. # calculate the angle between the last vertex and his precursor, that is the last segment of the hull
  433. # in reversed direction
  434. previous_angle = angle(hull[step - 1], hull[step - 2])
  435. # remove current_point from point_set
  436. point_set = remove_point(point_set, current_point)
  437. # increment counter
  438. step += 1
  439. all_inside = True
  440. i = len(point_set) - 1
  441. # check if all points are within the created polygon
  442. while (all_inside is True) and (i >= 0):
  443. all_inside = point_in_polygon_q(point_set[i], hull)
  444. i -= 1
  445. # since at least one point is out of the computed polygon, try again with a higher number of neighbors
  446. if all_inside is False:
  447. return concave_hull(points_list, kk + 1)
  448. # a valid hull has been constructed
  449. return hull