├── .gitignore ├── README.md ├── l2d.sh ├── l2d ├── __init__.py ├── geo.py ├── parsers.py ├── pdal.py ├── scripts │ ├── __init__.py │ ├── chm.py │ ├── classify.py │ ├── dems.py │ ├── process_voxels.py │ └── voxelize.py ├── tiles.py ├── utils.py ├── version.py └── voxel_utils.py ├── setup.py └── test ├── las ├── 25399731-5024-442b-b348-a8a987504c47.las ├── 600eaa2c-af07-4b60-90d8-e1e457156605.las ├── 719a3237-4236-4323-90b0-fb3c2f802b8a.las └── a8d2688b-5cad-4f25-93b0-d349b5234dcd.las ├── pdal_test.py ├── utils_test.py └── vectors ├── features.dbf ├── features.prj ├── features.qpj ├── features.shp ├── features.shx ├── tiles.dbf ├── tiles.prj ├── tiles.shp └── tiles.shx /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #lidar2dems 2 | 3 | The lidar2dems project is a collection open-source (FreeBSD license) command line utilities for supporting the easy creation of Digital Elevation Models (DEMs) from LiDAR data. lidar2dems uses the PDAL library (and associated dependencies) for doing the actual point processing and gridding of point clouds into raster data. 4 | 5 | See the [lidar2dems documentation](http://applied-geosolutions.github.io/lidar2dems/) for complete description, usage, and tutorial 6 | 7 | ####Funding 8 | 9 | lidar2dems was created by Applied GeoSolutions, LLC and the University of New Hampshire as part of a NASA-funded Carbon Monitoring System Project (NASA grant #NNX13AP88G; PI Stephen Hagen). In this project, Applied GeoSolutions and project partners are working with the government of Indonesia ([LAPAN](http://www.lapan.go.id)) to improve forest monitoring in Kalimantan by composing detailed, high resolution maps of forest carbon. If you have questions about this project please email [oss@appliedgeosolutions.com](mailto:oss@appliedgeosolutions.com) 10 | 11 | ####Authors and Contributors 12 | 13 | * [Matthew Hanson](http://github.com/matthewhanson), matt.a.hanson@gmail.com 14 | * Frankie Sullivan, franklin.sullivan@unh.edu 15 | * Steve Hagen, shagen@appliedgeosolutions.com 16 | * Ian Cooke, icooke@appliedgeosolutions.com 17 | 18 | ####License (FreeBSD) 19 | 20 | Copyright (c) 2015, Applied Geosolutions LLC, oss@appliedgeosolutions.com 21 | All rights reserved. 22 | 23 | Redistribution and use in source and binary forms, with or without 24 | modification, are permitted provided that the following conditions are met: 25 | 26 | * Redistributions of source code must retain the above copyright notice, this 27 | list of conditions and the following disclaimer. 28 | 29 | * Redistributions in binary form must reproduce the above copyright notice, 30 | this list of conditions and the following disclaimer in the documentation 31 | and/or other materials provided with the distribution. 32 | 33 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 34 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 35 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 36 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 37 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 38 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 39 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 40 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 41 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 42 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 43 | 44 | -------------------------------------------------------------------------------- /l2d.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | # script to classify LAS files and generate complete set of density, DSM, DTM, and CHM products 32 | 33 | d=$1 34 | 35 | site=features.shp 36 | bsite=${filename%.*} 37 | lasdir=lasclass 38 | demdir=dems 39 | 40 | echo Creating DEMs for directory $d 41 | 42 | echo Creating density image 43 | l2d_dems density $d/LAS -s $d/$site --outdir $d/$demdir 44 | 45 | echo Classifying with decimation 46 | l2d_classify $d/LAS -s $d/$site --outdir $d/$lasdir --deci 10 47 | 48 | echo Creating DSM 49 | l2d_dems dsm $d/$lasdir -s $d/$site --outdir $d/$demdir --gapfill --maxsd 2.5 --maxangle 19 --maxz 400 50 | 51 | echo Creating DTM 52 | l2d_dems dtm $d/$lasdir -s $d/$site --outdir $d/$demdir --gapfill --radius 0.56 1.41 2.50 3.00 53 | 54 | echo Creating CHM 55 | l2d $d/dsm.max.vrt $d/dtm.idw.vrt --fout $d/chm.tif 56 | 57 | echo Generating hillshades 58 | gdaldem hillshade $d/dsm.max.vrt $d/dsm-hillshade.max.tif 59 | gdaldem hillshade $d/dtm.idw.vrt $d/dtm-hillshade.idw.tif 60 | -------------------------------------------------------------------------------- /l2d/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # lidar2dems - utilties for creating DEMs from LiDAR data 3 | # 4 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 5 | # 6 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright notice, this 12 | # list of conditions and the following disclaimer. 13 | # 14 | # * Redistributions in binary form must reproduce the above copyright notice, 15 | # this list of conditions and the following disclaimer in the documentation 16 | # and/or other materials provided with the distribution. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | ################################################################################ 29 | from version import __version__ 30 | -------------------------------------------------------------------------------- /l2d/geo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | # Library functions and classes for working with geospatial data 32 | 33 | # The GeoVector class from GIPPY is mostly used, however for creating a shapefile 34 | # the OGR python bindings are used instead 35 | 36 | import os 37 | import tempfile 38 | import commands 39 | import shutil 40 | import subprocess 41 | import json 42 | from math import floor, ceil 43 | import gippy 44 | from shapely.geometry import box 45 | from shapely.wkt import loads 46 | 47 | 48 | def translate(filename, srs): 49 | """ Transform vector file to another SRS""" 50 | # TODO - move functionality into GIPPY 51 | bname = os.path.splitext(os.path.basename(filename))[0] 52 | td = tempfile.mkdtemp() 53 | fout = os.path.join(td, bname + '_warped.shp') 54 | prjfile = os.path.join(td, bname + '.prj') 55 | f = open(prjfile, 'w') 56 | f.write(srs) 57 | f.close() 58 | cmd = 'ogr2ogr %s %s -t_srs %s' % (fout, filename, prjfile) 59 | result = commands.getstatusoutput(cmd) 60 | return fout 61 | 62 | 63 | def crop2vector(img, vector): 64 | """ Crop a GeoImage down to a vector """ 65 | # transform vector to srs of image 66 | vecname = translate(vector.Filename(), img.Projection()) 67 | warped_vec = gippy.GeoVector(vecname) 68 | # rasterize the vector 69 | td = tempfile.mkdtemp() 70 | mask = gippy.GeoImage(os.path.join(td, vector.LayerName()), img, gippy.GDT_Byte, 1) 71 | maskname = mask.Filename() 72 | mask = None 73 | cmd = 'gdal_rasterize -at -burn 1 -l %s %s %s' % (warped_vec.LayerName(), vecname, maskname) 74 | result = commands.getstatusoutput(cmd) 75 | mask = gippy.GeoImage(maskname) 76 | img.AddMask(mask[0]).Process().ClearMasks() 77 | mask = None 78 | shutil.rmtree(os.path.dirname(maskname)) 79 | shutil.rmtree(os.path.dirname(vecname)) 80 | return img 81 | 82 | 83 | def check_overlap(filenames, vector): 84 | """ Return filtered list of filenames that intersect with vector """ 85 | sitegeom = loads(vector.WKT()) 86 | goodf = [] 87 | for f in filenames: 88 | try: 89 | bbox = get_bounds(f) 90 | if sitegeom.intersection(bbox).area > 0: 91 | goodf.append(f) 92 | except: 93 | pass 94 | return goodf 95 | 96 | 97 | def get_meta_data(filename): 98 | """ Get metadata from lasfile as dictionary """ 99 | cmd = ['pdal', 'info', '--metadata', '--input', os.path.abspath(filename)] 100 | out = subprocess.check_output(cmd) 101 | meta = json.loads(out)['metadata'] 102 | return meta 103 | 104 | 105 | def get_bounds(filename): 106 | """ Return shapely geometry of bounding box from lasfile """ 107 | bounds = get_bounding_box(filename) 108 | return box(bounds[0][0], bounds[0][1], bounds[2][0], bounds[2][1]) 109 | 110 | 111 | def get_bounding_box(filename, min_points=2): 112 | """ Get bounding box from LAS file """ 113 | meta = get_meta_data(filename) 114 | mx, my, Mx, My = meta['minx'], meta['miny'], meta['maxx'], meta['maxy'] 115 | if meta['count'] < min_points: 116 | raise Exception('{} contains only {} points (min_points={}).' 117 | .format(filename, meta['count'], min_points)) 118 | bounds = [(mx, my), (Mx, my), (Mx, My), (mx, My), (mx, my)] 119 | return bounds 120 | 121 | 122 | def get_vector_bounds(vector): 123 | """ Get vector bounds from GeoVector, on closest integer grid """ 124 | extent = vector.Extent() 125 | bounds = [floor(extent.x0()), floor(extent.y0()), ceil(extent.x1()), ceil(extent.y1())] 126 | return bounds 127 | -------------------------------------------------------------------------------- /l2d/parsers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | import argparse 32 | import sys 33 | 34 | 35 | class l2dParser(argparse.ArgumentParser): 36 | """ Extends argparser parser """ 37 | 38 | demtypes = { 39 | 'density': 'Total point density with optional filters', 40 | 'dsm': 'Digital Surface Model (non-ground points)', 41 | 'dtm': 'Digital Terrain Model (ground points)' 42 | } 43 | 44 | def __init__(self, commands=False, **kwargs): 45 | super(l2dParser, self).__init__(**kwargs) 46 | self.commands = commands 47 | self.formatter_class = argparse.ArgumentDefaultsHelpFormatter 48 | self.parent_parsers = [] 49 | 50 | def error(self, message): 51 | """ print help on error """ 52 | sys.stderr.write('error: %s\n' % message) 53 | self.print_help() 54 | sys.exit(2) 55 | 56 | def get_parser(self): 57 | """ Get new parser if using commands otherwise return self """ 58 | if self.commands: 59 | return l2dParser(add_help=False) 60 | else: 61 | return self 62 | 63 | def parse_args(self, **kwargs): 64 | if self.commands: 65 | subparser = self.add_subparsers(dest='demtype') 66 | for src, desc in self.demtypes.items(): 67 | subparser.add_parser(src, help=desc, parents=self.parent_parsers) 68 | args = super(l2dParser, self).parse_args(**kwargs) 69 | return args 70 | 71 | def add_input_parser(self): 72 | """ Add input arguments to parser """ 73 | parser = self.get_parser() 74 | group = parser.add_argument_group('input options') 75 | group.add_argument('lasdir', help='Directory of LAS file(s) to process') 76 | group.add_argument( 77 | '--vendor_classified', 78 | help='Files are not classified by l2d, the l2d naming scheme was not used for classified files', default=False) 79 | group.add_argument('--slope', help='Slope (override)', default=None) 80 | group.add_argument('--cellsize', help='Cell Size (override)', default=None) 81 | group.add_argument('-r', '--radius', help='Create DEM or each provided radius', nargs='*', default=['0.56']) 82 | group.add_argument('-s', '--site', help='Shapefile of site(s) in same projection as LiDAR', default=None) 83 | group.add_argument('-v', '--verbose', help='Print additional info', default=False, action='store_true') 84 | self.parent_parsers.append(parser) 85 | 86 | def add_output_parser(self): 87 | parser = self.get_parser() 88 | group = parser.add_argument_group('output options') 89 | group.add_argument('--outdir', help='Output directory', default='./') 90 | group.add_argument('--suffix', help='Suffix to append to output', default='') 91 | group.add_argument( 92 | '-g', '--gapfill', default=False, action='store_true', 93 | help='Gapfill using multiple radii products and interpolation (no effect on density products)') 94 | group.add_argument( 95 | '-o', '--overwrite', default=False, action='store_true', 96 | help='Overwrite any existing output files') 97 | self.parent_parsers.append(parser) 98 | 99 | def add_filter_parser(self): 100 | """ Add a few different filter options to the parser """ 101 | parser = self.get_parser() 102 | group = parser.add_argument_group('filtering options') 103 | group.add_argument('--maxsd', help='Filter outliers with this SD threshold', default=None) 104 | group.add_argument('--maxangle', help='Filter by maximum absolute scan angle', default=None) 105 | group.add_argument('--maxz', help='Filter by maximum elevation value', default=None) 106 | # group.add_argument('--scanedge', help='Filter by scanedge value (0 or 1)', default=None) 107 | group.add_argument('--returnnum', help='Filter by return number', default=None) 108 | h = 'Decimate the points (steps between points, 1 is no pruning' 109 | group.add_argument('--decimation', help=h, default=None) 110 | self.parent_parsers.append(parser) 111 | -------------------------------------------------------------------------------- /l2d/pdal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | # Library functions for creating DEMs from Lidar data 32 | 33 | import os 34 | from lxml import etree 35 | import tempfile 36 | 37 | from shapely.wkt import loads 38 | import glob 39 | from datetime import datetime 40 | import uuid 41 | from .utils import class_params, class_suffix, dem_products, gap_fill 42 | 43 | 44 | """ XML Functions """ 45 | 46 | 47 | def _xml_base(): 48 | """ Create initial XML for PDAL pipeline """ 49 | xml = etree.Element("Pipeline", version="1.0") 50 | return xml 51 | 52 | 53 | def _xml_p2g_base(fout, output, radius, site=None): 54 | """ Create initial XML for PDAL pipeline containing a Writer element """ 55 | xml = _xml_base() 56 | etree.SubElement(xml, "Writer", type="writers.p2g") 57 | etree.SubElement(xml[0], "Option", name="grid_dist_x").text = "1.0" 58 | etree.SubElement(xml[0], "Option", name="grid_dist_y").text = "1.0" 59 | etree.SubElement(xml[0], "Option", name="radius").text = str(radius) 60 | etree.SubElement(xml[0], "Option", name="output_format").text = "tif" 61 | # add EPSG option? - 'EPSG:%s' % epsg 62 | if site is not None: 63 | etree.SubElement(xml[0], "Option", name="spatialreference").text = site.Projection() 64 | # this not yet working in p2g 65 | # bounds = get_vector_bounds(site) 66 | # bounds = '([%s, %s], [%s, %s])' % (bounds[0], bounds[2], bounds[1], bounds[3]) 67 | # etree.SubElement(xml[0], "Option", name="bounds").text = bounds 68 | etree.SubElement(xml[0], "Option", name="filename").text = fout 69 | for t in output: 70 | etree.SubElement(xml[0], "Option", name="output_type").text = t 71 | return xml 72 | 73 | 74 | def _xml_las_base(fout): 75 | """ Create initial XML for writing to a LAS file """ 76 | xml = _xml_base() 77 | etree.SubElement(xml, "Writer", type="writers.las") 78 | etree.SubElement(xml[0], "Option", name="filename").text = fout 79 | return xml 80 | 81 | 82 | def _xml_add_pclblock(xml, pclblock): 83 | """ Add pclblock Filter element by taking in filename of a JSON file """ 84 | _xml = etree.SubElement(xml, "Filter", type="filters.pclblock") 85 | etree.SubElement(_xml, "Option", name="filename").text = pclblock 86 | return _xml 87 | 88 | 89 | def _xml_add_pmf(xml, slope, cellsize): 90 | """ Add progressive morphological filter """ 91 | # create JSON file for performing outlier removal 92 | j1 = '{"pipeline": {"name": "PMF","version": 1.0,"filters":' 93 | json = j1 + '[{"name": "ProgressiveMorphologicalFilter","setSlope": %s,"setellSize": %s}]}}' % (slope, cellsize) 94 | f, fname = tempfile.mkstemp(suffix='.json') 95 | os.write(f, json) 96 | os.close(f) 97 | return _xml_add_pclblock(xml, fname) 98 | 99 | 100 | def _xml_add_decimation_filter(xml, step): 101 | """ Add decimation Filter element and return """ 102 | fxml = etree.SubElement(xml, "Filter", type="filters.decimation") 103 | etree.SubElement(fxml, "Option", name="step").text = str(step) 104 | return fxml 105 | 106 | 107 | def _xml_add_classification_filter(xml, classification, equality="equals"): 108 | """ Add classification Filter element and return """ 109 | fxml = etree.SubElement(xml, "Filter", type="filters.range") 110 | _xml = etree.SubElement(fxml, "Option", name="dimension") 111 | _xml.text = "Classification" 112 | _xml = etree.SubElement(_xml, "Options") 113 | etree.SubElement(_xml, "Option", name=equality).text = str(classification) 114 | return fxml 115 | 116 | 117 | def _xml_add_maxsd_filter(xml, meank=20, thresh=3.0): 118 | """ Add outlier Filter element and return """ 119 | # create JSON file for performing outlier removal 120 | j1 = '{"pipeline": {"name": "Outlier Removal","version": 1.0,"filters":' 121 | json = j1 + '[{"name": "StatisticalOutlierRemoval","setMeanK": %s,"setStddevMulThresh": %s}]}}' % (meank, thresh) 122 | f, fname = tempfile.mkstemp(suffix='.json') 123 | os.write(f, json) 124 | os.close(f) 125 | return _xml_add_pclblock(xml, fname) 126 | 127 | 128 | def _xml_add_maxz_filter(xml, maxz): 129 | """ Add max elevation Filter element and return """ 130 | fxml = etree.SubElement(xml, "Filter", type="filters.range") 131 | _xml = etree.SubElement(fxml, "Option", name="dimension") 132 | _xml.text = "Z" 133 | _xml = etree.SubElement(_xml, "Options") 134 | etree.SubElement(_xml, "Option", name="max").text = maxz 135 | return fxml 136 | 137 | 138 | def _xml_add_maxangle_filter(xml, maxabsangle): 139 | """ Add scan angle Filter element and return """ 140 | fxml = etree.SubElement(xml, "Filter", type="filters.range") 141 | _xml = etree.SubElement(fxml, "Option", name="dimension") 142 | _xml.text = "ScanAngleRank" 143 | _xml = etree.SubElement(_xml, "Options") 144 | etree.SubElement(_xml, "Option", name="max").text = maxabsangle 145 | etree.SubElement(_xml, "Option", name="min").text = str(-float(maxabsangle)) 146 | return fxml 147 | 148 | 149 | def _xml_add_scanedge_filter(xml, value): 150 | """ Add EdgeOfFlightLine Filter element and return """ 151 | fxml = etree.SubElement(xml, "Filter", type="filters.range") 152 | _xml = etree.SubElement(fxml, "Option", name="dimension") 153 | _xml.text = "EdgeOfFlightLine" 154 | _xml = etree.SubElement(_xml, "Options") 155 | etree.SubElement(_xml, "Option", name="equals").text = value 156 | return fxml 157 | 158 | 159 | def _xml_add_returnnum_filter(xml, value): 160 | """ Add ReturnNum Filter element and return """ 161 | fxml = etree.SubElement(xml, "Filter", type="filters.range") 162 | _xml = etree.SubElement(fxml, "Option", name="dimension") 163 | _xml.text = "ReturnNum" 164 | _xml = etree.SubElement(_xml, "Options") 165 | etree.SubElement(_xml, "Option", name="equals").text = value 166 | return fxml 167 | 168 | 169 | def _xml_add_filters(xml, maxsd=None, maxz=None, maxangle=None, returnnum=None): 170 | if maxsd is not None: 171 | xml = _xml_add_maxsd_filter(xml, thresh=maxsd) 172 | if maxz is not None: 173 | xml = _xml_add_maxz_filter(xml, maxz) 174 | if maxangle is not None: 175 | xml = _xml_add_maxangle_filter(xml, maxangle) 176 | if returnnum is not None: 177 | xml = _xml_add_returnnum_filter(xml, returnnum) 178 | return xml 179 | 180 | 181 | def _xml_add_crop_filter(xml, wkt): 182 | """ Add cropping polygon as Filter Element and return """ 183 | fxml = etree.SubElement(xml, "Filter", type="filters.crop") 184 | etree.SubElement(fxml, "Option", name="polygon").text = wkt 185 | return fxml 186 | 187 | 188 | def _xml_add_reader(xml, filename): 189 | """ Add LAS Reader Element and return """ 190 | _xml = etree.SubElement(xml, "Reader", type="readers.las") 191 | etree.SubElement(_xml, "Option", name="filename").text = os.path.abspath(filename) 192 | return _xml 193 | 194 | 195 | def _xml_add_readers(xml, filenames): 196 | """ Add merge Filter element and readers to a Writer element and return Filter element """ 197 | if len(filenames) > 1: 198 | fxml = etree.SubElement(xml, "Filter", type="filters.merge") 199 | else: 200 | fxml = xml 201 | for f in filenames: 202 | _xml_add_reader(fxml, f) 203 | return fxml 204 | 205 | 206 | def _xml_print(xml): 207 | """ Pretty print xml """ 208 | print etree.tostring(xml, pretty_print=True) 209 | 210 | 211 | """ Run PDAL commands """ 212 | 213 | 214 | def run_pipeline(xml, verbose=False): 215 | """ Run PDAL Pipeline with provided XML """ 216 | if verbose: 217 | _xml_print(xml) 218 | 219 | # write to temp file 220 | f, xmlfile = tempfile.mkstemp(suffix='.xml') 221 | if verbose: 222 | print 'Pipeline file: %s' % xmlfile 223 | os.write(f, etree.tostring(xml)) 224 | os.close(f) 225 | 226 | cmd = [ 227 | 'pdal', 228 | 'pipeline', 229 | '-i %s' % xmlfile, 230 | '-v4', 231 | ] 232 | if verbose: 233 | out = os.system(' '.join(cmd)) 234 | else: 235 | out = os.system(' '.join(cmd) + ' > /dev/null 2>&1') 236 | os.remove(xmlfile) 237 | 238 | 239 | def run_pdalground(fin, fout, slope, cellsize, maxWindowSize, maxDistance, verbose=False): 240 | """ Run PDAL ground """ 241 | cmd = [ 242 | 'pdal', 243 | 'ground', 244 | '-i %s' % fin, 245 | '-o %s' % fout, 246 | '--slope %s' % slope, 247 | '--cellSize %s' % cellsize 248 | ] 249 | if maxWindowSize is not None: 250 | cmd.append('--maxWindowSize %s' %maxWindowSize) 251 | if maxDistance is not None: 252 | cmd.append('--maxDistance %s' %maxDistance) 253 | 254 | cmd.append('--classify') 255 | 256 | if verbose: 257 | cmd.append('-v1') 258 | print ' '.join(cmd) 259 | print ' '.join(cmd) 260 | out = os.system(' '.join(cmd)) 261 | if verbose: 262 | print out 263 | 264 | 265 | # LiDAR Classification and DEM creation 266 | 267 | def merge_files(filenames, fout=None, site=None, buff=20, decimation=None, verbose=False): 268 | """ Create merged las file """ 269 | start = datetime.now() 270 | if fout is None: 271 | # TODO ? use temp folder? 272 | fout = os.path.join(os.path.abspath(os.path.dirname(filenames[0])), str(uuid.uuid4()) + '.las') 273 | xml = _xml_las_base(fout) 274 | _xml = xml[0] 275 | if decimation is not None: 276 | _xml = _xml_add_decimation_filter(_xml, decimation) 277 | # need to build PDAL with GEOS 278 | if site is not None: 279 | wkt = loads(site.WKT()).buffer(buff).wkt 280 | _xml = _xml_add_crop_filter(_xml, wkt) 281 | _xml_add_readers(_xml, filenames) 282 | try: 283 | run_pipeline(xml, verbose=verbose) 284 | except: 285 | raise Exception("Error merging LAS files") 286 | print 'Created merged file %s in %s' % (os.path.relpath(fout), datetime.now() - start) 287 | return fout 288 | 289 | 290 | def classify(filenames, fout, slope=None, cellsize=None, maxWindowSize=10, maxDistance=1, 291 | site=None, buff=20, decimation=None, verbose=False): 292 | """ Classify files and output single las file """ 293 | start = datetime.now() 294 | 295 | print 'Classifying %s files into %s' % (len(filenames), os.path.relpath(fout)) 296 | 297 | # problem using PMF in XML - instead merge to ftmp and run 'pdal ground' 298 | ftmp = merge_files(filenames, site=site, buff=buff, decimation=decimation, verbose=verbose) 299 | 300 | try: 301 | run_pdalground(ftmp, fout, slope, cellsize, maxWindowSize, maxDistance, verbose=verbose) 302 | # verify existence of fout 303 | if not os.path.exists(fout): 304 | raise Exception("Error creating classified file %s" % fout) 305 | except: 306 | raise Exception("Error creating classified file %s" % fout) 307 | finally: 308 | # remove temp file 309 | os.remove(ftmp) 310 | 311 | print 'Created %s in %s' % (os.path.relpath(fout), datetime.now() - start) 312 | return fout 313 | 314 | 315 | def create_dems(filenames, demtype, radius=['0.56'], site=None, gapfill=False, 316 | outdir='', suffix='', overwrite=False, **kwargs): 317 | """ Create DEMS for multiple radii, and optionally gapfill """ 318 | fouts = [] 319 | for rad in radius: 320 | fouts.append( 321 | create_dem(filenames, demtype, 322 | radius=rad, site=site, outdir=outdir, suffix=suffix, overwrite=overwrite, **kwargs)) 323 | fnames = {} 324 | # convert from list of dicts, to dict of lists 325 | for product in fouts[0].keys(): 326 | fnames[product] = [f[product] for f in fouts] 327 | fouts = fnames 328 | 329 | # gapfill all products (except density) 330 | _fouts = {} 331 | if gapfill: 332 | for product in fouts.keys(): 333 | # do not gapfill, but keep product pointing to first radius run 334 | if product == 'den': 335 | _fouts[product] = fouts[product][0] 336 | continue 337 | # output filename 338 | bname = '' if site is None else site.Basename() + '_' 339 | fout = os.path.join(outdir, bname + '%s%s.%s.tif' % (demtype, suffix, product)) 340 | if not os.path.exists(fout) or overwrite: 341 | gap_fill(fouts[product], fout, site=site) 342 | _fouts[product] = fout 343 | else: 344 | # only return single filename (first radius run) 345 | for product in fouts.keys(): 346 | _fouts[product] = fouts[product][0] 347 | 348 | return _fouts 349 | 350 | 351 | def create_dem(filenames, demtype, radius='0.56', site=None, decimation=None, 352 | maxsd=None, maxz=None, maxangle=None, returnnum=None, 353 | products=None, outdir='', suffix='', overwrite=False, verbose=False): 354 | """ Create DEM from collection of LAS files """ 355 | start = datetime.now() 356 | # filename based on demtype, radius, and optional suffix 357 | bname = '' if site is None else site.Basename() + '_' 358 | bname = os.path.join(os.path.abspath(outdir), '%s%s_r%s%s' % (bname, demtype, radius, suffix)) 359 | ext = 'tif' 360 | 361 | # products (den, max, etc) 362 | if products is None: 363 | products = dem_products(demtype) 364 | fouts = {o: bname + '.%s.%s' % (o, ext) for o in products} 365 | prettyname = os.path.relpath(bname) + ' [%s]' % (' '.join(products)) 366 | 367 | # run if any products missing (any extension version is ok, i.e. vrt or tif) 368 | run = False 369 | for f in fouts.values(): 370 | if len(glob.glob(f[:-3] + '*')) == 0: 371 | run = True 372 | 373 | if run or overwrite: 374 | print 'Creating %s from %s files' % (prettyname, len(filenames)) 375 | # xml pipeline 376 | xml = _xml_p2g_base(bname, products, radius, site) 377 | _xml = xml[0] 378 | if decimation is not None: 379 | _xml = _xml_add_decimation_filter(_xml, decimation) 380 | _xml = _xml_add_filters(_xml, maxsd, maxz, maxangle, returnnum) 381 | if demtype == 'dsm': 382 | _xml = _xml_add_classification_filter(_xml, 1, equality='max') 383 | elif demtype == 'dtm': 384 | _xml = _xml_add_classification_filter(_xml, 2) 385 | _xml_add_readers(_xml, filenames) 386 | run_pipeline(xml, verbose=verbose) 387 | # verify existence of fout 388 | exists = True 389 | for f in fouts.values(): 390 | if not os.path.exists(f): 391 | exists = False 392 | if not exists: 393 | raise Exception("Error creating dems: %s" % ' '.join(fouts)) 394 | 395 | print 'Completed %s in %s' % (prettyname, datetime.now() - start) 396 | return fouts 397 | -------------------------------------------------------------------------------- /l2d/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # lidar2dems - utilties for creating DEMs from LiDAR data 3 | # 4 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 5 | # 6 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright notice, this 12 | # list of conditions and the following disclaimer. 13 | # 14 | # * Redistributions in binary form must reproduce the above copyright notice, 15 | # this list of conditions and the following disclaimer in the documentation 16 | # and/or other materials provided with the distribution. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | ################################################################################ -------------------------------------------------------------------------------- /l2d/scripts/chm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | """ 32 | Create Canopy Height model from a DSM and DTM 33 | """ 34 | 35 | import os 36 | import argparse 37 | import datetime as dt 38 | from l2d.utils import create_chm, create_hillshade, create_vrt 39 | import gippy 40 | 41 | 42 | def main(): 43 | dhf = argparse.ArgumentDefaultsHelpFormatter 44 | 45 | desc = 'Calculate and create CHM from a DSM and DTM' 46 | parser = argparse.ArgumentParser(description=desc, formatter_class=dhf) 47 | parser.add_argument( 48 | 'demdir', help='Directory holding DEMs (and used to store CHM output') 49 | parser.add_argument( 50 | '-s', '--site', default=None, 51 | help='Site shapefile name (use if used for DTM/DSM creation') 52 | parser.add_argument( 53 | '--dsm', default='dsm.max.tif', 54 | help='Filename of DSM input (will be preceded by feature name if using shapefile') 55 | parser.add_argument( 56 | '--dtm', default='dtm.idw.tif', 57 | help='Filename of DTM input (will be preceded by feature name if using shapefile') 58 | parser.add_argument( 59 | '--fout', default='chm.tif', 60 | help='Output filename (created in demdir)') 61 | parser.add_argument( 62 | '--hillshade', default=False, action='store_true', 63 | help='Generate hillshade') 64 | parser.add_argument( 65 | '-v', '--verbose', default=False, action='store_true', 66 | help='Print additional info') 67 | args = parser.parse_args() 68 | 69 | start = dt.datetime.now() 70 | print 'Creating CHM from DEMS in %s' % (os.path.relpath(args.demdir)) 71 | 72 | if args.site is not None: 73 | site = gippy.GeoVector(args.site) 74 | else: 75 | site = [None] 76 | 77 | fout_final = os.path.join(args.demdir, os.path.splitext(args.fout)[0] + '.vrt') 78 | 79 | fouts = [] 80 | hillfouts = [] 81 | for feature in site: 82 | prefix = os.path.join(args.demdir, '' if feature is None else feature.Basename() + '_') 83 | fdtm = prefix + args.dtm 84 | fdsm = prefix + args.dsm 85 | if not os.path.exists(fdtm) or not os.path.exists(fdsm): 86 | print "No valid input files found (%s)" % prefix 87 | continue 88 | try: 89 | fout = create_chm(fdtm, fdsm, prefix + args.fout) 90 | fouts.append(fout) 91 | except Exception as e: 92 | print "Error creating %s: %s" % (fout, e) 93 | if args.verbose: 94 | import traceback 95 | print traceback.format_exc() 96 | 97 | if args.hillshade: 98 | hillfouts.append(create_hillshade(fout)) 99 | 100 | # if multiple file output then combine them together 101 | if len(fouts) > 0 and site[0] is not None: 102 | create_vrt(fouts, fout_final, site=site) 103 | if args.hillshade: 104 | fout = os.path.splitext(fout_final)[0] + '_hillshade.tif' 105 | create_vrt(hillfouts, fout, site=site) 106 | 107 | print 'Completed %s in %s' % (fout_final, dt.datetime.now() - start) 108 | 109 | 110 | if __name__ == "__main__": 111 | main() 112 | -------------------------------------------------------------------------------- /l2d/scripts/classify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | """ 32 | Calls `pdal ground` for all provided filenames and creates new las file output with parameters in output filenames 33 | """ 34 | 35 | import os 36 | import argparse 37 | from datetime import datetime 38 | from l2d.utils import find_lasfiles, get_classification_filename, class_params 39 | from l2d.pdal import classify 40 | import gippy 41 | 42 | 43 | def main(): 44 | dhf = argparse.ArgumentDefaultsHelpFormatter 45 | 46 | parser = argparse.ArgumentParser(description='Classify LAS file(s)', formatter_class=dhf) 47 | parser.add_argument('lasdir', help='Directory of LAS file(s) to classify') 48 | parser.add_argument('-s', '--site', help='Polygon(s) to process', default=None) 49 | h = 'Amount to buffer out site polygons when merging LAS files' 50 | parser.add_argument('-b', '--buff', help=h, default=20) 51 | parser.add_argument('--slope', help='Slope (override)', default=None) 52 | parser.add_argument('--cellsize', help='Cell Size (override)', default=None) 53 | parser.add_argument('--maxWindowSize', help='Max Window Size (override)', default=None) 54 | parser.add_argument('--maxDistance', help='Max Distance (override)', default=None) 55 | parser.add_argument('--outdir', help='Output directory location', default='./') 56 | h = 'Decimate the points (steps between points, 1 is no pruning' 57 | parser.add_argument('--decimation', help=h, default=None) 58 | parser.add_argument( 59 | '-o', '--overwrite', default=False, action='store_true', 60 | help='Overwrite any existing output files') 61 | parser.add_argument('-v', '--verbose', help='Print additional info', default=False, action='store_true') 62 | 63 | args = parser.parse_args() 64 | 65 | start = datetime.now() 66 | 67 | if not os.path.exists(args.outdir): 68 | os.makedirs(args.outdir) 69 | 70 | if args.site is not None: 71 | site = gippy.GeoVector(args.site) 72 | else: 73 | site = [None] 74 | 75 | fouts = [] 76 | for feature in site: 77 | # get output filename 78 | fout = get_classification_filename(feature, args.outdir, args.slope, args.cellsize) 79 | 80 | # retrieve parameters from input site 81 | slope, cellsize = class_params(feature, args.slope, args.cellsize) 82 | 83 | if not os.path.exists(fout) or args.overwrite: 84 | try: 85 | filenames = find_lasfiles(args.lasdir, site=feature, checkoverlap=True) 86 | fout = classify(filenames, fout, slope=slope, cellsize=cellsize, 87 | site=feature, buff=args.buff, 88 | decimation=args.decimation, verbose=args.verbose) 89 | except Exception as e: 90 | print "Error creating %s: %s" % (os.path.relpath(fout), e) 91 | if args.verbose: 92 | import traceback 93 | print traceback.format_exc() 94 | fouts.append(fout) 95 | 96 | print 'l2d_classify completed in %s' % (datetime.now() - start) 97 | 98 | 99 | if __name__ == '__main__': 100 | main() 101 | -------------------------------------------------------------------------------- /l2d/scripts/dems.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | """ 32 | Creates density image of all files 33 | """ 34 | 35 | import os 36 | from datetime import datetime 37 | from l2d.pdal import create_dems 38 | from l2d.utils import find_lasfiles, find_classified_lasfile, dem_products, class_params, create_vrt 39 | from l2d.parsers import l2dParser 40 | from gippy import GeoVector 41 | 42 | 43 | def main(): 44 | parser = l2dParser(description='Create DEM(s) from LiDAR files', commands=True) 45 | parser.add_input_parser() 46 | parser.add_output_parser() 47 | parser.add_filter_parser() 48 | # parser.add_argument('--vendor_classified', 49 | # help='Files are not classified by l2d, the l2d naming scheme was not used for classified files', 50 | # default=False) 51 | args = parser.parse_args() 52 | 53 | start0 = datetime.now() 54 | 55 | lasdir = args.lasdir 56 | 57 | # open site vector 58 | if args.site is not None: 59 | try: 60 | site = GeoVector(args.site) 61 | except: 62 | print 'Error opening %s' % args.site 63 | exit(2) 64 | else: 65 | site = [None] 66 | 67 | # make sure outdir exists 68 | args.outdir = os.path.abspath(args.outdir) 69 | if not os.path.exists(args.outdir): 70 | os.makedirs(args.outdir) 71 | 72 | args.lasdir = os.path.abspath(args.lasdir) 73 | 74 | # the final filenames 75 | products = dem_products(args.demtype) 76 | bnames = {p: '%s%s.%s' % (args.demtype, args.suffix, p) for p in products} 77 | prefix = '' # if args.site is None else site.Basename() + '_' 78 | fouts = {p: os.path.join(args.outdir, '%s%s%s.%s.vrt' % (prefix, args.demtype, args.suffix, p)) for p in products} 79 | 80 | # pull out the arguments to pass to create_dems 81 | keys = ['radius', 'decimation', 'maxsd', 'maxz', 'maxangle', 'returnnum', 82 | 'outdir', 'suffix', 'verbose', 'overwrite'] 83 | vargs = vars(args) 84 | kwargs = {k: vargs[k] for k in vargs if k in keys} 85 | 86 | # run if any products are missing 87 | exists = all([os.path.exists(f) for f in fouts.values()]) 88 | if exists and not args.overwrite: 89 | print 'Already created %s in %s' % (args.demtype, os.path.relpath(args.outdir)) 90 | exit(0) 91 | 92 | # loop through features 93 | pieces = [] 94 | for feature in site: 95 | try: 96 | # find las files 97 | if args.demtype == 'density': 98 | lasfiles = find_lasfiles(args.lasdir, site=feature, checkoverlap=True) 99 | else: 100 | if args.vendor_classified == False: 101 | parameters = class_params(feature, args.slope, args.cellsize) 102 | lasfiles = find_classified_lasfile(args.lasdir, site=feature, params=parameters) 103 | else: 104 | lasfiles = find_lasfiles(args.lasdir, site=feature, checkoverlap=True) 105 | # create dems 106 | pouts = create_dems(lasfiles, args.demtype, site=feature, gapfill=args.gapfill, **kwargs) 107 | # NOTE - if gapfill then fouts is dict, otherwise is list of dicts (1 for each radius) 108 | pieces.append(pouts) 109 | except Exception, e: 110 | print "Error creating %s %s: %s" % (args.demtype, '' if feature is None else feature.Basename(), e) 111 | if args.verbose: 112 | import traceback 113 | print traceback.format_exc() 114 | 115 | # combine all features into single file and align to site 116 | for product in products: 117 | # there will be mult if gapfill False and multiple radii....use 1st one 118 | fnames = [piece[product] for piece in pieces] 119 | if len(fnames) > 0: 120 | create_vrt(fnames, fouts[product], site=site) 121 | 122 | 123 | print 'l2d_dems %s completed (%s) in %s' % (args.demtype, os.path.relpath(args.outdir), datetime.now() - start0) 124 | 125 | 126 | if __name__ == '__main__': 127 | main() 128 | -------------------------------------------------------------------------------- /l2d/scripts/process_voxels.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # this script can serve as an example for post-processing voxels 4 | # from here, you're on your own! 5 | # note the three critical (and general) steps involved: 6 | # 1. read voxel image to array 7 | # 2. perform aggregation if needed and calculation of region of interest 8 | # 3. output image with calculated metric 9 | 10 | """ 11 | Creates raster of transformed relative density models for declared region of interest 12 | """ 13 | 14 | from datetime import datetime 15 | import argparse 16 | from osgeo import gdal 17 | from l2d.utils import find_lasfiles, find_classified_lasfile, create_vrt, class_params 18 | from l2d.voxel_utils import aggregate, clip_by_site 19 | import os, math, numpy, glob, sys 20 | from scipy import signal, interpolate 21 | import scipy 22 | import gippy 23 | from gippy import GeoVector 24 | 25 | def main(): 26 | dhf = argparse.ArgumentDefaultsHelpFormatter 27 | 28 | desc = 'Process voxels into relative density metrics; note this script will require modifications for specific calculations, users are responsible for this' 29 | parser = argparse.ArgumentParser(description=desc, formatter_class=dhf) 30 | parser.add_argument( 31 | 'voxdir', help='Directory holding voxel lidar data') 32 | parser.add_argument( 33 | '--voxtype', help='Type of return data to use for calculations', nargs='*', default=['count']) 34 | parser.add_argument( 35 | '--metric', help='Metric name user defined, used for naming output image', default=None) 36 | parser.add_argument( 37 | '--start', help='Low height of relative density region of interest', default=['1']) 38 | parser.add_argument( 39 | '--stop', help='Top height of relative density region of interest', default=['5']) 40 | parser.add_argument( 41 | '--pixelsize', help='Output image pixel size, used to aggregate voxels in x-y dimension', default=['1']) 42 | parser.add_argument( 43 | '-s', '--site', default=None, 44 | help='Site shapefile name used for ') 45 | parser.add_argument( 46 | '--outdir', help='Directory to output metric rasters, directory name should specify type of metric') 47 | parser.add_argument( 48 | '-o', '--overwrite', default=False, action='store_true', 49 | help='Overwrite any existing output files') 50 | parser.add_argument( 51 | '-v', '--verbose', default=False, action='store_true', 52 | help='Print additional info') 53 | args = parser.parse_args() 54 | 55 | start0 = datetime.now() 56 | 57 | #variables describing region of interest and scale 58 | startoff = int(args.start) 59 | cutoff = int(args.stop) 60 | pixelsize = int(args.pixelsize) 61 | if args.metric is None: 62 | args.metric = 'rdm-%s_to_%s' %(startoff,cutoff) 63 | voxdir = args.voxdir 64 | 65 | # make sure outdir exists 66 | args.outdir = os.path.abspath(args.outdir) 67 | if not os.path.exists(args.outdir): 68 | os.makedirs(args.outdir) 69 | 70 | # the final filenames 71 | product = args.metric 72 | #fouts = os.path.join(args.outdir, '%s.voxel_metric.vrt' % (product)) 73 | fouts = {p: os.path.join(args.outdir, '%s.voxel_metric.vrt' % (p)) for p in [product]} 74 | 75 | # run if any products are missing 76 | exists = all([os.path.exists(f) for f in fouts.values()]) 77 | if exists and not args.overwrite: 78 | print 'Already created metric rasters in %s' % (os.path.relpath(args.outdir)) 79 | exit(0) 80 | 81 | # loop through voxel rasters 82 | # site = glob.glob('*.%s.*.tif' %(args.voxtype)) 83 | # open site vector 84 | if args.site is not None: 85 | try: 86 | site = GeoVector(args.site) 87 | except: 88 | print 'Error opening %s' % args.site 89 | exit(2) 90 | else: 91 | site = [None] 92 | 93 | pieces = [] 94 | for feature in site: 95 | try: 96 | 97 | # extract naming convention 98 | bname = '' if feature is None else feature.Basename() + '_' 99 | ftr = bname.split('_')[0] 100 | bname = os.path.join(os.path.abspath(voxdir), '%s' % (bname)) 101 | vox_name = bname + 'voxels.%s.tif' %(args.voxtype[0]) 102 | out = os.path.join(args.outdir, '%s_%s.voxel_metric.tif' % (ftr,product)) 103 | print out 104 | 105 | #open image 106 | vox_img = gippy.GeoImage(vox_name) 107 | proj = vox_img.Projection() 108 | affine = vox_img.Affine() 109 | affine[1],affine[5] = pixelsize,-pixelsize 110 | vox_arr = vox_img.Read().squeeze() 111 | # vox_img = gdal.Open(vox_name) 112 | # vox_arr = vox_img.ReadAsArray() 113 | nbands,nrows,ncols = vox_arr.shape 114 | print 'voxel dimensions: %s, %s, %s' %(nbands,nrows,ncols) 115 | sys.stdout.flush() 116 | 117 | # calculate relative density ratio of returns -- this section of code is the section that should be modified depending on needs 118 | # modifications if desired; also note, startoff and cutoff values may not be relevant if code is changed 119 | data = aggregate(vox_arr,pixelsize) 120 | nbands,nrows,ncols = data.shape 121 | print 'aggregated dimensions: %s, %s, %s, Calculating...' %data.shape 122 | sys.stdout.flush() 123 | i1 = numpy.sum(data[startoff+1:cutoff+1],axis=0,dtype=float) 124 | i2 = numpy.sum(data,axis=0,dtype=float) 125 | 126 | ratio = numpy.zeros(i1.shape, dtype=float) 127 | ratio[numpy.where(i2>0)] = i1[numpy.where(i2>0)]/i2[numpy.where(i2>0)] 128 | transformed = numpy.sqrt(ratio)+0.001 129 | 130 | print 'writing image' 131 | sys.stdout.flush() 132 | #output ratio image 133 | 134 | imgout = gippy.GeoImage(out,ncols,nrows,1,gippy.GDT_Float32) 135 | imgout.SetProjection(proj) 136 | imgout.SetAffine(affine) 137 | imgout[0].Write(transformed) 138 | imgout = None 139 | # 140 | # modification area ends 141 | 142 | print 'clipping image to feature' 143 | 144 | clip_by_site(out,feature) 145 | 146 | pieces.append(out) 147 | 148 | except Exception, e: 149 | print "Error creating metric: %s" % e 150 | if args.verbose: 151 | import traceback 152 | print traceback.format_exc() 153 | 154 | 155 | # combine all features into single file and align to site for chm 156 | create_vrt(pieces, fouts[product], site=site) 157 | 158 | 159 | print 'l2d_process_voxels completed (%s) in %s' % (os.path.relpath(args.outdir), datetime.now() - start0) 160 | 161 | 162 | if __name__ == '__main__': 163 | main() 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /l2d/scripts/voxelize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | """ 32 | Creates raster of return or intensity counts in cubic meter voxels 33 | """ 34 | 35 | import os 36 | from datetime import datetime 37 | import argparse 38 | from l2d.voxel_utils import create_voxels 39 | from l2d.utils import find_lasfiles, find_classified_lasfile, create_vrt, class_params 40 | from l2d.parsers import l2dParser 41 | from gippy import GeoVector 42 | 43 | def main(): 44 | dhf = argparse.ArgumentDefaultsHelpFormatter 45 | 46 | desc = 'Voxelize lidar data to output rasters' 47 | parser = argparse.ArgumentParser(description=desc, formatter_class=dhf) 48 | parser.add_argument( 49 | 'lasdir', help='Directory holding classified LAS files') 50 | parser.add_argument( 51 | 'demdir', help='Directory holding DEMs (including DSM and DTM for each feature)') 52 | parser.add_argument( 53 | '--voxtypes', help='Type of return data in output voxels (e.g. counts, intensity); option to output new CHM with "chm"', nargs='*', default=['count','intensity']) 54 | parser.add_argument( 55 | '-s', '--site', default=None, 56 | help='Site shapefile name (use if used for DTM/DSM creation); if area of interest is smaller than whole scene, l2d_dems should be run again using voxel region of interest shapefile') 57 | parser.add_argument( 58 | '--vendor_classified', 59 | help='Files are not classified by l2d, the l2d naming scheme was not used for classified files', default=False) 60 | parser.add_argument('--slope', help='Slope (override)', default=None) 61 | parser.add_argument('--cellsize', help='Cell Size (override)', default=None) 62 | parser.add_argument( 63 | '--outdir', help='Directory to output voxel rasters') 64 | parser.add_argument( 65 | '-o', '--overwrite', default=False, action='store_true', 66 | help='Overwrite any existing output files') 67 | parser.add_argument( 68 | '-v', '--verbose', default=False, action='store_true', 69 | help='Print additional info') 70 | args = parser.parse_args() 71 | 72 | start0 = datetime.now() 73 | 74 | # open site vector 75 | if args.site is not None: 76 | try: 77 | site = GeoVector(args.site) 78 | except: 79 | print 'Error opening %s' % args.site 80 | exit(2) 81 | else: 82 | site = [None] 83 | 84 | # make sure outdir exists 85 | args.outdir = os.path.abspath(args.outdir) 86 | if not os.path.exists(args.outdir): 87 | os.makedirs(args.outdir) 88 | 89 | args.lasdir = os.path.abspath(args.lasdir) 90 | 91 | # the final filenames 92 | products = args.voxtypes 93 | fouts = {p: os.path.join(args.outdir, '%s.voxels.vrt' % (p)) for p in products} 94 | 95 | # run if any products are missing 96 | exists = all([os.path.exists(f) for f in fouts.values()]) 97 | if exists and not args.overwrite: 98 | print 'Already created %s in %s' % (args.voxtypes, os.path.relpath(args.outdir)) 99 | exit(0) 100 | 101 | # loop through features 102 | pieces = [] 103 | for feature in site: 104 | try: 105 | # find las files 106 | if args.vendor_classified == False: 107 | parameters = class_params(feature, args.slope, args.cellsize) 108 | lasfiles = find_classified_lasfile(args.lasdir, site=feature, params=parameters) 109 | else: 110 | lasfiles = find_lasfiles(args.lasdir, site=feature, checkoverlap=True) 111 | # create voxels - perhaps not loop over features, but instead voxelize each tile...for loop over lasfiles here. would need to determine output image dimensions though since they could no longer be pulled from existing feature geotiff. 112 | pouts = create_voxels(lasfiles, voxtypes=args.voxtypes, demdir=args.demdir, site=feature, 113 | outdir=args.outdir, overwrite=args.overwrite) 114 | pieces.append(pouts) 115 | except Exception, e: 116 | print "Error creating voxels: %s" % e 117 | if args.verbose: 118 | import traceback 119 | print traceback.format_exc() 120 | 121 | # combine all features into single file and align to site for chm 122 | if 'chm' in products: 123 | fnames = [piece['chm'] for piece in pieces] 124 | if len(fnames) > 0: 125 | create_vrt(fnames, fouts['chm'], site=site) 126 | 127 | 128 | print 'l2d_voxelize completed (%s) in %s' % (os.path.relpath(args.outdir), datetime.now() - start0) 129 | 130 | 131 | if __name__ == '__main__': 132 | main() 133 | -------------------------------------------------------------------------------- /l2d/tiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | import argparse 32 | from glob import glob 33 | from copy import deepcopy 34 | from os import path, remove 35 | from datetime import datetime 36 | from collections import OrderedDict 37 | 38 | from l2d.geo import get_bounding_box, Vector 39 | from fiona import collection 40 | 41 | _polygon_template = { 42 | 'geometry': { 43 | 'coordinates': [], # list of lists of points defining box 44 | 'type': 'Polygon' 45 | }, 46 | 'id': '0', 47 | 'properties': OrderedDict([(u'las_file', u'')]), 48 | 'type': 'Feature' 49 | } 50 | 51 | _crs_template = { 52 | u'datum': u'WGS84', 53 | u'no_defs': True, 54 | u'proj': u'utm', 55 | u'units': u'm', 56 | u'zone': 50 57 | } 58 | 59 | _polygon_file_schema = { 60 | 'geometry': 'Polygon', 61 | 'properties': OrderedDict([(u'las_file', 'str')]) 62 | } 63 | 64 | 65 | def lasdir2shp(lasdir, fout, crs, overwrite=False): 66 | ''' 67 | Map each file in the lasdir to a polygon in the shapefile fout. 68 | ''' 69 | if path.exists(fout): 70 | if overwrite: 71 | remove(fout) 72 | else: 73 | print('Output file {} already exists. Skipping...'.format(fout)) 74 | return 75 | 76 | filenames = glob(path.join(lasdir, '*.las')) 77 | oschema = _polygon_file_schema.copy() 78 | poly = deepcopy(_polygon_template) 79 | 80 | with collection( 81 | fout, 'w', crs=crs, driver="ESRI Shapefile", schema=oschema 82 | ) as oshp: 83 | for filename in filenames: 84 | try: 85 | poly['geometry']['coordinates'] = [get_bounding_box(filename)] 86 | except Exception as e: 87 | if 'min_points' in str(e): 88 | continue 89 | raise e 90 | poly['properties']['las_file'] = filename 91 | oshp.write(poly) 92 | poly['id'] = str(int(poly['id']) + 1) 93 | 94 | 95 | def process_polygon_dirs(parentdir, shapename, overwrite=False,): 96 | ''' 97 | Find all directories in parentdir, and create a polygon shapefile in each 98 | directory using lasdir2shp. 99 | ''' 100 | params = sorted( 101 | map( 102 | lambda x: [x] + x.split('_')[1:4:2], 103 | glob(path.join(parentdir, 'Polygon_???_utm_???')) 104 | ), 105 | key=lambda y: y[1], 106 | ) 107 | 108 | for poly_dir, poly_id, utmzone in params: 109 | print poly_dir, poly_id, utmzone 110 | crs = deepcopy(_crs_template) 111 | if utmzone.endswith('S'): 112 | crs['south'] = True 113 | crs['zone'] = utmzone[:-1] 114 | lasdir2shp( 115 | path.join(poly_dir, 'LAS'), 116 | path.join(poly_dir, 'tiles.shp'), 117 | crs, 118 | overwrite, 119 | ) 120 | 121 | 122 | def main(): 123 | dhf = argparse.ArgumentDefaultsHelpFormatter 124 | 125 | parser = argparse.ArgumentParser(description='Create shapefile showing bounds of LAS files', formatter_class=dhf) 126 | parser.add_argument('files', help="List of input LiDAR files") 127 | parser.add_argument( 128 | '-s', '--shapefile', required=False, default='tiles.shp', 129 | help='Name of output shapefile') 130 | parser.add_argument( 131 | '-o', '--overwrite', required=False, default=False, action='store_true', 132 | help='Overwrite existing output shapefile') 133 | parser.add_argument( 134 | '-epsg', default=4326, 135 | help='EPSG code of LiDAR spatial reference system') 136 | parser.add_argument( 137 | '-v', '--verbose', default=False, action='store_true', 138 | help='Print additional info') 139 | 140 | # args = parser.parse_args() 141 | 142 | # start = datetime.now() 143 | 144 | print 'l2d_tiles not yet implemented' 145 | 146 | # print 'Completed in %s' % (datetime.now() - start) 147 | 148 | 149 | if __name__ == '__main__': 150 | main() 151 | -------------------------------------------------------------------------------- /l2d/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | # Utilities mostly for interfacing with file system and settings 32 | 33 | 34 | import os 35 | import sys 36 | import glob 37 | from datetime import datetime 38 | import gippy 39 | from gippy.algorithms import CookieCutter 40 | import numpy 41 | from .geo import check_overlap, get_vector_bounds 42 | 43 | 44 | # File utilities 45 | 46 | def get_classification_filename(site, outdir='', slope=None, cellsize=None, suffix=''): 47 | """ Generate filename for classification """ 48 | fout = '' if site is None else site.Basename() + '_' 49 | slope, cellsize = class_params(site, slope, cellsize) 50 | fout = os.path.join(os.path.abspath(outdir), fout + class_suffix(slope, cellsize, suffix)) 51 | return fout 52 | 53 | 54 | def dem_products(demtype): 55 | """ Return products for this dem type """ 56 | products = { 57 | 'density': ['den'], 58 | 'dsm': ['den', 'max'], 59 | 'dtm': ['den', 'min', 'max', 'idw'] 60 | } 61 | return products[demtype] 62 | 63 | 64 | def splitexts(filename): 65 | """ Split off two extensions """ 66 | bname, ext = os.path.splitext(filename) 67 | parts = os.path.splitext(bname) 68 | if len(parts) == 2 and parts[1] in ['.den', '.min', '.max', '.mean', '.idw']: 69 | bname = parts[0] 70 | ext = parts[1] + ext 71 | return bname, ext 72 | 73 | 74 | def class_params(feature, slope=None, cellsize=None): 75 | """ Get classification parameters based on land classification """ 76 | if slope is not None and cellsize is not None: 77 | return (slope, cellsize) 78 | else: 79 | try: 80 | # TODO - read in from config file ? 81 | params = { 82 | '1': (1, 3), # non-forest, flat 83 | '2': (1, 2), # forest, flat 84 | '3': (5, 2), # non-forest, complex 85 | '4': (10, 2), # forest, complex 86 | } 87 | return params[feature['class']] 88 | except: 89 | if slope is None: 90 | slope = '1' 91 | if cellsize is None: 92 | cellsize = '3' 93 | return (slope, cellsize) 94 | 95 | 96 | def class_suffix(slope, cellsize, suffix=''): 97 | """" Generate LAS classification suffix """ 98 | return '%sl2d_s%sc%s.las' % (suffix, slope, cellsize) 99 | 100 | 101 | def find_lasfiles(lasdir='', site=None, checkoverlap=False): 102 | """" Find lasfiles intersecting with site """ 103 | filenames = glob.glob(os.path.join(lasdir, '*.las')) 104 | if checkoverlap and site is not None: 105 | filenames = check_overlap(filenames, site) 106 | if len(filenames) == 0: 107 | raise Exception("No LAS files found") 108 | return filenames 109 | 110 | 111 | def find_classified_lasfile(lasdir='', site=None, params=('1', '3')): 112 | """ Locate LAS files within vector or given and/or matching classification parameters """ 113 | bname = '' if site is None else site.Basename() + '_' 114 | slope, cellsize = params[0], params[1] 115 | pattern = bname + class_suffix(slope, cellsize) 116 | filenames = glob.glob(os.path.join(lasdir, pattern)) 117 | if len(filenames) == 0: 118 | raise Exception("No classified LAS files found") 119 | return filenames 120 | 121 | 122 | # Image processing utilities 123 | 124 | 125 | def create_chm(dtm, dsm, chm): 126 | """ Create CHM from a DTM and DSM - assumes common grid """ 127 | dtm_img = gippy.GeoImage(dtm) 128 | dsm_img = gippy.GeoImage(dsm) 129 | imgout = gippy.GeoImage(chm, dtm_img) 130 | 131 | # set nodata 132 | dtm_nodata = dtm_img[0].NoDataValue() 133 | dsm_nodata = dsm_img[0].NoDataValue() 134 | nodata = dtm_nodata 135 | imgout.SetNoData(nodata) 136 | 137 | dsm_arr = dsm_img[0].Read() 138 | dtm_arr = dtm_img[0].Read() 139 | 140 | # ensure same size arrays, clip to smallest 141 | s1 = dsm_arr.shape 142 | s2 = dtm_arr.shape 143 | if s1 != s2: 144 | if s1[0] > s2[0]: 145 | dsm_arr = dsm_arr[0:s2[0], :] 146 | elif s2[0] > s1[0]: 147 | dtm_arr = dtm_arr[0:s1[0], :] 148 | if s1[1] > s2[1]: 149 | dsm_arr = dsm_arr[:, 0:s2[1]] 150 | elif s2[1] > s1[1]: 151 | dtm_arr = dtm_arr[:, 0:s1[1]] 152 | 153 | arr = dsm_arr - dtm_arr 154 | 155 | # set to nodata if no ground pixel 156 | arr[dtm_arr == dtm_nodata] = nodata 157 | # set to nodata if no surface pixel 158 | locs = numpy.where(dsm_arr == dsm_nodata) 159 | arr[locs] = nodata 160 | 161 | imgout[0].Write(arr) 162 | return imgout.Filename() 163 | 164 | 165 | def gap_fill(filenames, fout, site=None, interpolation='nearest'): 166 | """ Gap fill from higher radius DTMs, then fill remainder with interpolation """ 167 | start = datetime.now() 168 | from scipy.interpolate import griddata 169 | if len(filenames) == 0: 170 | raise Exception('No filenames provided!') 171 | 172 | filenames = sorted(filenames) 173 | imgs = gippy.GeoImages(filenames) 174 | nodata = imgs[0][0].NoDataValue() 175 | arr = imgs[0][0].Read() 176 | 177 | for i in range(1, imgs.size()): 178 | locs = numpy.where(arr == nodata) 179 | arr[locs] = imgs[i][0].Read()[locs] 180 | 181 | # interpolation at bad points 182 | goodlocs = numpy.where(arr != nodata) 183 | badlocs = numpy.where(arr == nodata) 184 | arr[badlocs] = griddata(goodlocs, arr[goodlocs], badlocs, method=interpolation) 185 | 186 | # write output 187 | imgout = gippy.GeoImage(fout, imgs[0]) 188 | imgout.SetNoData(nodata) 189 | imgout[0].Write(arr) 190 | fout = imgout.Filename() 191 | imgout = None 192 | 193 | # align and clip 194 | if site is not None: 195 | from osgeo import gdal 196 | # get resolution 197 | ds = gdal.Open(fout, gdal.GA_ReadOnly) 198 | gt = ds.GetGeoTransform() 199 | ds = None 200 | parts = splitexts(fout) 201 | _fout = parts[0] + '_clip' + parts[1] 202 | CookieCutter(gippy.GeoImages([fout]), site, _fout, gt[1], abs(gt[5]), True) 203 | if os.path.exists(fout): 204 | os.remove(fout) 205 | os.rename(_fout, fout) 206 | 207 | print 'Completed gap-filling to create %s in %s' % (os.path.relpath(fout), datetime.now() - start) 208 | 209 | return fout 210 | 211 | 212 | # GDAL Utility wrappers 213 | 214 | 215 | def create_vrt(filenames, fout, site=[None], overwrite=False, verbose=False): 216 | """ Combine filenames into single file and align if given site """ 217 | if os.path.exists(fout) and not overwrite: 218 | return fout 219 | cmd = [ 220 | 'gdalbuildvrt', 221 | ] 222 | if not verbose: 223 | cmd.append('-q') 224 | if site[0] is not None: 225 | bounds = get_vector_bounds(site) 226 | cmd.append('-te %s' % (' '.join(map(str, bounds)))) 227 | cmd.append(fout) 228 | cmd = cmd + filenames 229 | if verbose: 230 | print 'Combining %s files into %s' % (len(filenames), fout) 231 | # print ' '.join(cmd) 232 | # subprocess.check_output(cmd) 233 | os.system(' '.join(cmd)) 234 | return fout 235 | 236 | 237 | def create_hillshade(filename): 238 | """ Create hillshade image from this file """ 239 | fout = os.path.splitext(filename)[0] + '_hillshade.tif' 240 | sys.stdout.write('Creating hillshade: ') 241 | sys.stdout.flush() 242 | cmd = 'gdaldem hillshade %s %s' % (filename, fout) 243 | os.system(cmd) 244 | return fout 245 | -------------------------------------------------------------------------------- /l2d/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | __version__ = '1.1.0b1' 32 | -------------------------------------------------------------------------------- /l2d/voxel_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Franklin Sullivan, fsulliva@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | # Library functions for creating voxel rasters from Lidar data 32 | 33 | import os, numpy, math, sys 34 | import gippy 35 | import glob 36 | from datetime import datetime 37 | from laspy import file 38 | from gippy.algorithms import CookieCutter 39 | from .utils import splitexts 40 | 41 | 42 | # text file management 43 | def run_las2txt(fin, fout, verbose=False): 44 | """ Run las2txt """ 45 | cmd = [ 46 | 'las2txt', 47 | '-i %s' % fin, 48 | '-o %s' % fout, 49 | '-parse %s' % 'xyzirncpt', 50 | '-sep %s' % 'komma', 51 | ] 52 | if verbose: 53 | cmd.append('-v1') 54 | print ' '.join(cmd) 55 | out = os.system(' '.join(cmd)) 56 | if verbose: 57 | print out 58 | 59 | def delete_txtfile(f): 60 | """ Delete tmp txt file """ 61 | cmd = [ 62 | 'rm', 63 | f, 64 | ] 65 | out = os.system(' '.join(cmd)) 66 | 67 | # point record scaling functions 68 | 69 | def scale_x(las_file, point): 70 | """ Calculate scaled value of x for point record """ 71 | _px = point.X 72 | scale = las_file.header.scale[0] 73 | offset = las_file.header.offset[0] 74 | return _px*scale+offset 75 | 76 | def scale_y(las_file, point): 77 | """ Calculate scaled value of x for point record """ 78 | _py = point.Y 79 | scale = las_file.header.scale[1] 80 | offset = las_file.header.offset[1] 81 | return _py*scale+offset 82 | 83 | def scale_z(las_file, point): 84 | """ Calculate scaled value of x for point record """ 85 | _pz = point.Z 86 | scale = las_file.header.scale[2] 87 | offset = las_file.header.offset[2] 88 | return _pz*scale+offset 89 | 90 | 91 | # dtm value location tools 92 | 93 | def coldex(x, xi, res): 94 | 95 | out = int((x-xi)/res) 96 | 97 | return out 98 | 99 | 100 | def rowdex(y, yi, res): 101 | 102 | out = int((yi-y)/res) 103 | 104 | return out 105 | 106 | 107 | def get_dtm_value(dtm, x, y, xi, yi, dtm_res, y_size, x_size): 108 | """ Retrieve ground elevation below lidar return """ 109 | 110 | col = coldex(x, xi, dtm_res) 111 | if (col == x_size): 112 | col = int(x_size-1) 113 | row = rowdex(y, yi, dtm_res) 114 | if (row == y_size): 115 | row = int(y_size-1) 116 | 117 | zd = dtm[row][col] 118 | 119 | return zd 120 | 121 | # generation of voxels 122 | 123 | def create_voxels(filenames, voxtypes=['count','intensity'], demdir='.', site=None, outdir='', overwrite=False, verbose=False): 124 | 125 | """ Create voxels from LAS file """ 126 | 127 | start = datetime.now() 128 | # filename based on feature number 129 | bname = '' if site is None else site.Basename() + '_' 130 | dtmname = '%sdtm.idw.vrt' %bname 131 | chmname = '%schm.tif' %bname 132 | bname = os.path.join(os.path.abspath(outdir), '%s' % (bname)) 133 | ext = 'tif' 134 | 135 | # products (vox) 136 | products = voxtypes 137 | fouts = {o: bname + 'voxels.%s.%s' % (o, ext) for o in products} 138 | # print fouts 139 | prettyname = os.path.relpath(bname) + ' [%s]' % (' '.join(products)) 140 | 141 | # run if any products missing (any extension version is ok, i.e. vrt or tif) 142 | run = False 143 | for f in fouts.values(): 144 | if len(glob.glob(f[:-3] + '*')) == 0: 145 | run = True 146 | 147 | if run or overwrite: 148 | # find dtm and chm files and check if they exist 149 | dtmpath = os.path.join(demdir, dtmname) 150 | if not os.path.exists(dtmpath): 151 | dtmname = '%sdtm.idw.tif' %('' if site is None else site.Basename() + '_') 152 | dtmpath = os.path.join(demdir, dtmname) 153 | chmpath = os.path.join(demdir, chmname) 154 | if not os.path.exists(chmpath): 155 | chmname = 'chm.tif' 156 | chmpath = os.path.join(demdir, chmname) 157 | print dtmpath, chmpath 158 | paths = [dtmpath, chmpath] 159 | exists = all([os.path.exists(f) for f in paths]) 160 | # print paths 161 | if not exists: 162 | print 'DTM and/or CHM do not exist: Check DEM directory (%s)!' % (demdir) 163 | exit(0) 164 | 165 | print 'Creating %s from %s files' % (prettyname, len(filenames)) 166 | voxelize(filenames,voxtypes,site,dtmpath,chmpath,outdir) 167 | else: 168 | print 'Already created %s in %s' % (voxtypes, os.path.relpath(outdir)) 169 | exit(0) 170 | 171 | # check if voxel files were created & align and clip to site 172 | exists=True 173 | for f in fouts.values(): 174 | clip_by_site(f,site) 175 | if not os.path.exists(f): 176 | exists = False 177 | if not exists: 178 | raise Exception("Error creating voxels: %s" % ' '.join(fouts)) 179 | 180 | print 'Completed %s in %s' % (prettyname, datetime.now() - start) 181 | return fouts 182 | sys.stdout.flush() 183 | 184 | 185 | def voxelize(lasfiles, products=['count','intensity'], site=None, dtmpath='', chmpath='', outdir=''): 186 | 187 | # filename based on demtype, radius, and optional suffix 188 | bname = '' if site is None else site.Basename() + '_' 189 | bname = os.path.join(os.path.abspath(outdir), '%s' % (bname)) 190 | 191 | # product output image names 192 | denout = bname + 'voxels.count.tif' 193 | intout = bname + 'voxels.intensity.tif' 194 | chmout = bname + 'voxels.chm.tif' 195 | 196 | # read dtm and chm arrays 197 | dtm_img = gippy.GeoImage(dtmpath) 198 | chm_img = gippy.GeoImage(chmpath) 199 | chm_arr = chm_img[0].Read() 200 | dtm_arr = dtm_img[0].Read() 201 | 202 | dtm_y_shape, dtm_x_shape = dtm_arr.shape 203 | 204 | # chmMax is the number of bands that will be necessary to populate the output grids 205 | chmMax = numpy.int16(math.ceil(numpy.percentile(chm_arr[numpy.where(chm_arr<9999)],99.999))+1) 206 | print 'max canopy height is ', chmMax 207 | 208 | # get geo information from dtm image - unsure if this is needed 209 | srs = dtm_img.Projection() 210 | dtm_gt = dtm_img.Affine() 211 | dtm_minx, dtm_maxy = dtm_gt[0], dtm_gt[3] 212 | 213 | # loop through las file and populate multi-dimensional grid 214 | # create rhp and rhi, multi-dimensional output grids - rhp is density, rhi is intensity sum 215 | rhp = numpy.zeros((chmMax,dtm_y_shape,dtm_x_shape)) 216 | rhi = numpy.zeros((chmMax,dtm_y_shape,dtm_x_shape)) 217 | chm2 = numpy.zeros((dtm_y_shape,dtm_x_shape)) 218 | print 'created them!' 219 | bands,nrows,ncols = rhp.shape 220 | 221 | print "Populating Voxels" 222 | 223 | for lasfile in lasfiles: 224 | print "Iterating over points in files %s" %(lasfile) 225 | sys.stdout.flush() 226 | 227 | f = file.File(lasfile,mode='r') 228 | for p in f: 229 | # print 'x,y,z: ', p.x, p.y, p.z 230 | p.make_nice() 231 | x = scale_x(f, p) 232 | y = scale_y(f, p) 233 | z = scale_z(f, p) 234 | c = p.classification 235 | i = p.intensity 236 | 237 | col = coldex(x, dtm_minx, 1.0) 238 | row = rowdex(y, dtm_maxy, 1.0) 239 | 240 | if (0 <= col < dtm_x_shape) & (0 <= row < dtm_y_shape): 241 | 242 | zd = get_dtm_value(dtm_arr, x, y, dtm_minx, dtm_maxy, 1.0, dtm_y_shape, dtm_x_shape) 243 | z2 = z-zd 244 | 245 | if (c == 2): 246 | 247 | band = 0 248 | if 'count' in products: 249 | rhp[band][row][col] += 1 250 | if 'intensity' in products: 251 | rhi[band][row][col] += i 252 | if 'chm' in products: 253 | band = int(math.ceil(z2)) 254 | if (0 <= band < bands): 255 | chm2[row][col] = numpy.max([z2,chm2[row][col]]) 256 | 257 | else: 258 | 259 | band = int(math.ceil(z2)) 260 | if (0 <= band < bands): 261 | if 'count' in products: 262 | rhp[band][row][col] += 1 263 | if 'intensity' in products: 264 | rhi[band][row][col] += i 265 | if 'chm' in products: 266 | chm2[row][col] = numpy.max([z2,chm2[row][col]]) 267 | 268 | else: 269 | pass 270 | 271 | 272 | # output rhp and rhi images using gippy 273 | 274 | if 'count' in products: 275 | print 'Writing %s' %denout, 'fullest pixel has %i returns' %numpy.max(rhp) 276 | # numpy.save(denout,rhp) 277 | den_img = gippy.GeoImage(denout,dtm_img,gippy.GDT_Int16,bands) 278 | for b in range(0,bands): 279 | den_img[b].Write(rhp[b]) 280 | 281 | if 'intensity' in products: 282 | print 'Writing %s' %intout 283 | # numpy.save(intout,rhi) 284 | int_img = gippy.GeoImage(intout,dtm_img,gippy.GDT_Int32,bands) 285 | for b in range(0,bands): 286 | int_img[b].Write(rhi[b]) 287 | 288 | if 'chm' in products: 289 | print 'Writing %s' %chmout 290 | chm2_img = gippy.GeoImage(chmout,dtm_img,gippy.GDT_Float32) 291 | chm2_img[0].Write(chm2) 292 | 293 | # print "Completed Voxel products for %s" %(bname) 294 | 295 | # post-processing of voxels 296 | 297 | def aggregate(dat, window): 298 | """ Sums cubic meter voxels into coarser sizes """ 299 | dim = len(dat.shape) 300 | if dim==2: 301 | shp = (dat.shape[-2]/window, dat.shape[-1]/window) 302 | agg = numpy.zeros(shp, dtype=int) 303 | y = 0 304 | while y+window <= shp[0]: 305 | y_ = y*window 306 | x = 0 307 | while x+window <= shp[1]: 308 | x_ = x*window 309 | agg[y,x] = numpy.sum(dat[y_:y_+window,x_:x_+window]) 310 | x+=1 311 | y+=1 312 | if dim==3: 313 | shp = (dat.shape[-3], dat.shape[-2]/window, dat.shape[-1]/window) 314 | agg = numpy.zeros(shp, dtype=int) 315 | for z in range(0,shp[-3]): 316 | y = 0 317 | while y+window <= shp[-2]: 318 | y_ = y*window 319 | x = 0 320 | while x+window <= shp[-1]: 321 | x_ = x*window 322 | agg[z,y,x] = numpy.sum(dat[z,y_:y_+window,x_:x_+window]) 323 | x+=1 324 | y+=1 325 | return agg 326 | 327 | 328 | def clip_by_site(fout,site): 329 | 330 | # align and clip 331 | if site is not None: 332 | from osgeo import gdal 333 | # get resolution 334 | ds = gdal.Open(fout, gdal.GA_ReadOnly) 335 | gt = ds.GetGeoTransform() 336 | ds = None 337 | parts = splitexts(fout) 338 | _fout = parts[0] + '_clip' + parts[1] 339 | CookieCutter(gippy.GeoImages([fout]), site, _fout, gt[1], abs(gt[5]), True) 340 | if os.path.exists(fout): 341 | os.remove(fout) 342 | os.rename(_fout, fout) 343 | 344 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | 32 | import os 33 | from setuptools import setup 34 | import imp 35 | import glob 36 | import traceback 37 | 38 | __version__ = imp.load_source('l2d.version', 'l2d/version.py').__version__ 39 | 40 | scripts = [] 41 | for f in glob.glob('l2d/scripts/*.py'): 42 | try: 43 | name = os.path.splitext(os.path.basename(f))[0] 44 | if name not in ['__init__']: 45 | scripts.append('l2d_%s = l2d.scripts.%s:main' % (name, name.lower())) 46 | except: 47 | print traceback.format_exc() 48 | 49 | setup( 50 | name='lidar2dems', 51 | version=__version__, 52 | description='Utilities for creating DEMs from lidar data', 53 | author='Matthew Hanson', 54 | author_email='matt.a.hanson@gmail.com', 55 | license='FreeBSD copyright Applied Geosolutions LLC', 56 | packages=['l2d', 'l2d.scripts'], 57 | install_requires=['gippy', 'lxml', 'shapely', 'gdal', 'fiona'], 58 | entry_points={'console_scripts': scripts} 59 | ) 60 | -------------------------------------------------------------------------------- /test/las/25399731-5024-442b-b348-a8a987504c47.las: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applied-GeoSolutions/lidar2dems/a6a356c5caf38e6485cf130b2c059e6af38d109f/test/las/25399731-5024-442b-b348-a8a987504c47.las -------------------------------------------------------------------------------- /test/las/600eaa2c-af07-4b60-90d8-e1e457156605.las: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applied-GeoSolutions/lidar2dems/a6a356c5caf38e6485cf130b2c059e6af38d109f/test/las/600eaa2c-af07-4b60-90d8-e1e457156605.las -------------------------------------------------------------------------------- /test/las/719a3237-4236-4323-90b0-fb3c2f802b8a.las: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applied-GeoSolutions/lidar2dems/a6a356c5caf38e6485cf130b2c059e6af38d109f/test/las/719a3237-4236-4323-90b0-fb3c2f802b8a.las -------------------------------------------------------------------------------- /test/las/a8d2688b-5cad-4f25-93b0-d349b5234dcd.las: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applied-GeoSolutions/lidar2dems/a6a356c5caf38e6485cf130b2c059e6af38d109f/test/las/a8d2688b-5cad-4f25-93b0-d349b5234dcd.las -------------------------------------------------------------------------------- /test/pdal_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | # last modified 32 | # DATE: <2017-02-01 17:23:30 icooke on north> 33 | 34 | import os 35 | import shutil 36 | import unittest 37 | import gippy 38 | from l2d.utils import find_lasfiles, get_classification_filename, find_classified_lasfile, \ 39 | class_params, create_vrt, create_chm 40 | from l2d.pdal import classify, create_dems 41 | 42 | 43 | class PDALTest(unittest.TestCase): 44 | """ Test PDAL functions """ 45 | 46 | lasdir = os.path.join(os.path.dirname(__file__), 'las') 47 | testdir = os.path.join(os.path.dirname(__file__), 'testdir') 48 | vfilename = os.path.join(os.path.dirname(__file__), 'vectors/features.shp') 49 | 50 | def setUp(self): 51 | """ Set up test environment """ 52 | if not os.path.exists(self.testdir): 53 | os.makedirs(self.testdir) 54 | self.features = gippy.GeoVector(self.vfilename) 55 | 56 | def tearDown(self): 57 | """ Clean up test environment """ 58 | # shutil.rmtree(self.testdir) 59 | pass 60 | 61 | def test0_classify(self): 62 | """ Test classification """ 63 | fnames = find_lasfiles(self.lasdir) 64 | self.assertTrue(len(fnames) == 4) 65 | for f in self.features: 66 | fout = get_classification_filename(f, self.testdir) 67 | slope, cellsize = class_params(f) 68 | classify(fnames, fout, site=f, slope=slope, cellsize=cellsize) 69 | fouts = find_classified_lasfile(self.testdir, site=f, params=(slope, cellsize)) 70 | self.assertTrue(len(fouts) == 1) 71 | 72 | def test1_create_density(self): 73 | """ Test creating density """ 74 | fouts = [] 75 | for f in self.features: 76 | lasfiles = find_lasfiles(self.lasdir, site=f, checkoverlap=True) 77 | fout = create_dems(lasfiles, 'density', site=f, outdir=self.testdir) 78 | fouts.append(fout['den']) 79 | 80 | [self.assertTrue(os.path.exists(f)) for f in fouts] 81 | 82 | # create VRT 83 | fout = os.path.join(self.testdir, 'density.vrt') 84 | create_vrt(fouts, fout, site=self.features) 85 | 86 | def test2_create_dtm(self): 87 | """ Create DTM """ 88 | pieces = [] 89 | for f in self.features: 90 | lasfiles = find_classified_lasfile(self.testdir, site=f, params=class_params(f)) 91 | pouts = create_dems(lasfiles, 'dtm', site=f, gapfill=True, 92 | radius=['0.56', '1.41', '2.50'], outdir=self.testdir) 93 | [self.assertTrue(os.path.exists(fout) for fout in pouts.items())] 94 | pieces.append(pouts) 95 | 96 | for product in pouts.keys(): 97 | # there will be mult if gapfill False and multiple radii....use 1st one 98 | fnames = [piece[product] for piece in pieces] 99 | fout = os.path.join(self.testdir, 'dtm-%s.vrt' % product) 100 | create_vrt(fnames, fout, site=self.features) 101 | self.assertTrue(os.path.exists(fout)) 102 | 103 | def test3_create_dsm(self): 104 | """ Create DSM """ 105 | pieces = [] 106 | for f in self.features: 107 | lasfiles = find_classified_lasfile(self.testdir, site=f, params=class_params(f)) 108 | pouts = create_dems(lasfiles, 'dsm', site=f, gapfill=True, outdir=self.testdir) 109 | [self.assertTrue(os.path.exists(fout) for fout in pouts.items())] 110 | pieces.append(pouts) 111 | 112 | for product in pouts.keys(): 113 | # there will be mult if gapfill False and multiple radii....use 1st one 114 | fnames = [piece[product] for piece in pieces] 115 | fout = os.path.join(self.testdir, 'dsm-%s.vrt' % product) 116 | create_vrt(fnames, fout, site=self.features) 117 | self.assertTrue(os.path.exists(fout)) 118 | 119 | def test4_create_chm(self): 120 | """ Create CHM """ 121 | fouts = [] 122 | for f in self.features: 123 | prefix = os.path.join(self.testdir, f.Basename() + '_') 124 | fdtm = os.path.join(self.testdir, prefix + 'dtm.idw.tif') 125 | fdsm = os.path.join(self.testdir, prefix + 'dsm.max.tif') 126 | fout = create_chm(fdtm, fdsm, prefix + 'chm.tif') 127 | fouts.append(fout) 128 | self.assertTrue(os.path.exists(fout)) 129 | 130 | fout = create_vrt(fouts, os.path.join(self.testdir, 'chm.vrt'), site=self.features) 131 | self.assertTrue(os.path.exists(fout)) 132 | -------------------------------------------------------------------------------- /test/utils_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # lidar2dems - utilties for creating DEMs from LiDAR data 4 | # 5 | # AUTHOR: Matthew Hanson, matt.a.hanson@gmail.com 6 | # 7 | # Copyright (C) 2015 Applied Geosolutions LLC, oss@appliedgeosolutions.com 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | ################################################################################ 30 | 31 | import os 32 | import unittest 33 | import gippy 34 | from l2d.utils import get_classification_filename, find_lasfiles 35 | 36 | 37 | class UtilsTest(unittest.TestCase): 38 | 39 | vfilename = os.path.join(os.path.dirname(__file__), 'vectors/features.shp') 40 | tmpdir = os.path.join(os.path.dirname(__file__), 'tmp') 41 | lasdir = os.path.join(os.path.dirname(__file__), 'las') 42 | 43 | def setUp(self): 44 | """ Open vector file """ 45 | self.features = gippy.GeoVector(self.vfilename) 46 | 47 | def test_classification_filename(self): 48 | """ Test getting classification filename """ 49 | fname = get_classification_filename(None, outdir=self.tmpdir) 50 | self.assertEqual(os.path.dirname(fname), self.tmpdir) 51 | self.assertEqual(os.path.basename(fname)[0:4], 'l2d_') 52 | 53 | fname = get_classification_filename(self.features[0], outdir=self.tmpdir) 54 | self.assertEqual(os.path.dirname(fname), self.tmpdir) 55 | bname = os.path.splitext(os.path.basename(self.vfilename))[0] 56 | self.assertEqual(bname, bname[0:len(bname)]) 57 | 58 | def test_find_lasfiles(self): 59 | """ Test finding las files """ 60 | fnames = find_lasfiles(self.lasdir) 61 | self.assertTrue(len(fnames) == 4) 62 | 63 | def find_classified_lasfile(self): 64 | """ Test finding classified lasfile """ 65 | # should some classified las be stored in repo? 66 | pass 67 | -------------------------------------------------------------------------------- /test/vectors/features.dbf: -------------------------------------------------------------------------------- 1 | _aidN 2 | classN 3 | 0 1 1 2 2 3 -------------------------------------------------------------------------------- /test/vectors/features.prj: -------------------------------------------------------------------------------- 1 | PROJCS["WGS_1984_UTM_Zone_20S",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-63],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["Meter",1]] -------------------------------------------------------------------------------- /test/vectors/features.qpj: -------------------------------------------------------------------------------- 1 | PROJCS["WGS 84 / UTM zone 20S",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-63],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32720"]] 2 | -------------------------------------------------------------------------------- /test/vectors/features.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applied-GeoSolutions/lidar2dems/a6a356c5caf38e6485cf130b2c059e6af38d109f/test/vectors/features.shp -------------------------------------------------------------------------------- /test/vectors/features.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applied-GeoSolutions/lidar2dems/a6a356c5caf38e6485cf130b2c059e6af38d109f/test/vectors/features.shx -------------------------------------------------------------------------------- /test/vectors/tiles.dbf: -------------------------------------------------------------------------------- 1 | _aWidN 2 | classN 3 | 0********** 1********** 2********** 3********** -------------------------------------------------------------------------------- /test/vectors/tiles.prj: -------------------------------------------------------------------------------- 1 | PROJCS["WGS_1984_UTM_Zone_20S",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-63],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["Meter",1]] -------------------------------------------------------------------------------- /test/vectors/tiles.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applied-GeoSolutions/lidar2dems/a6a356c5caf38e6485cf130b2c059e6af38d109f/test/vectors/tiles.shp -------------------------------------------------------------------------------- /test/vectors/tiles.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applied-GeoSolutions/lidar2dems/a6a356c5caf38e6485cf130b2c059e6af38d109f/test/vectors/tiles.shx --------------------------------------------------------------------------------