Skip to content

Commit 44294c8

Browse files
committed
split pulse penetration ratio and dens abs mean z and norm features
1 parent 7bf3c15 commit 44294c8

7 files changed

+192
-36
lines changed

laserchicken/feature_extractor/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
from .entropy_norm_z_feature_extractor import EntropyNormZFeatureExtractor
3030
from .median_norm_z_feature_extractor import MedianNormZFeatureExtractor
3131
from .percentile_norm_z_feature_extractor import PercentileNormZFeatureExtractor
32+
from .density_absolute_mean_z_feature_extractor import DensityAbsoluteMeanZFeatureExtractor
33+
from .density_absolute_mean_norm_z_feature_extractor import DensityAbsoluteMeanNormZFeatureExtractor
3234

3335

3436
def _create_feature_map(module_name=__name__):
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Pulse penetration ratio and density absolute mean calculations.
2+
3+
See https://github.com/eEcoLiDAR/eEcoLiDAR/issues/23.
4+
"""
5+
6+
import numpy as np
7+
8+
from laserchicken.feature_extractor.abc import AbstractFeatureExtractor
9+
from laserchicken.feature_extractor.density_absolute_mean_z_feature_extractor import \
10+
DensityAbsoluteMeanZFeatureExtractor
11+
from laserchicken.keys import point, normalized_height
12+
13+
# classification according to
14+
# http://www.asprs.org/wp-content/uploads/2010/12/LAS_1-4_R6.pdf
15+
GROUND_TAGS = [2]
16+
17+
18+
class DensityAbsoluteMeanNormZFeatureExtractor(DensityAbsoluteMeanZFeatureExtractor):
19+
"""Feature extractor for the point density."""
20+
DATA_KEY = normalized_height
21+
22+
@classmethod
23+
def provides(cls):
24+
"""
25+
Get a list of names of the feature values.
26+
27+
This will return as many names as the number feature values that will be returned.
28+
For instance, if a feature extractor returns the first 3 eigen values, this method
29+
should return 3 names, for instance 'eigen_value_1', 'eigen_value_2' and 'eigen_value_3'.
30+
31+
:return: List of feature names
32+
"""
33+
return ['density_absolute_mean_norm_z']
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Pulse penetration ratio and density absolute mean calculations.
2+
3+
See https://github.com/eEcoLiDAR/eEcoLiDAR/issues/23.
4+
"""
5+
6+
import numpy as np
7+
8+
from laserchicken.feature_extractor.abc import AbstractFeatureExtractor
9+
from laserchicken.keys import point, normalized_height
10+
11+
# classification according to
12+
# http://www.asprs.org/wp-content/uploads/2010/12/LAS_1-4_R6.pdf
13+
GROUND_TAGS = [2]
14+
15+
16+
def _is_ground(i, point_cloud):
17+
return point_cloud[point]['raw_classification']["data"][i] in GROUND_TAGS
18+
19+
20+
class DensityAbsoluteMeanZFeatureExtractor(AbstractFeatureExtractor):
21+
"""Feature extractor for the point density."""
22+
DATA_KEY = 'z'
23+
24+
@classmethod
25+
def requires(cls):
26+
"""
27+
Get a list of names of the point attributes that are needed for this feature extraction.
28+
29+
For simple features, this could be just x, y, and z. Other features can build on again
30+
other features to have been computed first.
31+
32+
:return: List of feature names
33+
"""
34+
return []
35+
36+
@classmethod
37+
def provides(cls):
38+
"""
39+
Get a list of names of the feature values.
40+
41+
This will return as many names as the number feature values that will be returned.
42+
For instance, if a feature extractor returns the first 3 eigen values, this method
43+
should return 3 names, for instance 'eigen_value_1', 'eigen_value_2' and 'eigen_value_3'.
44+
45+
:return: List of feature names
46+
"""
47+
return ['density_absolute_mean_z']
48+
49+
def extract(self, point_cloud, neighborhood, target_point_cloud, target_index, volume_description):
50+
"""
51+
Extract the feature value(s) of the point cloud at location of the target.
52+
53+
:param point_cloud: environment (search space) point cloud
54+
:param neighborhood: array of indices of points within the point_cloud argument
55+
:param target_point_cloud: point cloud that contains target point
56+
:param target_index: index of the target point in the target point cloud
57+
:param volume_description: volume object that describes the shape and size of the search volume
58+
:return: feature value
59+
"""
60+
if 'raw_classification' not in point_cloud[point]:
61+
raise ValueError(
62+
'Missing raw_classification attribute which is necessary for calculating density_absolute_mean.')
63+
64+
non_ground_indices = [i for i in neighborhood if not _is_ground(i, point_cloud)]
65+
density_absolute_mean_z = self._get_density_absolute_mean(non_ground_indices, point_cloud)
66+
67+
return density_absolute_mean_z
68+
69+
@staticmethod
70+
def _get_ground_indices(point_cloud, ground_tags):
71+
index_grd = []
72+
for ipt, c in enumerate(point_cloud):
73+
if c in ground_tags:
74+
index_grd.append(ipt)
75+
return index_grd
76+
77+
def _get_density_absolute_mean(self, non_ground_indices, source_point_cloud):
78+
n_non_ground = len(non_ground_indices)
79+
z_non_ground = source_point_cloud[point][self.DATA_KEY]["data"][non_ground_indices]
80+
if n_non_ground == 0:
81+
density_absolute_mean = 0.
82+
else:
83+
density_absolute_mean = float(
84+
len(z_non_ground[z_non_ground > np.mean(z_non_ground)])) / n_non_ground * 100.
85+
return density_absolute_mean
86+
87+
def get_params(self):
88+
"""
89+
Return a tuple of parameters involved in the current feature extractor object.
90+
91+
Needed for provenance.
92+
"""
93+
return ()

laserchicken/feature_extractor/pulse_penetration_feature_extractor.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def provides(cls):
4343
4444
:return: List of feature names
4545
"""
46-
return ['pulse_penetration_ratio', 'density_absolute_mean_z', 'density_absolute_mean_norm_z']
46+
return ['pulse_penetration_ratio']
4747

4848
def extract(self, point_cloud, neighborhood, target_point_cloud, target_index, volume_description):
4949
"""
@@ -65,12 +65,7 @@ def extract(self, point_cloud, neighborhood, target_point_cloud, target_index, v
6565
pulse_penetration_ratio = self._get_pulse_penetration_ratio(
6666
ground_indices, len(neighborhood))
6767

68-
non_ground_indices = [i for i in neighborhood if not _is_ground(i, point_cloud)]
69-
density_absolute_mean_z = self._get_density_absolute_mean(non_ground_indices, point_cloud, 'z')
70-
density_absolute_mean_norm_z = self._get_density_absolute_mean(
71-
non_ground_indices, point_cloud, normalized_height)
72-
73-
return pulse_penetration_ratio, density_absolute_mean_z, density_absolute_mean_norm_z
68+
return pulse_penetration_ratio
7469

7570
@staticmethod
7671
def _get_ground_indices(point_cloud, ground_tags):
@@ -86,16 +81,6 @@ def _get_pulse_penetration_ratio(ground_indices, n_total_points):
8681
n_ground = len(ground_indices)
8782
return float(n_ground) / n_total
8883

89-
def _get_density_absolute_mean(self, non_ground_indices, source_point_cloud, height_key):
90-
n_non_ground = len(non_ground_indices)
91-
z_non_ground = source_point_cloud[point][height_key]["data"][non_ground_indices]
92-
if n_non_ground == 0:
93-
density_absolute_mean = 0.
94-
else:
95-
density_absolute_mean = float(
96-
len(z_non_ground[z_non_ground > np.mean(z_non_ground)])) / n_non_ground * 100.
97-
return density_absolute_mean
98-
9984
def get_params(self):
10085
"""
10186
Return a tuple of parameters involved in the current feature extractor object.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import unittest
2+
3+
import numpy as np
4+
5+
from laserchicken.feature_extractor.density_absolute_mean_norm_z_feature_extractor import \
6+
DensityAbsoluteMeanNormZFeatureExtractor
7+
from laserchicken.feature_extractor.density_absolute_mean_z_feature_extractor import \
8+
DensityAbsoluteMeanZFeatureExtractor
9+
from laserchicken.feature_extractor.pulse_penetration_feature_extractor import PulsePenetrationFeatureExtractor
10+
from laserchicken.keys import point
11+
from laserchicken.test_tools import create_point_cloud
12+
13+
14+
class TestDensityAbsoluteMeanNormZFeatureExtractorArtificialData(unittest.TestCase):
15+
def test_simle_case_correct(self):
16+
"""Check that one out of 4 points above mean of only vegetation points yields a value of 25"""
17+
ground = 2 # Ground tag
18+
veg = 4 # Medium vegetation tag
19+
x = y = z = np.array([10, 10, 10, 1, 1, 1, 2])
20+
point_cloud = create_point_cloud(x, y, np.zeros_like(z), normalized_z=z)
21+
point_cloud[point]['raw_classification'] = {'data': np.array([ground, ground, ground, veg, veg, veg, veg]),
22+
'type': 'double'}
23+
neighborhood = list(range(len(x)))
24+
25+
extractor = DensityAbsoluteMeanNormZFeatureExtractor()
26+
density_absolute_mean = extractor.extract(point_cloud, neighborhood, None, None, None)
27+
28+
self.assertAlmostEqual(density_absolute_mean, 25)
29+
30+
31+
if __name__ == '__main__':
32+
unittest.main()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import unittest
2+
3+
import numpy as np
4+
5+
from laserchicken.feature_extractor.density_absolute_mean_z_feature_extractor import \
6+
DensityAbsoluteMeanZFeatureExtractor
7+
from laserchicken.keys import point
8+
from laserchicken.test_tools import create_point_cloud
9+
10+
11+
class TestDensityAbsoluteMeanZFeatureExtractorArtificialData(unittest.TestCase):
12+
def test_simle_case_correct(self):
13+
"""Check that one out of 4 points above mean of only vegetation points yields a value of 25"""
14+
ground = 2 # Ground tag
15+
veg = 4 # Medium vegetation tag
16+
x = y = z = np.array([10, 10, 10, 1, 1, 1, 2])
17+
point_cloud = create_point_cloud(x, y, z)
18+
point_cloud[point]['raw_classification'] = {'data': np.array([ground, ground, ground, veg, veg, veg, veg]),
19+
'type': 'double'}
20+
neighborhood = list(range(len(x)))
21+
22+
extractor = DensityAbsoluteMeanZFeatureExtractor()
23+
density_absolute_mean = extractor.extract(point_cloud, neighborhood, None, None, None)
24+
25+
self.assertAlmostEqual(density_absolute_mean, 25)
26+
27+
28+
if __name__ == '__main__':
29+
unittest.main()

laserchicken/feature_extractor/test_pulse_penetration_extractor.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ class TestPulsePenetrationFeatureExtractorArtificialData(unittest.TestCase):
1818
def test_pulse(self):
1919
"""Pulse extractor on artificial data should yield expected feature values."""
2020
extractor = PulsePenetrationFeatureExtractor()
21-
pp_ratio, _ = extractor.extract(
22-
self.point_cloud, self.neighborhood, None, None, None)
21+
pp_ratio = extractor.extract(self.point_cloud, self.neighborhood, None, None, None)
2322
self.assertEqual(pp_ratio, self.expected_pp_ratio)
2423

2524
def _set_plane_data(self):
@@ -71,23 +70,6 @@ def setUp(self):
7170
self.expected_pp_ratio = float(self.points_per_plane) / n_points
7271

7372

74-
class TestDensityAbsoluteMeanFeatureExtractorArtificialData(unittest.TestCase):
75-
def test_simle_case_correct(self):
76-
"""Check that one out of 4 points above mean of only vegetation points yields a value of 25"""
77-
ground = 2 # Ground tag
78-
veg = 4 # Medium vegetation tag
79-
x = y = z = np.array([10, 10, 10, 1, 1, 1, 2])
80-
point_cloud = create_point_cloud(x, y, z)
81-
point_cloud[point]['raw_classification'] = {'data': np.array([ground, ground, ground, veg, veg, veg, veg]),
82-
'type': 'double'}
83-
neighborhood = list(range(len(x)))
84-
85-
extractor = PulsePenetrationFeatureExtractor()
86-
_, density_absolute_mean = extractor.extract(point_cloud, neighborhood, None, None, None)
87-
88-
self.assertAlmostEqual(density_absolute_mean, 25)
89-
90-
9173
class TestPulsePenetratioFeatureExtractorRealData(unittest.TestCase):
9274
"""Test the pulse extractor on real data and make sure it doesn't crash."""
9375
_test_file_name = 'AHN3.las'

0 commit comments

Comments
 (0)