Repository: eEcoLiDAR/laserchicken

Issue:
density absolute mean bug
uses ground points only where it should use everything but ground points

Discussion:

Patch:
diff --git a/laserchicken/feature_extractor/pulse_penetration_feature_extractor.py b/laserchicken/feature_extractor/pulse_penetration_feature_extractor.py
index f32c94a..65b7abb 100644
--- a/laserchicken/feature_extractor/pulse_penetration_feature_extractor.py
+++ b/laserchicken/feature_extractor/pulse_penetration_feature_extractor.py
@@ -13,6 +13,10 @@ from laserchicken.keys import point
 GROUND_TAGS = [2]
 
 
+def _is_ground(i, point_cloud):
+    return point_cloud[point]['raw_classification']["data"][i] in GROUND_TAGS
+
+
 class PulsePenetrationFeatureExtractor(AbstractFeatureExtractor):
     """Feature extractor for the point density."""
 
@@ -57,14 +61,13 @@ class PulsePenetrationFeatureExtractor(AbstractFeatureExtractor):
                 'Missing raw_classification attribute which is necessary for calculating pulse_penetratio and '
                 'density_absolute_mean features.')
 
-        class_neighbors = [point_cloud[point]['raw_classification']["data"][n] for n in neighborhood]
-
-        ground_indices = self._get_ground_indices(class_neighbors, GROUND_TAGS)
-
+        ground_indices = [i for i in neighborhood if _is_ground(i, point_cloud)]
         pulse_penetration_ratio = self._get_pulse_penetration_ratio(
-            ground_indices, class_neighbors)
+            ground_indices, len(neighborhood))
+
+        non_ground_indices = [i for i in neighborhood if not _is_ground(i, point_cloud)]
         density_absolute_mean = self._get_density_absolute_mean(
-            ground_indices, point_cloud)
+            non_ground_indices, point_cloud)
 
         return pulse_penetration_ratio, density_absolute_mean
 
@@ -77,20 +80,20 @@ class PulsePenetrationFeatureExtractor(AbstractFeatureExtractor):
         return index_grd
 
     @staticmethod
-    def _get_pulse_penetration_ratio(ground_indices, class_neighbors):
-        n_total = np.max((len(class_neighbors), 1))
+    def _get_pulse_penetration_ratio(ground_indices, n_total_points):
+        n_total = max(n_total_points, 1)
         n_ground = len(ground_indices)
         return float(n_ground) / n_total
 
     @staticmethod
-    def _get_density_absolute_mean(ground_indices, source_point_cloud):
-        n_ground = len(ground_indices)
-        z_ground = source_point_cloud[point]['z']["data"][ground_indices]
-        if n_ground == 0:
+    def _get_density_absolute_mean(non_ground_indices, source_point_cloud):
+        n_non_ground = len(non_ground_indices)
+        z_non_ground = source_point_cloud[point]['z']["data"][non_ground_indices]
+        if n_non_ground == 0:
             density_absolute_mean = 0.
         else:
             density_absolute_mean = float(
-                len(z_ground[z_ground > np.mean(z_ground)])) / n_ground * 100.
+                len(z_non_ground[z_non_ground > np.mean(z_non_ground)])) / n_non_ground * 100.
         return density_absolute_mean
 
     def get_params(self):


Test patch:
diff --git a/laserchicken/feature_extractor/test_pulse_penetration_extractor.py b/laserchicken/feature_extractor/test_pulse_penetration_extractor.py
index bf88823..ff37642 100644
--- a/laserchicken/feature_extractor/test_pulse_penetration_extractor.py
+++ b/laserchicken/feature_extractor/test_pulse_penetration_extractor.py
@@ -7,6 +7,8 @@ import numpy as np
 from laserchicken import keys, read_las, utils
 from laserchicken.compute_neighbors import compute_neighborhoods
 from laserchicken.feature_extractor.pulse_penetration_feature_extractor import PulsePenetrationFeatureExtractor
+from laserchicken.keys import point
+from laserchicken.test_tools import create_point_cloud
 from laserchicken.volume_specification import InfiniteCylinder
 
 
@@ -16,10 +18,9 @@ class TestPulsePenetrationFeatureExtractorArtificialData(unittest.TestCase):
     def test_pulse(self):
         """Pulse extractor on artificial data should yield expected feature values."""
         extractor = PulsePenetrationFeatureExtractor()
-        pp_ratio, density_absolute_mean = extractor.extract(
+        pp_ratio, _ = extractor.extract(
             self.point_cloud, self.neighborhood, None, None, None)
         self.assertEqual(pp_ratio, self.expected_pp_ratio)
-        self.assertEqual(density_absolute_mean, 50.)
 
     def _set_plane_data(self):
         """Create two planes of ground point at z = +- 0.1."""
@@ -70,6 +71,23 @@ class TestPulsePenetrationFeatureExtractorArtificialData(unittest.TestCase):
         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'