Module tagmaps.classes.alpha_shapes
Module for tag maps alpha shapes generation
Expand source code
# -*- coding: utf-8 -*-
"""
Module for tag maps alpha shapes generation
"""
# delay evaluation of annotations at runtime (PEP 563)
from __future__ import absolute_import, annotations
import math
from dataclasses import dataclass
from typing import List, Optional
import numpy as np
import shapely.geometry as geometry
from scipy.spatial import Delaunay # pylint: disable=E0611
from shapely.ops import polygonize, unary_union
from tagmaps.classes.compile_output import Compile
from tagmaps.classes.shared_structure import ItemCounter
@dataclass
class AlphaShapesAndMeta:
"""Stores cluster geometry and calculated summary statistics"""
shape: geometry.Polygon
post_count: int
views: int
user_count: int
item_name: str
item_totalcount: int
weightsv1: float
weightsv2: float
weightsv3: float
shapetype: str
def __getitem__(self, key):
"""Implements subscription by name"""
return getattr(self, key)
@dataclass
class AlphaShapesArea:
"""Collection of of Alpha Shapes (list of geometries and meta)
and total area
"""
alphashape: Optional[List[AlphaShapesAndMeta]]
item_area: float
class AlphaShapes:
"""Converts (cluster) point clouds to shapes"""
@staticmethod
def get_single_cluster_shape(
item: ItemCounter, single_post, cluster_distance: float, proj_coords
):
"""Get Shapes for items with no clusters
Will return a buffer based on cluster distance
"""
shapetype = "Single cluster"
# project lat/lng to UTM
point_x, point_y = proj_coords(single_post.lng, single_post.lat)
pcoordinate = geometry.Point(point_x, point_y)
# single dots are presented
# as buffers with 0.5% of width-area
result_polygon = pcoordinate.buffer(cluster_distance / 4, resolution=3)
# result_polygon = pcoordinate.buffer(
# min(distXLng,distYLat)/100,
# resolution=3)
if result_polygon is None or result_polygon.is_empty:
return None
# append statistics for item with no cluster
single_cluster_result = AlphaShapesAndMeta(
result_polygon,
1,
max(single_post.post_views_count, single_post.post_like_count),
1,
item.name,
item.ucount,
1,
1,
1,
shapetype,
)
return single_cluster_result
@staticmethod
def get_cluster_shape(
item: ItemCounter,
clustered_post_guids,
cleaned_post_dict,
cluster_distance: float,
local_saturation_check,
proj_coords,
):
"""Returns alpha shapes and tag_area (sqm) for a point cloud"""
# we define a new list of Temp Alpha Shapes outside the loop,
# so that it is not overwritten each time
alphashapes_and_meta_tmp = list()
item_area = 0
for post_guids in clustered_post_guids:
# for each cluster for this toptag
posts = [cleaned_post_dict[x] for x in post_guids]
post_count = len(post_guids)
unique_user_count = len({post.user_guid for post in posts})
sum_views = sum([post.post_views_count for post in posts])
# needs to be moved to CompileOutput:
weightsv1 = Compile.get_weight(1, post_count, unique_user_count)
weightsv2 = Compile.get_weight(2, post_count, unique_user_count)
weightsv3 = Compile.get_weight(3, post_count, unique_user_count)
distinct_locations = {post.loc_id for post in posts}
# simple list comprehension with projection:
points = [
geometry.Point(
proj_coords(
float(location.split(":")[1]), float(location.split(":")[0])
)
)
for location in distinct_locations
]
# get poly shape from points
result = AlphaShapes._get_poly(points, cluster_distance)
result_polygon = result[0]
shapetype = result[1]
# Geom, Join_Count, Views, COUNT_User,ImpTag,TagCountG,HImpTag
if result_polygon is not None and not result_polygon.is_empty:
if local_saturation_check:
item_area += result_polygon.area
alphashapedata = AlphaShapesAndMeta(
result_polygon,
post_count,
sum_views,
unique_user_count,
item.name,
item.ucount,
weightsv1,
weightsv2,
weightsv3,
shapetype,
)
alphashapes_and_meta_tmp.append(alphashapedata)
if alphashapes_and_meta_tmp:
# finally sort and append all
# cluster shapes for this tag
alphashapes_and_meta_tmp = sorted(
alphashapes_and_meta_tmp, key=lambda x: -x.weightsv1
)
return AlphaShapesArea(alphashapes_and_meta_tmp, item_area)
@staticmethod
def _get_poly_five_to_ten(point_collection, cluster_distance):
"""convex hull for small point clouds"""
result_polygon = point_collection.convex_hull
result_polygon = result_polygon.buffer(cluster_distance / 4, resolution=3)
shapetype = "between 5 and 10 points_convexHull"
# result_polygon = result_polygon.buffer(
# min(distXLng,distYLat)/100,resolution=3)
return result_polygon, shapetype
@staticmethod
def _get_poly_two_to_five(point_collection, cluster_distance):
"""convex hull for tiny point clouds"""
shapetype = "between 2 and 5 points_buffer"
# calc distance between points, see
# http://www.mathwarehouse.com/algebra/distance_formula/index.php
# bdist = math.sqrt(
# (points[0].coords.xy[0][0]-points[1].coords.xy[0][0])**2
# + (points[0].coords.xy[1][0]-points[1].coords.xy[1][0])**2)
# print(str(bdist))
# single dots are presented as buffer with 0.5% of width-area:
poly_shape = point_collection.buffer(cluster_distance / 4, resolution=3)
poly_shape = poly_shape.convex_hull
# result_polygon = point_collection.buffer(
# min(distXLng,distYLat)/100,resolution=3)
# #single dots are presented as buffer with 0.5% of width-area
return poly_shape, shapetype
@staticmethod
def _get_poly_larger_ten(points, pointcount, cluster_distance, point_collection):
"""Get poly for larger point clouds
This is still recursive trial & error
Repeat generating alpha shapes with
smaller alpha value if Multigon is
generated. Smaller alpha values mean
less granularity of resulting polygon,
but too large alpha may result
in empty polygon
(this branch is sometimes
executed for larger scales)
"""
if pointcount > 500:
startalpha = 1000000
elif pointcount > 200:
startalpha = 10000
else:
startalpha = 9000
# concave hull/alpha shape /50000:
poly_shape = AlphaShapes.alpha_shape(
points, alpha=cluster_distance / startalpha
)
shapetype = "Initial Alpha Shape + Buffer"
if not isinstance(poly_shape, bool) and poly_shape.is_empty:
# try again with centered axis reduced alpha
poly_shape = AlphaShapes.alpha_shape(
points, alpha=(cluster_distance / startalpha), centeraxis=True
)
# if not poly_shape.is_empty:
# print("Success")
# check type comparison here
if isinstance(poly_shape, (bool, geometry.multipolygon.MultiPolygon)):
for i in range(1, 6):
# try decreasing alpha
# ** means cube
alpha = startalpha + (startalpha * (i**i))
# /100000
poly_shape = AlphaShapes.alpha_shape(
points, alpha=cluster_distance / alpha
)
if not (
isinstance(poly_shape, geometry.multipolygon.MultiPolygon)
and not isinstance(poly_shape, bool)
):
shapetype = "Multipolygon Alpha Shape /" + str(alpha)
break
if isinstance(poly_shape, (bool, geometry.multipolygon.MultiPolygon)):
# try increasing alpha
for i in range(1, 6):
# try decreasing alpha
alpha = startalpha / (i * i)
# /100000
poly_shape = AlphaShapes.alpha_shape(
points, alpha=cluster_distance / alpha
)
if not (
isinstance(poly_shape, geometry.multipolygon.MultiPolygon)
and not isinstance(poly_shape, bool)
):
shapetype = "Multipolygon Alpha Shape /" + str(alpha)
break
if isinstance(poly_shape, geometry.multipolygon.MultiPolygon):
shapetype = "Multipolygon Alpha Shape -> Convex Hull"
# if still of type multipolygon,
# try to remove holes and do a convex_hull
poly_shape = poly_shape.convex_hull
# OR: in case there was a problem with generating
# alpha shapes
# (circum_r = a*b*c/(4.0*area)
# --> ZeroDivisionError: float division by zero)
# this branch is rarely executed
# for large point clusters where
# alpha is perhaps set too small
if (
isinstance(poly_shape, bool)
or poly_shape.is_empty
or AlphaShapes._few_polyinpoints(points, poly_shape)
):
shapetype = "BoolAlpha/Empty -> Fallback to PointCloud Convex Hull"
# convex hull
poly_shape = point_collection.convex_hull
# Finally do a buffer to smooth alpha
poly_shape = poly_shape.buffer(cluster_distance / 4, resolution=3)
# result_polygon = result_polygon.buffer(
# min(distXLng,distYLat)/100,resolution=3)
return poly_shape, shapetype
@staticmethod
def _few_polyinpoints(points, poly):
"""Check how many of original points are in Alpha
Because some QHull Delauney return a small Alpha Shape that
does not include most points, this is a fallback solution.
Better solution would be to revise QHull Delauney/Alpha,
but even with centered axis, this issue remains (see alpha_shape).
"""
p_count = 1
for point in points:
if point.within(poly):
p_count += 1
perc = p_count / (len(points) / 100)
if perc < 50:
return True
return False
@staticmethod
def _get_poly_one(point_collection, cluster_distance):
shapetype = "1 point cluster"
# single dots are presented as buffer
# with 0.5% of width-area
poly_shape = point_collection.buffer(cluster_distance / 4, resolution=3)
# result_polygon = point_collection.buffer(
# min(distXLng,distYLat)/100,resolution=3)
# #single dots are presented as buffer
# with 0.5% of width-area
return poly_shape, shapetype
@staticmethod
def _get_poly(points, cluster_distance):
"""Get polygon/shape for collection
of geooordinates (points)
Returns:
Shape (Polygon)
Shapetype (Approach of Shape-Gen)
"""
point_collection = geometry.MultiPoint(list(points))
poly_shape = None
pointcount = len(points)
shapetype: str = ""
if pointcount >= 5:
if pointcount < 10:
poly_shape, shapetype = AlphaShapes._get_poly_five_to_ten(
point_collection, cluster_distance
)
else:
poly_shape, shapetype = AlphaShapes._get_poly_larger_ten(
points, pointcount, cluster_distance, point_collection
)
elif 2 <= pointcount < 5:
poly_shape, shapetype = AlphaShapes._get_poly_two_to_five(
point_collection, cluster_distance
)
elif (
pointcount == 1
or isinstance(poly_shape, geometry.point.Point)
or poly_shape is None
):
poly_shape, shapetype = AlphaShapes._get_poly_one(
point_collection, cluster_distance
)
# final check for multipolygon
if (
isinstance(poly_shape, geometry.multipolygon.MultiPolygon)
or poly_shape.is_empty
):
# usually not executed
poly_shape = poly_shape.convex_hull
shapetype = "Convex Hull Final Fallback"
return poly_shape, shapetype
@staticmethod
def alpha_shape(points, alpha, centeraxis=None):
"""
Alpha Shapes Code by KEVIN DWYER, see
http://blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python/
Compute the alpha shape (concave hull) of a set
of points.
@param points: Iterable container of points.
@param alpha: alpha value to influence the
gooeyness of the border. Smaller numbers
don't fall inward as much as larger numbers.
Too large, and you lose everything!
with minor adaptions to Tag Maps clustering.
Notes:
see also floating point resolution issue:
https://stackoverflow.com/questions/8071382/points-left-out-when-nearby-in-scipy-spatial-delaunay
Consider replace with shapely.Delauney
http://kuanbutts.com/2017/08/17/delaunay-triangulation/
"""
if centeraxis is None:
centeraxis = False
if len(points) < 4:
# When you have a triangle,
# there is no sense
# in computing an alpha shape.
return geometry.MultiPoint(list(points)).convex_hull
def add_edge(edges, edge_points, coords, i, j):
"""
Add a line between the i-th and j-th points,
if not in the list already
"""
if (i, j) in edges or (j, i) in edges:
# already added
return
edges.add((i, j))
edge_points.append(coords[[i, j]])
coords = np.array([point.coords[0] for point in points])
# qhull_options = 'Qt'
# #To avoid this error,
# joggle the data by specifying the
# 'QJ' option to the DELAUNAY function.see:
# https://de.mathworks.com/matlabcentral/answers/94438-why-does-the-delaunay-function-in-matlab-7-0-r14-produce-an-error-when-passed-colinear-points?s_tid=gn_loc_drop
if centeraxis:
# Center axis for avoiding floating point precision issues
norm_co = coords[0]
coords = coords - norm_co
tri = Delaunay(coords)
# print problematic points:
# print(tri.coplanar)
# tri = Delaunay(coords,qhull_topions='QJ')
# #Version 3.1 added triangulated output ('Qt').
# It should be used for Delaunay triangulations
# instead of using joggled input ('QJ').
edges = set()
edge_points = []
# loop over triangles:
# vert_ia, ib, ic = indices of corner points of the
# triangle
for vert_ia, vert_ib, vert_ic in tri.simplices:
pa_v = coords[vert_ia]
pb_v = coords[vert_ib]
pc_v = coords[vert_ic]
# Lengths of sides of triangle
a_val = math.sqrt((pa_v[0] - pb_v[0]) ** 2 + (pa_v[1] - pb_v[1]) ** 2)
b_val = math.sqrt((pb_v[0] - pc_v[0]) ** 2 + (pb_v[1] - pc_v[1]) ** 2)
c_val = math.sqrt((pc_v[0] - pa_v[0]) ** 2 + (pc_v[1] - pa_v[1]) ** 2)
# Semiperimeter of triangle
s_res = (a_val + b_val + c_val) / 2.0
# Area of triangle by Heron's formula
try:
area = math.sqrt(
s_res * (s_res - a_val) * (s_res - b_val) * (s_res - c_val)
)
except ValueError:
return False
if area == 0:
return False
circum_r = a_val * b_val * c_val / (4.0 * area)
# Here's the radius filter.
# print circum_r
if circum_r < 1.0 / alpha:
add_edge(edges, edge_points, coords, vert_ia, vert_ib)
add_edge(edges, edge_points, coords, vert_ib, vert_ic)
add_edge(edges, edge_points, coords, vert_ic, vert_ia)
if centeraxis:
# return to original axis center
edge_points = [
np.array([edgepoint[0] + norm_co, edgepoint[1] + norm_co])
for edgepoint in edge_points
]
mmulti_line_str = geometry.MultiLineString(edge_points)
triangles = [geom for geom in polygonize(mmulti_line_str.geoms)]
return unary_union(triangles) # , edge_points
# return geometry.polygon.asPolygon(edge_points,holes=None)
Classes
class AlphaShapes
-
Converts (cluster) point clouds to shapes
Expand source code
class AlphaShapes: """Converts (cluster) point clouds to shapes""" @staticmethod def get_single_cluster_shape( item: ItemCounter, single_post, cluster_distance: float, proj_coords ): """Get Shapes for items with no clusters Will return a buffer based on cluster distance """ shapetype = "Single cluster" # project lat/lng to UTM point_x, point_y = proj_coords(single_post.lng, single_post.lat) pcoordinate = geometry.Point(point_x, point_y) # single dots are presented # as buffers with 0.5% of width-area result_polygon = pcoordinate.buffer(cluster_distance / 4, resolution=3) # result_polygon = pcoordinate.buffer( # min(distXLng,distYLat)/100, # resolution=3) if result_polygon is None or result_polygon.is_empty: return None # append statistics for item with no cluster single_cluster_result = AlphaShapesAndMeta( result_polygon, 1, max(single_post.post_views_count, single_post.post_like_count), 1, item.name, item.ucount, 1, 1, 1, shapetype, ) return single_cluster_result @staticmethod def get_cluster_shape( item: ItemCounter, clustered_post_guids, cleaned_post_dict, cluster_distance: float, local_saturation_check, proj_coords, ): """Returns alpha shapes and tag_area (sqm) for a point cloud""" # we define a new list of Temp Alpha Shapes outside the loop, # so that it is not overwritten each time alphashapes_and_meta_tmp = list() item_area = 0 for post_guids in clustered_post_guids: # for each cluster for this toptag posts = [cleaned_post_dict[x] for x in post_guids] post_count = len(post_guids) unique_user_count = len({post.user_guid for post in posts}) sum_views = sum([post.post_views_count for post in posts]) # needs to be moved to CompileOutput: weightsv1 = Compile.get_weight(1, post_count, unique_user_count) weightsv2 = Compile.get_weight(2, post_count, unique_user_count) weightsv3 = Compile.get_weight(3, post_count, unique_user_count) distinct_locations = {post.loc_id for post in posts} # simple list comprehension with projection: points = [ geometry.Point( proj_coords( float(location.split(":")[1]), float(location.split(":")[0]) ) ) for location in distinct_locations ] # get poly shape from points result = AlphaShapes._get_poly(points, cluster_distance) result_polygon = result[0] shapetype = result[1] # Geom, Join_Count, Views, COUNT_User,ImpTag,TagCountG,HImpTag if result_polygon is not None and not result_polygon.is_empty: if local_saturation_check: item_area += result_polygon.area alphashapedata = AlphaShapesAndMeta( result_polygon, post_count, sum_views, unique_user_count, item.name, item.ucount, weightsv1, weightsv2, weightsv3, shapetype, ) alphashapes_and_meta_tmp.append(alphashapedata) if alphashapes_and_meta_tmp: # finally sort and append all # cluster shapes for this tag alphashapes_and_meta_tmp = sorted( alphashapes_and_meta_tmp, key=lambda x: -x.weightsv1 ) return AlphaShapesArea(alphashapes_and_meta_tmp, item_area) @staticmethod def _get_poly_five_to_ten(point_collection, cluster_distance): """convex hull for small point clouds""" result_polygon = point_collection.convex_hull result_polygon = result_polygon.buffer(cluster_distance / 4, resolution=3) shapetype = "between 5 and 10 points_convexHull" # result_polygon = result_polygon.buffer( # min(distXLng,distYLat)/100,resolution=3) return result_polygon, shapetype @staticmethod def _get_poly_two_to_five(point_collection, cluster_distance): """convex hull for tiny point clouds""" shapetype = "between 2 and 5 points_buffer" # calc distance between points, see # http://www.mathwarehouse.com/algebra/distance_formula/index.php # bdist = math.sqrt( # (points[0].coords.xy[0][0]-points[1].coords.xy[0][0])**2 # + (points[0].coords.xy[1][0]-points[1].coords.xy[1][0])**2) # print(str(bdist)) # single dots are presented as buffer with 0.5% of width-area: poly_shape = point_collection.buffer(cluster_distance / 4, resolution=3) poly_shape = poly_shape.convex_hull # result_polygon = point_collection.buffer( # min(distXLng,distYLat)/100,resolution=3) # #single dots are presented as buffer with 0.5% of width-area return poly_shape, shapetype @staticmethod def _get_poly_larger_ten(points, pointcount, cluster_distance, point_collection): """Get poly for larger point clouds This is still recursive trial & error Repeat generating alpha shapes with smaller alpha value if Multigon is generated. Smaller alpha values mean less granularity of resulting polygon, but too large alpha may result in empty polygon (this branch is sometimes executed for larger scales) """ if pointcount > 500: startalpha = 1000000 elif pointcount > 200: startalpha = 10000 else: startalpha = 9000 # concave hull/alpha shape /50000: poly_shape = AlphaShapes.alpha_shape( points, alpha=cluster_distance / startalpha ) shapetype = "Initial Alpha Shape + Buffer" if not isinstance(poly_shape, bool) and poly_shape.is_empty: # try again with centered axis reduced alpha poly_shape = AlphaShapes.alpha_shape( points, alpha=(cluster_distance / startalpha), centeraxis=True ) # if not poly_shape.is_empty: # print("Success") # check type comparison here if isinstance(poly_shape, (bool, geometry.multipolygon.MultiPolygon)): for i in range(1, 6): # try decreasing alpha # ** means cube alpha = startalpha + (startalpha * (i**i)) # /100000 poly_shape = AlphaShapes.alpha_shape( points, alpha=cluster_distance / alpha ) if not ( isinstance(poly_shape, geometry.multipolygon.MultiPolygon) and not isinstance(poly_shape, bool) ): shapetype = "Multipolygon Alpha Shape /" + str(alpha) break if isinstance(poly_shape, (bool, geometry.multipolygon.MultiPolygon)): # try increasing alpha for i in range(1, 6): # try decreasing alpha alpha = startalpha / (i * i) # /100000 poly_shape = AlphaShapes.alpha_shape( points, alpha=cluster_distance / alpha ) if not ( isinstance(poly_shape, geometry.multipolygon.MultiPolygon) and not isinstance(poly_shape, bool) ): shapetype = "Multipolygon Alpha Shape /" + str(alpha) break if isinstance(poly_shape, geometry.multipolygon.MultiPolygon): shapetype = "Multipolygon Alpha Shape -> Convex Hull" # if still of type multipolygon, # try to remove holes and do a convex_hull poly_shape = poly_shape.convex_hull # OR: in case there was a problem with generating # alpha shapes # (circum_r = a*b*c/(4.0*area) # --> ZeroDivisionError: float division by zero) # this branch is rarely executed # for large point clusters where # alpha is perhaps set too small if ( isinstance(poly_shape, bool) or poly_shape.is_empty or AlphaShapes._few_polyinpoints(points, poly_shape) ): shapetype = "BoolAlpha/Empty -> Fallback to PointCloud Convex Hull" # convex hull poly_shape = point_collection.convex_hull # Finally do a buffer to smooth alpha poly_shape = poly_shape.buffer(cluster_distance / 4, resolution=3) # result_polygon = result_polygon.buffer( # min(distXLng,distYLat)/100,resolution=3) return poly_shape, shapetype @staticmethod def _few_polyinpoints(points, poly): """Check how many of original points are in Alpha Because some QHull Delauney return a small Alpha Shape that does not include most points, this is a fallback solution. Better solution would be to revise QHull Delauney/Alpha, but even with centered axis, this issue remains (see alpha_shape). """ p_count = 1 for point in points: if point.within(poly): p_count += 1 perc = p_count / (len(points) / 100) if perc < 50: return True return False @staticmethod def _get_poly_one(point_collection, cluster_distance): shapetype = "1 point cluster" # single dots are presented as buffer # with 0.5% of width-area poly_shape = point_collection.buffer(cluster_distance / 4, resolution=3) # result_polygon = point_collection.buffer( # min(distXLng,distYLat)/100,resolution=3) # #single dots are presented as buffer # with 0.5% of width-area return poly_shape, shapetype @staticmethod def _get_poly(points, cluster_distance): """Get polygon/shape for collection of geooordinates (points) Returns: Shape (Polygon) Shapetype (Approach of Shape-Gen) """ point_collection = geometry.MultiPoint(list(points)) poly_shape = None pointcount = len(points) shapetype: str = "" if pointcount >= 5: if pointcount < 10: poly_shape, shapetype = AlphaShapes._get_poly_five_to_ten( point_collection, cluster_distance ) else: poly_shape, shapetype = AlphaShapes._get_poly_larger_ten( points, pointcount, cluster_distance, point_collection ) elif 2 <= pointcount < 5: poly_shape, shapetype = AlphaShapes._get_poly_two_to_five( point_collection, cluster_distance ) elif ( pointcount == 1 or isinstance(poly_shape, geometry.point.Point) or poly_shape is None ): poly_shape, shapetype = AlphaShapes._get_poly_one( point_collection, cluster_distance ) # final check for multipolygon if ( isinstance(poly_shape, geometry.multipolygon.MultiPolygon) or poly_shape.is_empty ): # usually not executed poly_shape = poly_shape.convex_hull shapetype = "Convex Hull Final Fallback" return poly_shape, shapetype @staticmethod def alpha_shape(points, alpha, centeraxis=None): """ Alpha Shapes Code by KEVIN DWYER, see http://blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python/ Compute the alpha shape (concave hull) of a set of points. @param points: Iterable container of points. @param alpha: alpha value to influence the gooeyness of the border. Smaller numbers don't fall inward as much as larger numbers. Too large, and you lose everything! with minor adaptions to Tag Maps clustering. Notes: see also floating point resolution issue: https://stackoverflow.com/questions/8071382/points-left-out-when-nearby-in-scipy-spatial-delaunay Consider replace with shapely.Delauney http://kuanbutts.com/2017/08/17/delaunay-triangulation/ """ if centeraxis is None: centeraxis = False if len(points) < 4: # When you have a triangle, # there is no sense # in computing an alpha shape. return geometry.MultiPoint(list(points)).convex_hull def add_edge(edges, edge_points, coords, i, j): """ Add a line between the i-th and j-th points, if not in the list already """ if (i, j) in edges or (j, i) in edges: # already added return edges.add((i, j)) edge_points.append(coords[[i, j]]) coords = np.array([point.coords[0] for point in points]) # qhull_options = 'Qt' # #To avoid this error, # joggle the data by specifying the # 'QJ' option to the DELAUNAY function.see: # https://de.mathworks.com/matlabcentral/answers/94438-why-does-the-delaunay-function-in-matlab-7-0-r14-produce-an-error-when-passed-colinear-points?s_tid=gn_loc_drop if centeraxis: # Center axis for avoiding floating point precision issues norm_co = coords[0] coords = coords - norm_co tri = Delaunay(coords) # print problematic points: # print(tri.coplanar) # tri = Delaunay(coords,qhull_topions='QJ') # #Version 3.1 added triangulated output ('Qt'). # It should be used for Delaunay triangulations # instead of using joggled input ('QJ'). edges = set() edge_points = [] # loop over triangles: # vert_ia, ib, ic = indices of corner points of the # triangle for vert_ia, vert_ib, vert_ic in tri.simplices: pa_v = coords[vert_ia] pb_v = coords[vert_ib] pc_v = coords[vert_ic] # Lengths of sides of triangle a_val = math.sqrt((pa_v[0] - pb_v[0]) ** 2 + (pa_v[1] - pb_v[1]) ** 2) b_val = math.sqrt((pb_v[0] - pc_v[0]) ** 2 + (pb_v[1] - pc_v[1]) ** 2) c_val = math.sqrt((pc_v[0] - pa_v[0]) ** 2 + (pc_v[1] - pa_v[1]) ** 2) # Semiperimeter of triangle s_res = (a_val + b_val + c_val) / 2.0 # Area of triangle by Heron's formula try: area = math.sqrt( s_res * (s_res - a_val) * (s_res - b_val) * (s_res - c_val) ) except ValueError: return False if area == 0: return False circum_r = a_val * b_val * c_val / (4.0 * area) # Here's the radius filter. # print circum_r if circum_r < 1.0 / alpha: add_edge(edges, edge_points, coords, vert_ia, vert_ib) add_edge(edges, edge_points, coords, vert_ib, vert_ic) add_edge(edges, edge_points, coords, vert_ic, vert_ia) if centeraxis: # return to original axis center edge_points = [ np.array([edgepoint[0] + norm_co, edgepoint[1] + norm_co]) for edgepoint in edge_points ] mmulti_line_str = geometry.MultiLineString(edge_points) triangles = [geom for geom in polygonize(mmulti_line_str.geoms)] return unary_union(triangles) # , edge_points # return geometry.polygon.asPolygon(edge_points,holes=None)
Static methods
def alpha_shape(points, alpha, centeraxis=None)
-
Alpha Shapes Code by KEVIN DWYER, see http://blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python/ Compute the alpha shape (concave hull) of a set of points. @param points: Iterable container of points. @param alpha: alpha value to influence the gooeyness of the border. Smaller numbers don't fall inward as much as larger numbers. Too large, and you lose everything!
with minor adaptions to Tag Maps clustering.
Notes: see also floating point resolution issue: https://stackoverflow.com/questions/8071382/points-left-out-when-nearby-in-scipy-spatial-delaunay
Consider replace with shapely.Delauney http://kuanbutts.com/2017/08/17/delaunay-triangulation/
Expand source code
@staticmethod def alpha_shape(points, alpha, centeraxis=None): """ Alpha Shapes Code by KEVIN DWYER, see http://blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python/ Compute the alpha shape (concave hull) of a set of points. @param points: Iterable container of points. @param alpha: alpha value to influence the gooeyness of the border. Smaller numbers don't fall inward as much as larger numbers. Too large, and you lose everything! with minor adaptions to Tag Maps clustering. Notes: see also floating point resolution issue: https://stackoverflow.com/questions/8071382/points-left-out-when-nearby-in-scipy-spatial-delaunay Consider replace with shapely.Delauney http://kuanbutts.com/2017/08/17/delaunay-triangulation/ """ if centeraxis is None: centeraxis = False if len(points) < 4: # When you have a triangle, # there is no sense # in computing an alpha shape. return geometry.MultiPoint(list(points)).convex_hull def add_edge(edges, edge_points, coords, i, j): """ Add a line between the i-th and j-th points, if not in the list already """ if (i, j) in edges or (j, i) in edges: # already added return edges.add((i, j)) edge_points.append(coords[[i, j]]) coords = np.array([point.coords[0] for point in points]) # qhull_options = 'Qt' # #To avoid this error, # joggle the data by specifying the # 'QJ' option to the DELAUNAY function.see: # https://de.mathworks.com/matlabcentral/answers/94438-why-does-the-delaunay-function-in-matlab-7-0-r14-produce-an-error-when-passed-colinear-points?s_tid=gn_loc_drop if centeraxis: # Center axis for avoiding floating point precision issues norm_co = coords[0] coords = coords - norm_co tri = Delaunay(coords) # print problematic points: # print(tri.coplanar) # tri = Delaunay(coords,qhull_topions='QJ') # #Version 3.1 added triangulated output ('Qt'). # It should be used for Delaunay triangulations # instead of using joggled input ('QJ'). edges = set() edge_points = [] # loop over triangles: # vert_ia, ib, ic = indices of corner points of the # triangle for vert_ia, vert_ib, vert_ic in tri.simplices: pa_v = coords[vert_ia] pb_v = coords[vert_ib] pc_v = coords[vert_ic] # Lengths of sides of triangle a_val = math.sqrt((pa_v[0] - pb_v[0]) ** 2 + (pa_v[1] - pb_v[1]) ** 2) b_val = math.sqrt((pb_v[0] - pc_v[0]) ** 2 + (pb_v[1] - pc_v[1]) ** 2) c_val = math.sqrt((pc_v[0] - pa_v[0]) ** 2 + (pc_v[1] - pa_v[1]) ** 2) # Semiperimeter of triangle s_res = (a_val + b_val + c_val) / 2.0 # Area of triangle by Heron's formula try: area = math.sqrt( s_res * (s_res - a_val) * (s_res - b_val) * (s_res - c_val) ) except ValueError: return False if area == 0: return False circum_r = a_val * b_val * c_val / (4.0 * area) # Here's the radius filter. # print circum_r if circum_r < 1.0 / alpha: add_edge(edges, edge_points, coords, vert_ia, vert_ib) add_edge(edges, edge_points, coords, vert_ib, vert_ic) add_edge(edges, edge_points, coords, vert_ic, vert_ia) if centeraxis: # return to original axis center edge_points = [ np.array([edgepoint[0] + norm_co, edgepoint[1] + norm_co]) for edgepoint in edge_points ] mmulti_line_str = geometry.MultiLineString(edge_points) triangles = [geom for geom in polygonize(mmulti_line_str.geoms)] return unary_union(triangles) # , edge_points # return geometry.polygon.asPolygon(edge_points,holes=None)
def get_cluster_shape(item: ItemCounter, clustered_post_guids, cleaned_post_dict, cluster_distance: float, local_saturation_check, proj_coords)
-
Returns alpha shapes and tag_area (sqm) for a point cloud
Expand source code
@staticmethod def get_cluster_shape( item: ItemCounter, clustered_post_guids, cleaned_post_dict, cluster_distance: float, local_saturation_check, proj_coords, ): """Returns alpha shapes and tag_area (sqm) for a point cloud""" # we define a new list of Temp Alpha Shapes outside the loop, # so that it is not overwritten each time alphashapes_and_meta_tmp = list() item_area = 0 for post_guids in clustered_post_guids: # for each cluster for this toptag posts = [cleaned_post_dict[x] for x in post_guids] post_count = len(post_guids) unique_user_count = len({post.user_guid for post in posts}) sum_views = sum([post.post_views_count for post in posts]) # needs to be moved to CompileOutput: weightsv1 = Compile.get_weight(1, post_count, unique_user_count) weightsv2 = Compile.get_weight(2, post_count, unique_user_count) weightsv3 = Compile.get_weight(3, post_count, unique_user_count) distinct_locations = {post.loc_id for post in posts} # simple list comprehension with projection: points = [ geometry.Point( proj_coords( float(location.split(":")[1]), float(location.split(":")[0]) ) ) for location in distinct_locations ] # get poly shape from points result = AlphaShapes._get_poly(points, cluster_distance) result_polygon = result[0] shapetype = result[1] # Geom, Join_Count, Views, COUNT_User,ImpTag,TagCountG,HImpTag if result_polygon is not None and not result_polygon.is_empty: if local_saturation_check: item_area += result_polygon.area alphashapedata = AlphaShapesAndMeta( result_polygon, post_count, sum_views, unique_user_count, item.name, item.ucount, weightsv1, weightsv2, weightsv3, shapetype, ) alphashapes_and_meta_tmp.append(alphashapedata) if alphashapes_and_meta_tmp: # finally sort and append all # cluster shapes for this tag alphashapes_and_meta_tmp = sorted( alphashapes_and_meta_tmp, key=lambda x: -x.weightsv1 ) return AlphaShapesArea(alphashapes_and_meta_tmp, item_area)
def get_single_cluster_shape(item: ItemCounter, single_post, cluster_distance: float, proj_coords)
-
Get Shapes for items with no clusters Will return a buffer based on cluster distance
Expand source code
@staticmethod def get_single_cluster_shape( item: ItemCounter, single_post, cluster_distance: float, proj_coords ): """Get Shapes for items with no clusters Will return a buffer based on cluster distance """ shapetype = "Single cluster" # project lat/lng to UTM point_x, point_y = proj_coords(single_post.lng, single_post.lat) pcoordinate = geometry.Point(point_x, point_y) # single dots are presented # as buffers with 0.5% of width-area result_polygon = pcoordinate.buffer(cluster_distance / 4, resolution=3) # result_polygon = pcoordinate.buffer( # min(distXLng,distYLat)/100, # resolution=3) if result_polygon is None or result_polygon.is_empty: return None # append statistics for item with no cluster single_cluster_result = AlphaShapesAndMeta( result_polygon, 1, max(single_post.post_views_count, single_post.post_like_count), 1, item.name, item.ucount, 1, 1, 1, shapetype, ) return single_cluster_result
class AlphaShapesAndMeta (shape: geometry.Polygon, post_count: int, views: int, user_count: int, item_name: str, item_totalcount: int, weightsv1: float, weightsv2: float, weightsv3: float, shapetype: str)
-
Stores cluster geometry and calculated summary statistics
Expand source code
@dataclass class AlphaShapesAndMeta: """Stores cluster geometry and calculated summary statistics""" shape: geometry.Polygon post_count: int views: int user_count: int item_name: str item_totalcount: int weightsv1: float weightsv2: float weightsv3: float shapetype: str def __getitem__(self, key): """Implements subscription by name""" return getattr(self, key)
Class variables
var item_name : str
var item_totalcount : int
var post_count : int
var shape : shapely.geometry.polygon.Polygon
var shapetype : str
var user_count : int
var views : int
var weightsv1 : float
var weightsv2 : float
var weightsv3 : float
class AlphaShapesArea (alphashape: Optional[List[AlphaShapesAndMeta]], item_area: float)
-
Collection of of Alpha Shapes (list of geometries and meta) and total area
Expand source code
@dataclass class AlphaShapesArea: """Collection of of Alpha Shapes (list of geometries and meta) and total area """ alphashape: Optional[List[AlphaShapesAndMeta]] item_area: float
Class variables
var alphashape : Optional[List[AlphaShapesAndMeta]]
var item_area : float