From 44294c88e897a94ac943d811809cc66e044a19f5 Mon Sep 17 00:00:00 2001 From: cwmeijer Date: Wed, 27 Feb 2019 14:59:42 +0100 Subject: [PATCH] split pulse penetration ratio and dens abs mean z and norm features --- laserchicken/feature_extractor/__init__.py | 2 + ..._absolute_mean_norm_z_feature_extractor.py | 33 +++++++ ...nsity_absolute_mean_z_feature_extractor.py | 93 +++++++++++++++++++ .../pulse_penetration_feature_extractor.py | 19 +--- ..._density_absolute_mean_norm_z_extractor.py | 32 +++++++ .../test_density_absolute_mean_z_extractor.py | 29 ++++++ .../test_pulse_penetration_extractor.py | 20 +--- 7 files changed, 192 insertions(+), 36 deletions(-) create mode 100644 laserchicken/feature_extractor/density_absolute_mean_norm_z_feature_extractor.py create mode 100644 laserchicken/feature_extractor/density_absolute_mean_z_feature_extractor.py create mode 100644 laserchicken/feature_extractor/test_density_absolute_mean_norm_z_extractor.py create mode 100644 laserchicken/feature_extractor/test_density_absolute_mean_z_extractor.py diff --git a/laserchicken/feature_extractor/__init__.py b/laserchicken/feature_extractor/__init__.py index d1325b3..292ef00 100644 --- a/laserchicken/feature_extractor/__init__.py +++ b/laserchicken/feature_extractor/__init__.py @@ -29,6 +29,8 @@ from .entropy_norm_z_feature_extractor import EntropyNormZFeatureExtractor from .median_norm_z_feature_extractor import MedianNormZFeatureExtractor from .percentile_norm_z_feature_extractor import PercentileNormZFeatureExtractor +from .density_absolute_mean_z_feature_extractor import DensityAbsoluteMeanZFeatureExtractor +from .density_absolute_mean_norm_z_feature_extractor import DensityAbsoluteMeanNormZFeatureExtractor def _create_feature_map(module_name=__name__): diff --git a/laserchicken/feature_extractor/density_absolute_mean_norm_z_feature_extractor.py b/laserchicken/feature_extractor/density_absolute_mean_norm_z_feature_extractor.py new file mode 100644 index 0000000..b2e90e4 --- /dev/null +++ b/laserchicken/feature_extractor/density_absolute_mean_norm_z_feature_extractor.py @@ -0,0 +1,33 @@ +"""Pulse penetration ratio and density absolute mean calculations. + +See https://github.com/eEcoLiDAR/eEcoLiDAR/issues/23. +""" + +import numpy as np + +from laserchicken.feature_extractor.abc import AbstractFeatureExtractor +from laserchicken.feature_extractor.density_absolute_mean_z_feature_extractor import \ + DensityAbsoluteMeanZFeatureExtractor +from laserchicken.keys import point, normalized_height + +# classification according to +# http://www.asprs.org/wp-content/uploads/2010/12/LAS_1-4_R6.pdf +GROUND_TAGS = [2] + + +class DensityAbsoluteMeanNormZFeatureExtractor(DensityAbsoluteMeanZFeatureExtractor): + """Feature extractor for the point density.""" + DATA_KEY = normalized_height + + @classmethod + def provides(cls): + """ + Get a list of names of the feature values. + + This will return as many names as the number feature values that will be returned. + For instance, if a feature extractor returns the first 3 eigen values, this method + should return 3 names, for instance 'eigen_value_1', 'eigen_value_2' and 'eigen_value_3'. + + :return: List of feature names + """ + return ['density_absolute_mean_norm_z'] diff --git a/laserchicken/feature_extractor/density_absolute_mean_z_feature_extractor.py b/laserchicken/feature_extractor/density_absolute_mean_z_feature_extractor.py new file mode 100644 index 0000000..64f1878 --- /dev/null +++ b/laserchicken/feature_extractor/density_absolute_mean_z_feature_extractor.py @@ -0,0 +1,93 @@ +"""Pulse penetration ratio and density absolute mean calculations. + +See https://github.com/eEcoLiDAR/eEcoLiDAR/issues/23. +""" + +import numpy as np + +from laserchicken.feature_extractor.abc import AbstractFeatureExtractor +from laserchicken.keys import point, normalized_height + +# classification according to +# http://www.asprs.org/wp-content/uploads/2010/12/LAS_1-4_R6.pdf +GROUND_TAGS = [2] + + +def _is_ground(i, point_cloud): + return point_cloud[point]['raw_classification']["data"][i] in GROUND_TAGS + + +class DensityAbsoluteMeanZFeatureExtractor(AbstractFeatureExtractor): + """Feature extractor for the point density.""" + DATA_KEY = 'z' + + @classmethod + def requires(cls): + """ + Get a list of names of the point attributes that are needed for this feature extraction. + + For simple features, this could be just x, y, and z. Other features can build on again + other features to have been computed first. + + :return: List of feature names + """ + return [] + + @classmethod + def provides(cls): + """ + Get a list of names of the feature values. + + This will return as many names as the number feature values that will be returned. + For instance, if a feature extractor returns the first 3 eigen values, this method + should return 3 names, for instance 'eigen_value_1', 'eigen_value_2' and 'eigen_value_3'. + + :return: List of feature names + """ + return ['density_absolute_mean_z'] + + def extract(self, point_cloud, neighborhood, target_point_cloud, target_index, volume_description): + """ + Extract the feature value(s) of the point cloud at location of the target. + + :param point_cloud: environment (search space) point cloud + :param neighborhood: array of indices of points within the point_cloud argument + :param target_point_cloud: point cloud that contains target point + :param target_index: index of the target point in the target point cloud + :param volume_description: volume object that describes the shape and size of the search volume + :return: feature value + """ + if 'raw_classification' not in point_cloud[point]: + raise ValueError( + 'Missing raw_classification attribute which is necessary for calculating density_absolute_mean.') + + non_ground_indices = [i for i in neighborhood if not _is_ground(i, point_cloud)] + density_absolute_mean_z = self._get_density_absolute_mean(non_ground_indices, point_cloud) + + return density_absolute_mean_z + + @staticmethod + def _get_ground_indices(point_cloud, ground_tags): + index_grd = [] + for ipt, c in enumerate(point_cloud): + if c in ground_tags: + index_grd.append(ipt) + return index_grd + + def _get_density_absolute_mean(self, non_ground_indices, source_point_cloud): + n_non_ground = len(non_ground_indices) + z_non_ground = source_point_cloud[point][self.DATA_KEY]["data"][non_ground_indices] + if n_non_ground == 0: + density_absolute_mean = 0. + else: + density_absolute_mean = float( + len(z_non_ground[z_non_ground > np.mean(z_non_ground)])) / n_non_ground * 100. + return density_absolute_mean + + def get_params(self): + """ + Return a tuple of parameters involved in the current feature extractor object. + + Needed for provenance. + """ + return () diff --git a/laserchicken/feature_extractor/pulse_penetration_feature_extractor.py b/laserchicken/feature_extractor/pulse_penetration_feature_extractor.py index 5ca0607..2e1ad8d 100644 --- a/laserchicken/feature_extractor/pulse_penetration_feature_extractor.py +++ b/laserchicken/feature_extractor/pulse_penetration_feature_extractor.py @@ -43,7 +43,7 @@ def provides(cls): :return: List of feature names """ - return ['pulse_penetration_ratio', 'density_absolute_mean_z', 'density_absolute_mean_norm_z'] + return ['pulse_penetration_ratio'] def extract(self, point_cloud, neighborhood, target_point_cloud, target_index, volume_description): """ @@ -65,12 +65,7 @@ def extract(self, point_cloud, neighborhood, target_point_cloud, target_index, v pulse_penetration_ratio = self._get_pulse_penetration_ratio( ground_indices, len(neighborhood)) - non_ground_indices = [i for i in neighborhood if not _is_ground(i, point_cloud)] - density_absolute_mean_z = self._get_density_absolute_mean(non_ground_indices, point_cloud, 'z') - density_absolute_mean_norm_z = self._get_density_absolute_mean( - non_ground_indices, point_cloud, normalized_height) - - return pulse_penetration_ratio, density_absolute_mean_z, density_absolute_mean_norm_z + return pulse_penetration_ratio @staticmethod def _get_ground_indices(point_cloud, ground_tags): @@ -86,16 +81,6 @@ def _get_pulse_penetration_ratio(ground_indices, n_total_points): n_ground = len(ground_indices) return float(n_ground) / n_total - def _get_density_absolute_mean(self, non_ground_indices, source_point_cloud, height_key): - n_non_ground = len(non_ground_indices) - z_non_ground = source_point_cloud[point][height_key]["data"][non_ground_indices] - if n_non_ground == 0: - density_absolute_mean = 0. - else: - density_absolute_mean = float( - len(z_non_ground[z_non_ground > np.mean(z_non_ground)])) / n_non_ground * 100. - return density_absolute_mean - def get_params(self): """ Return a tuple of parameters involved in the current feature extractor object. diff --git a/laserchicken/feature_extractor/test_density_absolute_mean_norm_z_extractor.py b/laserchicken/feature_extractor/test_density_absolute_mean_norm_z_extractor.py new file mode 100644 index 0000000..3be8fdb --- /dev/null +++ b/laserchicken/feature_extractor/test_density_absolute_mean_norm_z_extractor.py @@ -0,0 +1,32 @@ +import unittest + +import numpy as np + +from laserchicken.feature_extractor.density_absolute_mean_norm_z_feature_extractor import \ + DensityAbsoluteMeanNormZFeatureExtractor +from laserchicken.feature_extractor.density_absolute_mean_z_feature_extractor import \ + DensityAbsoluteMeanZFeatureExtractor +from laserchicken.feature_extractor.pulse_penetration_feature_extractor import PulsePenetrationFeatureExtractor +from laserchicken.keys import point +from laserchicken.test_tools import create_point_cloud + + +class TestDensityAbsoluteMeanNormZFeatureExtractorArtificialData(unittest.TestCase): + def test_simle_case_correct(self): + """Check that one out of 4 points above mean of only vegetation points yields a value of 25""" + ground = 2 # Ground tag + veg = 4 # Medium vegetation tag + x = y = z = np.array([10, 10, 10, 1, 1, 1, 2]) + point_cloud = create_point_cloud(x, y, np.zeros_like(z), normalized_z=z) + point_cloud[point]['raw_classification'] = {'data': np.array([ground, ground, ground, veg, veg, veg, veg]), + 'type': 'double'} + neighborhood = list(range(len(x))) + + extractor = DensityAbsoluteMeanNormZFeatureExtractor() + density_absolute_mean = extractor.extract(point_cloud, neighborhood, None, None, None) + + self.assertAlmostEqual(density_absolute_mean, 25) + + +if __name__ == '__main__': + unittest.main() diff --git a/laserchicken/feature_extractor/test_density_absolute_mean_z_extractor.py b/laserchicken/feature_extractor/test_density_absolute_mean_z_extractor.py new file mode 100644 index 0000000..8a3d3c3 --- /dev/null +++ b/laserchicken/feature_extractor/test_density_absolute_mean_z_extractor.py @@ -0,0 +1,29 @@ +import unittest + +import numpy as np + +from laserchicken.feature_extractor.density_absolute_mean_z_feature_extractor import \ + DensityAbsoluteMeanZFeatureExtractor +from laserchicken.keys import point +from laserchicken.test_tools import create_point_cloud + + +class TestDensityAbsoluteMeanZFeatureExtractorArtificialData(unittest.TestCase): + def test_simle_case_correct(self): + """Check that one out of 4 points above mean of only vegetation points yields a value of 25""" + ground = 2 # Ground tag + veg = 4 # Medium vegetation tag + x = y = z = np.array([10, 10, 10, 1, 1, 1, 2]) + point_cloud = create_point_cloud(x, y, z) + point_cloud[point]['raw_classification'] = {'data': np.array([ground, ground, ground, veg, veg, veg, veg]), + 'type': 'double'} + neighborhood = list(range(len(x))) + + extractor = DensityAbsoluteMeanZFeatureExtractor() + density_absolute_mean = extractor.extract(point_cloud, neighborhood, None, None, None) + + self.assertAlmostEqual(density_absolute_mean, 25) + + +if __name__ == '__main__': + unittest.main() diff --git a/laserchicken/feature_extractor/test_pulse_penetration_extractor.py b/laserchicken/feature_extractor/test_pulse_penetration_extractor.py index ff37642..4245536 100644 --- a/laserchicken/feature_extractor/test_pulse_penetration_extractor.py +++ b/laserchicken/feature_extractor/test_pulse_penetration_extractor.py @@ -18,8 +18,7 @@ class TestPulsePenetrationFeatureExtractorArtificialData(unittest.TestCase): def test_pulse(self): """Pulse extractor on artificial data should yield expected feature values.""" extractor = PulsePenetrationFeatureExtractor() - pp_ratio, _ = extractor.extract( - self.point_cloud, self.neighborhood, None, None, None) + pp_ratio = extractor.extract(self.point_cloud, self.neighborhood, None, None, None) self.assertEqual(pp_ratio, self.expected_pp_ratio) def _set_plane_data(self): @@ -71,23 +70,6 @@ def setUp(self): self.expected_pp_ratio = float(self.points_per_plane) / n_points -class TestDensityAbsoluteMeanFeatureExtractorArtificialData(unittest.TestCase): - def test_simle_case_correct(self): - """Check that one out of 4 points above mean of only vegetation points yields a value of 25""" - ground = 2 # Ground tag - veg = 4 # Medium vegetation tag - x = y = z = np.array([10, 10, 10, 1, 1, 1, 2]) - point_cloud = create_point_cloud(x, y, z) - point_cloud[point]['raw_classification'] = {'data': np.array([ground, ground, ground, veg, veg, veg, veg]), - 'type': 'double'} - neighborhood = list(range(len(x))) - - extractor = PulsePenetrationFeatureExtractor() - _, density_absolute_mean = extractor.extract(point_cloud, neighborhood, None, None, None) - - self.assertAlmostEqual(density_absolute_mean, 25) - - class TestPulsePenetratioFeatureExtractorRealData(unittest.TestCase): """Test the pulse extractor on real data and make sure it doesn't crash.""" _test_file_name = 'AHN3.las'