Skip to content

Commit

Permalink
split pulse penetration ratio and dens abs mean z and norm features
Browse files Browse the repository at this point in the history
  • Loading branch information
cwmeijer committed Feb 27, 2019
1 parent 7bf3c15 commit 44294c8
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 36 deletions.
2 changes: 2 additions & 0 deletions laserchicken/feature_extractor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__):
Expand Down
Original file line number Diff line number Diff line change
@@ -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']
Original file line number Diff line number Diff line change
@@ -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 ()
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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):
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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'
Expand Down

0 comments on commit 44294c8

Please sign in to comment.