123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- """
- /***************************************************************************
- KNearestConcaveHull.py
- ----------------------
- Date : November 2014
- Copyright : (C) 2014 by Detlev Neumann
- Dr. Neumann Consulting - Geospatial Services
- Email : dneumann@geospatial-services.de
- ***************************************************************************/
- /***************************************************************************
- * *
- * This program is free software; you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation; either version 2 of the License, or *
- * (at your option) any later version. *
- * *
- ***************************************************************************/
- """
- __author__ = 'Detlev Neumann'
- __date__ = 'November 2014'
- __copyright__ = '(C) 2014, Detlev Neumann'
- import os.path
- import math
- from qgis.PyQt.QtGui import QIcon
- from qgis.PyQt.QtCore import QVariant
- from qgis.core import (QgsApplication,
- QgsExpression,
- QgsFeature,
- QgsFeatureRequest,
- QgsFeatureSink,
- QgsField,
- QgsFields,
- QgsGeometry,
- QgsProcessing,
- QgsProcessingAlgorithm,
- QgsProcessingException,
- QgsProcessingParameterFeatureSink,
- QgsProcessingParameterFeatureSource,
- QgsProcessingParameterField,
- QgsProcessingParameterNumber,
- QgsPoint,
- QgsPointXY,
- QgsWkbTypes)
- from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
- class KNearestConcaveHull(QgisAlgorithm):
- KNEIGHBORS = 'KNEIGHBORS'
- INPUT = 'INPUT'
- OUTPUT = 'OUTPUT'
- FIELD = 'FIELD'
- def name(self):
- return 'knearestconcavehull'
- def displayName(self):
- return self.tr('Concave hull (k-nearest neighbor)')
- def shortDescription(self):
- return self.tr('Creates a concave hull using the k-nearest neighbor algorithm.')
- def icon(self):
- return QgsApplication.getThemeIcon("/algorithms/mAlgorithmConcaveHull.svg")
- def svgIconPath(self):
- return QgsApplication.iconPath("/algorithms/mAlgorithmConcaveHull.svg")
- def group(self):
- return self.tr('Vector geometry')
- def groupId(self):
- return 'vectorgeometry'
- def flags(self):
- return super().flags() | QgsProcessingAlgorithm.FlagDeprecated | QgsProcessingAlgorithm.FlagNotAvailableInStandaloneTool
- def __init__(self):
- super().__init__()
- def initAlgorithm(self, config=None):
- self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
- self.tr('Input layer')))
- self.addParameter(QgsProcessingParameterNumber(self.KNEIGHBORS,
- self.tr('Number of neighboring points to consider (a lower number is more concave, a higher number is smoother)'),
- QgsProcessingParameterNumber.Integer,
- defaultValue=3, minValue=3))
- self.addParameter(QgsProcessingParameterField(self.FIELD,
- self.tr('Field (set if creating concave hulls by class)'),
- parentLayerParameterName=self.INPUT, optional=True))
- self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Concave hull'),
- QgsProcessing.TypeVectorPolygon))
- def processAlgorithm(self, parameters, context, feedback):
- # Get variables from dialog
- source = self.parameterAsSource(parameters, self.INPUT, context)
- if source is None:
- raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))
- field_name = self.parameterAsString(parameters, self.FIELD, context)
- kneighbors = self.parameterAsInt(parameters, self.KNEIGHBORS, context)
- use_field = bool(field_name)
- field_index = -1
- fields = QgsFields()
- fields.append(QgsField('id', QVariant.Int, '', 20))
- current = 0
- # Get properties of the field the grouping is based on
- if use_field:
- field_index = source.fields().lookupField(field_name)
- if field_index >= 0:
- fields.append(source.fields()[field_index]) # Add a field with the name of the grouping field
- # Initialize writer
- (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
- fields, QgsWkbTypes.Polygon, source.sourceCrs())
- if sink is None:
- raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
- success = False
- fid = 0
- # Get unique values of grouping field
- unique_values = source.uniqueValues(field_index)
- total = 100.0 / float(source.featureCount() * len(unique_values))
- for unique in unique_values:
- points = []
- filter = QgsExpression.createFieldEqualityExpression(field_name, unique)
- request = QgsFeatureRequest().setFilterExpression(filter)
- request.setSubsetOfAttributes([])
- # Get features with the grouping attribute equal to the current grouping value
- features = source.getFeatures(request)
- for in_feature in features:
- if feedback.isCanceled():
- break
- # Add points or vertices of more complex geometry
- points.extend(extract_points(in_feature.geometry()))
- current += 1
- feedback.setProgress(int(current * total))
- # A minimum of 3 points is necessary to proceed
- if len(points) >= 3:
- out_feature = QgsFeature()
- the_hull = concave_hull(points, kneighbors)
- if the_hull:
- vertex = [QgsPointXY(point[0], point[1]) for point in the_hull]
- poly = QgsGeometry().fromPolygonXY([vertex])
- out_feature.setGeometry(poly)
- # Give the polygon the same attribute as the point grouping attribute
- out_feature.setAttributes([fid, unique])
- sink.addFeature(out_feature, QgsFeatureSink.FastInsert)
- success = True # at least one polygon created
- fid += 1
- if not success:
- raise QgsProcessingException('No hulls could be created. Most likely there were not at least three unique points in any of the groups.')
- else:
- # Field parameter provided but can't read from it
- raise QgsProcessingException('Unable to find grouping field')
- else:
- # Not grouped by field
- # Initialize writer
- (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
- fields, QgsWkbTypes.Polygon, source.sourceCrs())
- if sink is None:
- raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
- points = []
- request = QgsFeatureRequest()
- request.setSubsetOfAttributes([])
- features = source.getFeatures(request) # Get all features
- total = 100.0 / source.featureCount() if source.featureCount() else 0
- for in_feature in features:
- if feedback.isCanceled():
- break
- # Add points or vertices of more complex geometry
- points.extend(extract_points(in_feature.geometry()))
- current += 1
- feedback.setProgress(int(current * total))
- # A minimum of 3 points is necessary to proceed
- if len(points) >= 3:
- out_feature = QgsFeature()
- the_hull = concave_hull(points, kneighbors)
- if the_hull:
- vertex = [QgsPointXY(point[0], point[1]) for point in the_hull]
- poly = QgsGeometry().fromPolygonXY([vertex])
- out_feature.setGeometry(poly)
- out_feature.setAttributes([0])
- sink.addFeature(out_feature, QgsFeatureSink.FastInsert)
- else:
- # the_hull returns None only when there are less than three points after cleaning
- raise QgsProcessingException('At least three unique points are required to create a concave hull.')
- else:
- raise QgsProcessingException('At least three points are required to create a concave hull.')
- return {self.OUTPUT: dest_id}
- def clean_list(list_of_points):
- """
- Deletes duplicate points in list_of_points
- """
- return list(set(list_of_points))
- def find_min_y_point(list_of_points):
- """
- Returns that point of *list_of_points* having minimal y-coordinate
- :param list_of_points: list of tuples
- :return: tuple (x, y)
- """
- min_y_pt = list_of_points[0]
- for point in list_of_points[1:]:
- if point[1] < min_y_pt[1] or (point[1] == min_y_pt[1] and point[0] < min_y_pt[0]):
- min_y_pt = point
- return min_y_pt
- def add_point(vector, element):
- """
- Returns vector with the given element append to the right
- """
- vector.append(element)
- return vector
- def remove_point(vector, element):
- """
- Returns a copy of vector without the given element
- """
- vector.pop(vector.index(element))
- return vector
- def euclidean_distance(point1, point2):
- """
- Returns the euclidean distance of the 2 given points.
- :param point1: tuple (x, y)
- :param point2: tuple (x, y)
- :return: float
- """
- return math.sqrt(math.pow(point1[0] - point2[0], 2) + math.pow(point1[1] - point2[1], 2))
- def nearest_points(list_of_points, point, k):
- """
- Returns a list of the indices of the k closest neighbors from list_of_points to the specified point. The measure
- of proximity is the Euclidean distance. Internally, k becomes the minimum between the given value for k and the
- number of points in list_of_points
- :param list_of_points: list of tuples
- :param point: tuple (x, y)
- :param k: integer
- :return: list of k tuples
- """
- # build a list of tuples of distances between point *point* and every point in *list_of_points*, and
- # their respective index of list *list_of_distances*
- list_of_distances = []
- for index in range(len(list_of_points)):
- list_of_distances.append((euclidean_distance(list_of_points[index], point), index))
- # sort distances in ascending order
- list_of_distances.sort()
- # get the k nearest neighbors of point
- nearest_list = []
- for index in range(min(k, len(list_of_points))):
- nearest_list.append(list_of_points[list_of_distances[index][1]])
- return nearest_list
- def angle(from_point, to_point):
- """
- Returns the angle of the directed line segment, going from *from_point* to *to_point*, in radians. The angle is
- positive for segments with upward direction (north), otherwise negative (south). Values ranges from 0 at the
- right (east) to pi at the left side (west).
- :param from_point: tuple (x, y)
- :param to_point: tuple (x, y)
- :return: float
- """
- return math.atan2(to_point[1] - from_point[1], to_point[0] - from_point[0])
- def angle_difference(angle1, angle2):
- """
- Calculates the difference between the given angles in clockwise direction as radians.
- :param angle1: float
- :param angle2: float
- :return: float; between 0 and 2*Pi
- """
- if (angle1 > 0 and angle2 >= 0) and angle1 > angle2:
- return abs(angle1 - angle2)
- elif (angle1 >= 0 and angle2 > 0) and angle1 < angle2:
- return 2 * math.pi + angle1 - angle2
- elif (angle1 < 0 and angle2 <= 0) and angle1 < angle2:
- return 2 * math.pi + angle1 + abs(angle2)
- elif (angle1 <= 0 and angle2 < 0) and angle1 > angle2:
- return abs(angle1 - angle2)
- elif angle1 <= 0 < angle2:
- return 2 * math.pi + angle1 - angle2
- elif angle1 >= 0 >= angle2:
- return angle1 + abs(angle2)
- else:
- return 0
- def intersect(line1, line2):
- """
- Returns True if the two given line segments intersect each other, and False otherwise.
- :param line1: 2-tuple of tuple (x, y)
- :param line2: 2-tuple of tuple (x, y)
- :return: boolean
- """
- a1 = line1[1][1] - line1[0][1]
- b1 = line1[0][0] - line1[1][0]
- c1 = a1 * line1[0][0] + b1 * line1[0][1]
- a2 = line2[1][1] - line2[0][1]
- b2 = line2[0][0] - line2[1][0]
- c2 = a2 * line2[0][0] + b2 * line2[0][1]
- tmp = (a1 * b2 - a2 * b1)
- if tmp == 0:
- return False
- sx = (c1 * b2 - c2 * b1) / tmp
- if (sx > line1[0][0] and sx > line1[1][0]) or (sx > line2[0][0] and sx > line2[1][0]) or\
- (sx < line1[0][0] and sx < line1[1][0]) or (sx < line2[0][0] and sx < line2[1][0]):
- return False
- sy = (a1 * c2 - a2 * c1) / tmp
- if (sy > line1[0][1] and sy > line1[1][1]) or (sy > line2[0][1] and sy > line2[1][1]) or\
- (sy < line1[0][1] and sy < line1[1][1]) or (sy < line2[0][1] and sy < line2[1][1]):
- return False
- return True
- def point_in_polygon_q(point, list_of_points):
- """
- Return True if given point *point* is laying in the polygon described by the vertices *list_of_points*,
- otherwise False
- Based on the "Ray Casting Method" described by Joel Lawhead in this blog article:
- http://geospatialpython.com/2011/01/point-in-polygon.html
- """
- x = point[0]
- y = point[1]
- poly = [(pt[0], pt[1]) for pt in list_of_points]
- n = len(poly)
- inside = False
- p1x, p1y = poly[0]
- for i in range(n + 1):
- p2x, p2y = poly[i % n]
- if y > min(p1y, p2y):
- if y <= max(p1y, p2y):
- if x <= max(p1x, p2x):
- if p1y != p2y:
- xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
- if p1x == p2x or x <= xints:
- inside = not inside
- p1x, p1y = p2x, p2y
- return inside
- def extract_points(geom):
- """
- Generate list of QgsPoints from QgsGeometry *geom* ( can be point, line, or polygon )
- Code taken from fTools plugin
- :param geom: an arbitrary geometry feature
- :return: list of points
- """
- multi_geom = QgsGeometry()
- temp_geom = []
- # point geometry
- if geom.type() == 0:
- if geom.isMultipart():
- temp_geom = geom.asMultiPoint()
- else:
- temp_geom.append(geom.asPoint())
- # line geometry
- if geom.type() == 1:
- # if multipart feature explode to single part
- if geom.isMultipart():
- multi_geom = geom.asMultiPolyline()
- for i in multi_geom:
- temp_geom.extend(i)
- else:
- temp_geom = geom.asPolyline()
- # polygon geometry
- elif geom.type() == 2:
- # if multipart feature explode to single part
- if geom.isMultipart():
- multi_geom = geom.asMultiPolygon()
- # now single part polygons
- for i in multi_geom:
- # explode to line segments
- for j in i:
- temp_geom.extend(j)
- else:
- multi_geom = geom.asPolygon()
- # explode to line segments
- for i in multi_geom:
- temp_geom.extend(i)
- return temp_geom
- def sort_by_angle(list_of_points, last_point, last_angle):
- """
- returns the points in list_of_points in descending order of angle to the last segment of the envelope, measured
- in a clockwise direction. Thus, the rightmost of the neighboring points is always selected. The first point of
- this list will be the next point of the envelope.
- """
- def getkey(item):
- return angle_difference(last_angle, angle(last_point, item))
- vertex_list = sorted(list_of_points, key=getkey, reverse=True)
- return vertex_list
- def concave_hull(points_list, k):
- """
- Calculates a valid concave hull polygon containing all given points. The algorithm searches for that
- point in the neighborhood of k nearest neighbors which maximizes the rotation angle in clockwise direction
- without intersecting any previous line segments.
- This is an implementation of the algorithm described by Adriano Moreira and Maribel Yasmina Santos:
- CONCAVE HULL: A neighborhood_k-NEAREST NEIGHBORS APPROACH FOR THE COMPUTATION OF THE REGION OCCUPIED BY A SET OF POINTS.
- GRAPP 2007 - International Conference on Computer Graphics Theory and Applications; pp 61-68.
- :param points_list: list of tuples (x, y)
- :param k: integer
- :return: list of tuples (x, y)
- """
- # return an empty list if not enough points are given
- if k > len(points_list):
- k = len(points_list)
- # the number of nearest neighbors k must be greater than or equal to 3
- kk = max(k, 3)
- # delete duplicate points
- point_set = clean_list(points_list)
- # if point_set has less then 3 points no polygon can be created and an empty list will be returned
- if len(point_set) < 3:
- return None
- # if point_set has 3 points then these are already vertices of the hull. Append the first point to
- # close the hull polygon
- if len(point_set) == 3:
- return add_point(point_set, point_set[0])
- # make sure that k neighbors can be found
- kk = min(kk, len(point_set))
- # start with the point having the smallest y-coordinate (most southern point)
- first_point = find_min_y_point(point_set)
- # add this points as the first vertex of the hull
- hull = [first_point]
- # make the first vertex of the hull to the current point
- current_point = first_point
- # remove the point from the point_set, to prevent him being among the nearest points
- point_set = remove_point(point_set, first_point)
- previous_angle = math.pi
- # step counts the number of segments
- step = 2
- # as long as point_set is not empty or search is returning to the starting point
- while (current_point != first_point) or (step == 2) and (len(point_set) > 0):
- # after 3 iterations add the first point to point_set again, otherwise a hull cannot be closed
- if step == 5:
- point_set = add_point(point_set, first_point)
- # search the k nearest neighbors of the current point
- k_nearest_points = nearest_points(point_set, current_point, kk)
- # sort the candidates (neighbors) in descending order of right-hand turn. This way the algorithm progresses
- # in clockwise direction through as many points as possible
- c_points = sort_by_angle(k_nearest_points, current_point, previous_angle)
- its = True
- i = -1
- # search for the nearest point to which the connecting line does not intersect any existing segment
- while its is True and (i < len(c_points) - 1):
- i += 1
- if c_points[i] == first_point:
- last_point = 1
- else:
- last_point = 0
- j = 2
- its = False
- while its is False and (j < len(hull) - last_point):
- its = intersect((hull[step - 2], c_points[i]), (hull[step - 2 - j], hull[step - 1 - j]))
- j += 1
- # there is no candidate to which the connecting line does not intersect any existing segment, so the
- # for the next candidate fails. The algorithm starts again with an increased number of neighbors
- if its is True:
- return concave_hull(points_list, kk + 1)
- # the first point which complies with the requirements is added to the hull and gets the current point
- current_point = c_points[i]
- hull = add_point(hull, current_point)
- # calculate the angle between the last vertex and his precursor, that is the last segment of the hull
- # in reversed direction
- previous_angle = angle(hull[step - 1], hull[step - 2])
- # remove current_point from point_set
- point_set = remove_point(point_set, current_point)
- # increment counter
- step += 1
- all_inside = True
- i = len(point_set) - 1
- # check if all points are within the created polygon
- while (all_inside is True) and (i >= 0):
- all_inside = point_in_polygon_q(point_set[i], hull)
- i -= 1
- # since at least one point is out of the computed polygon, try again with a higher number of neighbors
- if all_inside is False:
- return concave_hull(points_list, kk + 1)
- # a valid hull has been constructed
- return hull
|