Skip to content

Commit 50e23ba

Browse files
authored
Merge pull request #116 from eEcoLiDAR/development
Development
2 parents 06b6b46 + a75e8c8 commit 50e23ba

13 files changed

+256
-61
lines changed

.idea/eEcoLiDAR.iml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
5+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
### Added
9+
- General tests that all current and future feature extractors will be checked against.
10+
11+
## Changed
12+
13+
## Fixed
14+
- Fixed many feature extractors for corner cases (e.g. zero points)
15+
16+
## Removed
17+
18+
## [0.1.0] - 2018-04-17

laserchicken/compute_neighbors.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ def compute_cylinder_neighborhood(environment_pc, target_pc, radius):
4141
if cyl_size > mem_size * MEMORY_THRESHOLD:
4242
y = target_pc[point]['y']['data']
4343

44-
num_points = math.floor(mem_size * MEMORY_THRESHOLD / \
45-
(avg_points_cyl * sys.getsizeof(int)))
46-
print("Number of points: %f" % num_points)
44+
num_points = int(math.floor(mem_size * MEMORY_THRESHOLD / (avg_points_cyl * sys.getsizeof(int))))
45+
print("Number of points: %d" % num_points)
4746

4847
env_tree = kd_tree.get_kdtree_for_pc(environment_pc)
4948

@@ -102,7 +101,6 @@ def compute_neighborhoods(env_pc, target_pc, volume_description):
102101
:return: indices of neighboring points from the environment point cloud for each target point
103102
"""
104103
volume_type = volume_description.get_type()
105-
neighbors = []
106104
if volume_type == Sphere.TYPE:
107105
neighbors = compute_sphere_neighborhood(
108106
env_pc, target_pc, volume_description.radius)

laserchicken/feature_extractor/density_feature_extractor.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,20 @@ def provides(cls):
3131
"""
3232
return ['point_density']
3333

34-
def extract(self, sourcepc, neighborhood, targetpc, targetindex, volume):
34+
def extract(self, source_point_cloud, neighborhood, target_point_cloud, target_index, volume):
3535
"""
36-
Extract the feature value(s) of the point cloud at location of the target.
36+
Extract either the surface density or volume density depending on the volume type.
3737
38-
:param point_cloud: environment (search space) point cloud
38+
:param source_point_cloud: environment (search space) point cloud
3939
:param neighborhood: array of indices of points within the point_cloud argument
4040
:param target_point_cloud: point cloud that contains target point
4141
:param target_index: index of the target point in the target pointcloud
42-
:param volume_description: volume object that describes the shape and size of the search volume
42+
:param volume: volume object that describes the shape and size of the search volume
4343
:return: feature value
4444
"""
4545

46-
if sourcepc is not None and isinstance(neighborhood, list):
47-
npts = float(len(sourcepc[point]['x']['data'][neighborhood]))
46+
npts = float(len(neighborhood))
4847

49-
elif targetpc is not None:
50-
npts = float(len(targetpc[point]['x']['data']))
51-
else:
52-
raise ValueError("You can either specify a sourcepc and a neighborhhod or a targetpc\n\
53-
example\nextractror.extract(sourcepc,index,None,None,volume)\n\
54-
extractror.extract(None,None,targetpc,None,volume)")
55-
56-
# sphere/cylinder cases
5748
if volume.get_type() == Sphere.TYPE:
5849
vol = volume.calculate_volume()
5950
return npts / vol

laserchicken/feature_extractor/entropy_feature_extractor.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ def get_params(self):
2828
return p
2929

3030
def extract(self, source_pc, neighborhood, target_pc, target_index, volume_description):
31+
if len(neighborhood) == 0:
32+
return 0
3133
z = source_pc[keys.point]["z"]["data"][neighborhood]
3234
_z_min = np.min(z) if self.z_min is None else self.z_min
3335
_z_max = np.max(z) if self.z_max is None else self.z_max
34-
if (_z_min == _z_max):
35-
return 0
36+
if _z_min == _z_max:
37+
return 0
3638
n_bins = int(np.ceil((_z_max - _z_min) / self.layer_thickness))
37-
data = np.histogram(z, bins=n_bins, range=(
38-
_z_min, _z_max), density=True)[0]
39+
data = np.histogram(z, bins=n_bins, range=(_z_min, _z_max), density=True)[0]
3940
entropy_func = np.vectorize(_x_log_2x)
4041
norm = np.sum(data)
4142
return -(entropy_func(data / norm)).sum()

laserchicken/feature_extractor/height_statistics_feature_extractor.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77

88
class HeightStatisticsFeatureExtractor(AbstractFeatureExtractor):
9+
DEFAULT_MAX = float('NaN')
10+
DEFAULT_MIN = float('NaN')
11+
912
@classmethod
1013
def requires(cls):
1114
return []
@@ -15,15 +18,18 @@ def provides(cls):
1518
return ['max_z', 'min_z', 'mean_z', 'median_z', 'std_z', 'var_z', 'range', 'coeff_var_z', 'skew_z', 'kurto_z']
1619

1720
def extract(self, sourcepc, neighborhood, targetpc, targetindex, volume_description):
18-
z = sourcepc[point]['z']['data'][neighborhood]
19-
max_z = np.max(z)
20-
min_z = np.min(z)
21-
mean_z = np.mean(z)
22-
median_z = np.median(z)
23-
std_z = np.std(z)
24-
var_z = np.var(z)
25-
range_z = max_z - min_z
26-
coeff_var_z = np.std(z) / np.mean(z)
27-
skew_z = stat.skew(z)
28-
kurto_z = stat.kurtosis(z)
21+
if neighborhood:
22+
z = sourcepc[point]['z']['data'][neighborhood]
23+
max_z = np.max(z) if len(z) > 0 else self.DEFAULT_MAX
24+
min_z = np.min(z) if len(z) > 0 else self.DEFAULT_MIN
25+
mean_z = np.mean(z)
26+
median_z = np.median(z)
27+
std_z = np.std(z)
28+
var_z = np.var(z)
29+
range_z = max_z - min_z
30+
coeff_var_z = np.std(z) / np.mean(z)
31+
skew_z = stat.skew(z)
32+
kurto_z = stat.kurtosis(z)
33+
else:
34+
max_z = min_z = mean_z = median_z = std_z = var_z = range_z = coeff_var_z = skew_z = kurto_z = np.NaN
2935
return max_z, min_z, mean_z, median_z, std_z, var_z, range_z, coeff_var_z, skew_z, kurto_z

laserchicken/feature_extractor/normal_plane_feature_extractor.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,30 @@ def provides(cls):
3232
"""
3333
return ['normal_vector_1', 'normal_vector_2', 'normal_vector_3', 'slope']
3434

35-
def extract(self, sourcepc, neighborhood, targetpc, targetindex, volume):
35+
def extract(self, point_cloud, neighborhood, target_point_cloud, target_index, volume_description):
3636
"""
3737
Extract the feature value(s) of the point cloud at location of the target.
3838
3939
:param point_cloud: environment (search space) point cloud
4040
:param neighborhood: array of indices of points within the point_cloud argument
4141
:param target_point_cloud: point cloud that contains target point
42-
:param target_index: index of the target point in the target pointcloud
42+
:param target_index: index of the target point in the target point cloud
4343
:param volume_description: volume object that describes the shape and size of the search volume
4444
:return: feature value
4545
"""
4646

47-
x = sourcepc[point]['x']['data'][neighborhood]
48-
y = sourcepc[point]['y']['data'][neighborhood]
49-
z = sourcepc[point]['z']['data'][neighborhood]
47+
x = _to_float64(point_cloud[point]['x']['data'][neighborhood])
48+
y = _to_float64(point_cloud[point]['y']['data'][neighborhood])
49+
z = _to_float64(point_cloud[point]['z']['data'][neighborhood])
5050

51-
nvect = fit_plane_svd(x, y, z)
52-
slope = np.dot(nvect, np.array([0., 0., 1.]))
51+
try:
52+
normal_vector = fit_plane_svd(x, y, z)
53+
slope = np.dot(normal_vector, np.array([0., 0., 1.]))
54+
except np.linalg.linalg.LinAlgError:
55+
normal_vector = [0.0, 0.0, 0.0]
56+
slope = 0.0
5357

54-
return nvect[0], nvect[1], nvect[2], slope
58+
return normal_vector[0], normal_vector[1], normal_vector[2], slope
5559

5660
def get_params(self):
5761
"""
@@ -60,3 +64,13 @@ def get_params(self):
6064
Needed for provenance.
6165
"""
6266
return ()
67+
68+
69+
def _to_float64(x_org):
70+
"""
71+
Returns the input if dtype is float64 or a copy of it with this dtype if it isn't.
72+
NB. Returns reference to input array or copy.
73+
:param x_org:
74+
:return: array with float64 dtype
75+
"""
76+
return x_org if x_org.dtype == np.float64 else np.array(x_org, dtype=np.float64)

laserchicken/feature_extractor/pulse_penetration_feature_extractor.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
from laserchicken.feature_extractor.abc import AbstractFeatureExtractor
99
from laserchicken.keys import point
1010

11+
# classification according to
12+
# http://www.asprs.org/wp-content/uploads/2010/12/LAS_1-4_R6.pdf
13+
GROUND_TAGS = [2]
14+
1115

1216
class PulsePenetrationFeatureExtractor(AbstractFeatureExtractor):
1317
"""Feature extractor for the point density."""
1418

15-
# classification according to
16-
# http://www.asprs.org/wp-content/uploads/2010/12/LAS_1-4_R6.pdf
17-
ground_tags = [2]
18-
1919
@classmethod
2020
def requires(cls):
2121
"""
@@ -52,10 +52,14 @@ def extract(self, point_cloud, neighborhood, target_point_cloud, target_index, v
5252
:param volume_description: volume object that describes the shape and size of the search volume
5353
:return: feature value
5454
"""
55+
if 'raw_classification' not in point_cloud[point]:
56+
raise ValueError(
57+
'Missing raw_classification attribute which is necessary for calculating pulse_penetratio and '
58+
'density_absolute_mean features.')
59+
5560
class_neighbors = [point_cloud[point]['raw_classification']["data"][n] for n in neighborhood]
5661

57-
ground_indices = self._get_ground_indices(
58-
class_neighbors, self.ground_tags)
62+
ground_indices = self._get_ground_indices(class_neighbors, GROUND_TAGS)
5963

6064
pulse_penetration_ratio = self._get_pulse_penetration_ratio(
6165
ground_indices, class_neighbors)
@@ -74,7 +78,7 @@ def _get_ground_indices(point_cloud, ground_tags):
7478

7579
@staticmethod
7680
def _get_pulse_penetration_ratio(ground_indices, class_neighbors):
77-
n_total = len(class_neighbors)
81+
n_total = np.max((len(class_neighbors), 1))
7882
n_ground = len(ground_indices)
7983
return float(n_ground) / n_total
8084

laserchicken/feature_extractor/sigma_z_feature_extractor.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
See https://github.com/eEcoLiDAR/eEcoLiDAR/issues/20
55
"""
66
import numpy as np
7+
from numpy.linalg import LinAlgError
78

89
from laserchicken.feature_extractor.abc import AbstractFeatureExtractor
910
from laserchicken.utils import get_point, fit_plane
@@ -49,9 +50,12 @@ def extract(self, source_point_cloud, neighborhood, target_point_cloud, target_i
4950
:return:
5051
"""
5152
x, y, z = get_point(source_point_cloud, neighborhood)
52-
plane_estimator = fit_plane(x, y, z)
53-
normalized = z - plane_estimator(x, y)
54-
return np.std(normalized)
53+
try:
54+
plane_estimator = fit_plane(x, y, z)
55+
normalized = z - plane_estimator(x, y)
56+
return np.std(normalized)
57+
except LinAlgError:
58+
return 0
5559

5660
def get_params(self):
5761
"""

0 commit comments

Comments
 (0)