# BEGIN OF LICENSE NOTE
# This file is part of Pyoints.
# Copyright (c) 2018, Sebastian Lamprecht, Trier University,
# lamprecht@uni-trier.de
#
# Pyoints 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 3 of the License, or
# (at your option) any later version.
#
# Pyoints is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Pyoints. If not, see <https://www.gnu.org/licenses/>.
# END OF LICENSE NOTE
"""Point filters.
"""
import numpy as np
from numbers import Number
from . import (
assertion,
distance,
interpolate,
IndexKD,
transformation,
vector,
)
from .misc import print_rounded
[docs]def extrema(indexKD, attributes, r, inverse=False):
"""Finds local maxima or minima of given point set.
Parameters
----------
indexKD : IndexKD
Spatial index of `n` points to filter.
attributes : array_like(Number, shape=(n))
Attributes to search for extrema. If none, the last coordinate
dimension is used as the attribute.
r : positive Number
Maximum distance between two points to be considered as neighbours.
inverse : optional, bool
Indicates if local maxima (False) or local minima (True) shall be
yielded.
Yields
------
positive int
Indices of local maxima or minima.
Examples
--------
Find local maxima.
>>> indexKD = IndexKD([(0, 0), (0, 1), (1, 1), (1, 0), (0.5, 0.5) ])
>>> attributes = [2, 0.1, 1, 0, 0.5]
>>> fIds = list(extrema(indexKD, attributes, 1.1))
>>> print_rounded(fIds)
[0 2]
Find local minima.
>>> fIds = list(extrema(indexKD, attributes, 1.1, inverse=True))
>>> print_rounded(fIds)
[3 1]
>>> fIds = list(extrema(indexKD, attributes, 1.5, inverse=True))
>>> print_rounded(fIds)
[3]
"""
# type validation
if not isinstance(indexKD, IndexKD):
raise TypeError("'indexKD' needs to be an instance of 'IndexKD'")
if not (assertion.isnumeric(r) and r > 0):
raise ValueError("'r' needs to be a number greater zero")
attributes = assertion.ensure_numvector(attributes, length=len(indexKD))
if not inverse:
attributes = -attributes
coords = indexKD.coords
order = np.argsort(attributes)
not_classified = np.ones(len(order), dtype=np.bool)
# define filter function
def check(pId, nIds):
value = attributes[pId]
for nId in nIds:
if attributes[nId] < value:
return False
return True
# filtering
for pId in order:
if not_classified[pId]:
nIds = indexKD.ball(coords[pId, :], r)
not_classified[nIds] = False
if check(pId, nIds):
yield pId
[docs]def min_filter(indexKD, attributes, r, inverse=False):
"""Finds minima or maxima within a specified radius for all points. For
each point the neighbouring point with extreme attribute is marked.
Parameters
----------
indexKD : IndexKD
Spatial index of `n` points to filter.
attributes : array_like(Number, shape=(n))
Attributes to search for extrema. If none, the last coordinate
dimension is used as the attribute.
r : positive Number
Local radius to search for a minimum.
inverse : optional, bool
Indicates if minima (False) or maxima (True) shall be identfied.
Returns
-------
np.ndarray(int, shape=(n))
Filtered point indices.
"""
# type validation
if not isinstance(indexKD, IndexKD):
raise TypeError("'indexKD' needs to be of type 'IndexKD'")
if not (assertion.isnumeric(r) and r > 0):
raise ValueError("'r' needs to be a number greater zero")
attributes = assertion.ensure_numvector(attributes, length=len(indexKD))
if inverse:
attributes = -attributes
# filtering
ballIter = indexKD.ball_iter(indexKD.coords, r, bulk=1000)
mask = np.zeros(len(indexKD), dtype=bool)
for nIds in ballIter:
nId = nIds[np.argmin(attributes[nIds])]
mask[nId] = True
return mask.nonzero()
[docs]def has_neighbour(indexKD, r):
"""Filters points which have neighbours within a specific radius.
Parameters
----------
indexKD : IndexKD
Spatial index of `n` points to filter.
r : positive Number
Maximum distance of a point to a neighbouring point to be still
considered as isolated.
Yields
------
positive int
Index of a point with at least one neighbouring point with radius `r`.
See also
--------
is_isolated
Examples
--------
>>> coords = [(0, 0), (0.5, 0.5), (0, 1), (0.7, 0.5), (-1, -1)]
>>> indexKD = IndexKD(coords)
>>> print_rounded(list(has_neighbour(indexKD, 0.7)))
[1 3]
"""
if not isinstance(indexKD, IndexKD):
raise TypeError("'indexKD' needs to be of type 'IndexKD'")
if not (assertion.isnumeric(r) and r > 0):
raise ValueError("'r' needs to be a number greater zero")
not_classified = np.ones(len(indexKD), dtype=np.bool)
for pId, coord in enumerate(indexKD.coords):
if not_classified[pId]:
nIds = indexKD.ball(coord, r)
if len(nIds) > 1:
not_classified[nIds] = False
yield pId
else:
yield pId
[docs]def is_isolated(indexKD, r):
"""Filters points which have no neighbours within a specific radius.
Parameters
----------
indexKD : IndexKD
Spatial index of `n` points to filter.
r : positive Number
Maximum distance of a point to a neighbouring point to be still
considered as isolated.
Yields
------
positive int
Index of an isolated point with no neighbouring point within radius
`r`.
See Also
--------
has_neighbour
Examples
--------
>>> coords = [(0, 0), (0.5, 0.5), (0, 1), (0.7, 0.5), (-1, -1)]
>>> indexKD = IndexKD(coords)
>>> print_rounded(list(is_isolated(indexKD, 0.7)))
[0 2 4]
"""
if not isinstance(indexKD, IndexKD):
raise TypeError("'indexKD' needs to be of type 'IndexKD'")
if not (assertion.isnumeric(r) and r > 0):
raise ValueError("'r' needs to be a number greater zero")
not_classified = np.ones(len(indexKD), dtype=np.bool)
for pId, coord in enumerate(indexKD.coords):
if not_classified[pId]:
nIds = indexKD.ball(coord, r)
if len(nIds) > 1:
not_classified[nIds] = False
else:
yield pId
[docs]def ball(indexKD, r, order=None, inverse=False, axis=-1, min_pts=1):
"""Filters coordinates by radius. This algorithm is suitable to remove
duplicate points or to get an almost uniform point density.
Parameters
----------
indexKD : IndexKD
IndexKD containing `n` points to filter.
r : positive float or array_like(float, shape=(n))
Ball radius or radii to select neighbouring points.
order : optional, array_like(int, shape=(m))
Order to proceed. If m < n, only a subset of points is investigated. If
none, ordered by `axis`.
axis : optional, int
Coordinate axis to use to generate the order.
inverse : bool
Indicates whether or not the `order` is inversed.
min_pts : optional, int
Specifies how many neighbouring points within radius `r` shall be
required to yield a filtered point. This parameter can be used to
filter noisy point sets.
Yields
------
positive int
Indices of the filtered points.
Notes
-----
Within a dense point cloud, the filter guarantees the distance of
neighboured points in a range of `]r, 2*r[`.
"""
# validation
if not isinstance(indexKD, IndexKD):
raise TypeError("'indexKD' needs to be an instance of 'IndexKD'")
coords = indexKD.coords
if order is None:
order = np.argsort(coords[:, axis])[::-1]
order = assertion.ensure_indices(order, max_value=len(indexKD) - 1)
if inverse:
order = order[::-1]
if not hasattr(r, '__len__'):
r = np.repeat(r, len(indexKD))
r = assertion.ensure_numvector(r)
if not np.all(r) > 0:
raise ValueError("radius greater zero required")
# filtering
not_classified = np.ones(len(indexKD), dtype=np.bool)
for pId in order:
if not_classified[pId]:
nIds = indexKD.ball(coords[pId, :], r[pId])
if len(nIds) >= min_pts:
not_classified[nIds] = False
yield pId
[docs]def in_convex_hull(hull_coords, coords):
"""Decides whether or not points are within a convex hull.
Parameters
----------
hull_coords : array_like(Number, shape=(m, k))
Represents `m` hull points of `k` dimensions.
coords : array_like(Number, shape=(n, k))
Represents `n` data points to check whether or not they are located
within the convex hull.
Returns
-------
array_like(bool, shape=(n))
Indicates whether or not the points are located within the convex hull.
Examples
--------
>>> hull = [(0, 0), (1, 0), (1, 2)]
>>> coords = [(0, 0), (0.5, 0.5), (0, 1), (0.7, 0.5), (-1, -1)]
>>> print(in_convex_hull(hull, coords))
[ True True False True False]
"""
hull_coords = assertion.ensure_coords(hull_coords)
coords = assertion.ensure_coords(coords)
interpolator = interpolate.LinearInterpolator(
hull_coords,
np.zeros(hull_coords.shape[0])
)
return ~np.isnan(interpolator(coords))
[docs]def surface(indexKD, r, order=None, inverse=False, axis=-1):
"""Filters points associated with a surface.
Parameters
----------
indexKD : IndexKD
IndexKD containing `n` points to filter.
r : positive float or array_like(float, shape=(n))
Ball radius or radii to apply.
order : optional, array_like(int, shape=(m))
Order to proceed. If m < n, only a subset of points is investigated.
inverse : optional, bool
Indicates whether or not to inverse the order.
axis : optional, int
Axis to use for generating the order.
Yields
------
positive int
Indices of the filtered points.
"""
if not isinstance(indexKD, IndexKD):
raise TypeError("'indexKD' needs to be of type 'IndexKD'")
if not (assertion.isnumeric(r) and r > 0):
raise ValueError("'r' needs to be a number greater zero")
coords = indexKD.coords
if order is None:
order = np.argsort(coords[:, axis])[::-1]
else:
order = assertion.ensure_indices(order, max_value=len(coords) - 1)
if inverse:
order = order[::-1]
inverseOrder = np.argsort(order)
not_classified = np.zeros(len(indexKD), dtype=np.bool)
not_classified[order] = True
for pId in order:
if not_classified[pId]:
nIds = np.array(indexKD.ball(coords[pId, :], r))
not_classified[nIds] = False
yield nIds[np.argmin(inverseOrder[nIds])]
[docs]def dem_filter(coords, r, max_angle=70):
"""Selects points suitable for generating a digital elevation model.
Parameters
----------
coords : array_like(Number, shape=(n, k))
Represents `n` points of `k` dimensions to filter.
r : Number or array_like(Number, shape=(n))
Radius or radii to apply.
max_angle : Number
Maximum allowed slope of a simplex.
Returns
-------
array_like(int, shape=(m))
Desired indices of points suitable for generating a surface model.
Notes
-----
Applies a `k-1` dimensional `ball` filter to identify a digital elevation
model. The point order is defined by the last coordinate dimension. The
filtering radius `r` is defined by the user. To optimize the quality of the
dem, all points with less than 5 neighbours within radius `2 * r` are
removed from further analysis. In addition a TIN (Triangulated Irregular
Network) is generated to model the surface. Each facet with a slope larger
than `max_angle` is removed iteratively.
See Also
--------
radial_dem_filter, ball
"""
coords = assertion.ensure_coords(coords, min_dim=3)
order = np.argsort(coords[:, -1])
if not hasattr(r, '__getitem__'):
r = np.repeat(r, len(order))
r = assertion.ensure_numvector(r)
if not (isinstance(max_angle, Number) and max_angle > 0):
raise ValueError("'max_angle' needs to be a number greater zero")
# filter
ball_gen = ball(IndexKD(coords[:, :-1]), r, order=order)
fIds = np.array(list(ball_gen), dtype=int)
# ensure neighbours
count = IndexKD(coords[fIds, :]).ball_count(2 * r[fIds])
fIds = fIds[count >= 6]
# subsequent filtering of the simplices
while True:
old_len = len(fIds)
fcoords = coords[fIds, :]
dem = interpolate.LinearInterpolator(fcoords[:, :-1], fcoords[:, -1])
# check principal component of the simplices
mask = np.ones(len(fIds), dtype=bool)
for simplex_indices in dem.delaunay.simplices:
simplex = fcoords[simplex_indices, :]
eig_vec = transformation.eigen(simplex)[0][:, -1]
angle = vector.zenith(eig_vec, deg=True)
if angle > max_angle:
idx = simplex_indices[np.argmax(simplex[:, -1])]
mask[idx] = False
fIds = fIds[mask]
if old_len == len(fIds):
break
return fIds
[docs]def radial_dem_filter(coords, angle_res, center=None, max_angle=70):
"""Filters surface points based on distance to the center. The algorithm
is designed to create digital elevation models of terrestrial laser scans.
Terrestrial laser scans are characterized by decreasing point densities
with increasing distance to the scanner position. Thus, the radius to
identify neighbouring points is adjusted to the distance of the scanner.
Parameters
----------
coords : array_like(Number, shape=(n, k))
Points to filter.
angle_res : positive Number
Filter resolution expressed as an angle.
center : optional, array_like(Number, shape=(k))
Desired center.
max_angle : Number
Maximum allowed angle of a simplex.
Returns
-------
array_like(int, shape=(m))
Indices of filtered points.
See Also
--------
dem_filter
"""
coords = assertion.ensure_coords(coords, min_dim=3)
if center is None:
center = np.zeros(coords.shape[1], dtype=float)
center = assertion.ensure_numvector(center, length=3)
if not (assertion.isnumeric(angle_res) and angle_res > 0):
raise ValueError('angle greater zero required')
dist = distance.dist(center[:-1], coords[:, :-1])
radii = dist * np.sin(angle_res / 180.0 * np.pi)
fIds = dem_filter(coords, radii, max_angle=max_angle)
fIds = fIds[coords[fIds, -1] < center[-1]]
return fIds