Skip to content

Commit

Permalink
Merge pull request #51 from eEcoLiDAR/new-reader
Browse files Browse the repository at this point in the history
Add command line tools
  • Loading branch information
cwmeijer committed Feb 1, 2018
2 parents 3b99dd9 + 5834727 commit 4ebb27e
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 1 deletion.
5 changes: 5 additions & 0 deletions laserchicken/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Command line tools."""


class ToolException(Exception):
"""Raise if a command line tool fails."""
112 changes: 112 additions & 0 deletions laserchicken/tools/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Command line tool definitions."""
from __future__ import print_function

import os

import click
from colorama import Back, init

from . import ToolException
from .._version import __version__
from ..select import select_above, select_below
from ..spatial_selections import points_in_polygon_shp_file, points_in_polygon_wkt, points_in_polygon_wkt_file
from .io import _check_save_path, _load, _save


@click.group(chain=True, invoke_without_command=True)
@click.version_option(version=__version__)
@click.argument('input_file', nargs=1, type=click.Path(exists=True, readable=True))
@click.argument('output_file', nargs=1, type=click.Path(writable=True))
def main(input_file, output_file):
"""Various commmands for selecting points from a point cloud.
Note that commands can be chained.
Example:
laserchicken testdata/AHN3.las test.ply filter_below intensity 100 filter_above intensity 60
"""


@main.resultcallback()
def process_pipeline(processors, input_file, output_file):
init(autoreset=True)
try:
_check_save_path(output_file)
point_cloud = _load(input_file)
for processor in processors:
point_cloud = processor(point_cloud)
_save(point_cloud, output_file)
except ToolException as exc:
print(Back.RED + "Error: " + str(exc))


@main.command('import')
def _import():
"""Read data from input file and save to output file.
Actually the word import can be omitted, as this is the default operation.
Examples:
laserchicken testdata/AHN2.las test.ply
laserchicken testdata/AHN2.las test.ply import
"""
return lambda point_cloud: point_cloud


@main.command('filter_below')
@click.argument('attribute')
@click.argument('threshold', type=click.FLOAT)
def _filter_below(attribute, threshold):
"""Select those points where the value of attribute is below threshold.
Example:
laserchicken testdata/AHN3.las test.ply filter_below intensity 100
"""
return lambda point_cloud: select_below(point_cloud, attribute, threshold)


@main.command('filter_above')
@click.argument('attribute')
@click.argument('threshold', type=click.FLOAT)
def _filter_above(attribute, threshold):
"""Select those points where the value of attribute is above threshold.
Example:
laserchicken testdata/AHN3.las test.ply filter_above intensity 100
"""
return lambda point_cloud: select_above(point_cloud, attribute, threshold)


@main.command('filter_in_polygon')
@click.argument('polygon')
def _filter_in_polygon(polygon):
"""Select those points that are inside polygon.
Polygon can be a shape file (with extension .shp or .wkt) or a WKT string.
Examples:
laserchicken testdata/AHN2.las test.ply filter_in_polygon testdata/ahn2_geometries_shp/ahn2_polygon.shp
laserchicken testdata/AHN2.las test.ply filter_in_polygon testdata/ahn2_geometries_wkt/ahn2_polygon.wkt
laserchicken testdata/AHN2.las test.ply filter_in_polygon 'POLYGON(( 243590.0 572110.0, 243640.0 572160.0, 243700.0 572110.0, 243640.0 572060.0, 243590.0 572110.0 ))'
"""
if os.path.isfile(polygon):
ext = os.path.splitext(polygon)[1].lower()
functions = {
'.shp': points_in_polygon_shp_file,
'.wkt': points_in_polygon_wkt_file,
}
if ext not in functions:
raise ToolException("Unable to determine type of shapefile, "
"choose from types {}".format(list(functions)))
return lambda point_cloud: functions[ext](point_cloud, polygon)
else:
print("polygon is not a file, assuming it is a WKT string")
return lambda point_cloud: points_in_polygon_wkt(point_cloud, polygon)
74 changes: 74 additions & 0 deletions laserchicken/tools/io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Functions for loading and saving a pointcloud from/to file."""
from __future__ import print_function

import os

from colorama import Fore

from . import ToolException
from ..read_las import read as read_las
from ..read_ply import read as read_ply
from ..write_ply import write as write_ply

READERS = {
'.ply': read_ply,
'.las': read_las,
}

WRITERS = {
'.ply': write_ply,
}


def _load(filename, reader=None):
"""Load point cloud from filename using reader."""
print("Reading the input file", end='')

if reader is None:
ext = os.path.splitext(filename)[1].lower()
if ext not in READERS:
raise ToolException("Unable to guess the file type from the extension {}, "
"please specify a reader argument".format(ext))
reader = READERS[ext]

point_cloud = reader(filename)
print(Fore.GREEN + " [DONE]")
return point_cloud


def _check_save_path(filename):
"""Check that filename can be used to save the point cloud."""
ext = os.path.splitext(filename)[1].lower()
if ext not in WRITERS:
ToolException("Unknown output file format {}, choose from {}".format(ext, list(WRITERS)))

if os.path.exists(filename):
raise ToolException("Output file already exists! --> {}".format(filename))

output_directory = os.path.dirname(filename)

if output_directory and not os.path.exists(output_directory):
raise ToolException("Output file path does not exist! --> {}".format(output_directory))


def _save(point_cloud, filename, writer=None):
"""Save point cloud to filename using writer."""
_check_save_path(filename)
print("File will be saved as {}".format(filename))

if writer is None:
ext = os.path.splitext(filename)[1].lower()
if ext not in WRITERS:
raise ToolException("Unable to guess the file type from the extension {}, "
"please specify a saver argument".format(ext))
writer = WRITERS[ext]

print("Saving, please wait...", end='')
try:
writer(point_cloud, filename)
except Exception as exc:
print(Fore.RED + " [ERROR]")
print("An exception of type {} occurred. Arguments:\n{!r}".format(type(exc).__name__, exc.args))
raise ToolException("Conversion has failed! \nCheck '{}' in laserchicken module.".format(writer))

print(Fore.GREEN + " [DONE]")
29 changes: 29 additions & 0 deletions laserchicken/tools/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest
from click.testing import CliRunner
from . import cli


@pytest.fixture
def runner():
return CliRunner()


# def test_cli(runner):
# result = runner.invoke(cli.main)
# assert result.exit_code == 0
# assert not result.exception
# assert result.output.strip() == 'Hello.'
#
#
# def test_cli_with_option(runner):
# result = runner.invoke(cli.main, ['--', './testdata/AHN2.las'])
# assert not result.exception
# assert result.exit_code == 0
# assert result.output.strip() == ''
#
#
# def test_cli_with_arg(runner):
# result = runner.invoke(cli.main, ['Chicken'])
# assert result.exit_code == 0
# assert not result.exception
# assert result.output.strip() == ''
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ python-dateutil
shapely
PyShp
pandas
click
colorama
8 changes: 7 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os

from setuptools import setup

with open(os.path.join(os.path.dirname(__file__), 'laserchicken/_version.py')) as versionpy:
Expand All @@ -19,11 +20,16 @@ def read(file_name):
license='Apache 2.0',
keywords=['Python', 'Point cloud'],
url='https://github.com/eEcoLiDAR/eEcoLiDAR',
packages=[],
packages=['laserchicken'],
install_requires=required,
long_description=read('README.md'),
classifiers=[
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
],
entry_points={
'console_scripts': [
'laserchicken = laserchicken.tools.cli:main',
],
},
)

0 comments on commit 4ebb27e

Please sign in to comment.