From 233dd33cb21a004db6dfd28f40b6382f653291e8 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Thu, 7 Mar 2019 13:08:38 +0100 Subject: [PATCH 01/24] -d now colors 16 divided lasses --- .gitignore | 5 +- point-cloud-colorize/las_colorize.py | 605 +++++++++++++--------- point-cloud-colorize/las_colorize.py.save | 359 +++++++++++++ point-cloud-colorize/pdal_colorize.py | 410 +++++++-------- tst2cesium.sh | 11 + tstpipeline.sh | 14 + 6 files changed, 948 insertions(+), 456 deletions(-) create mode 100644 point-cloud-colorize/las_colorize.py.save create mode 100644 tst2cesium.sh create mode 100644 tstpipeline.sh diff --git a/.gitignore b/.gitignore index 3583243..9b354ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .vscode/ .spyproject/ -debug/ \ No newline at end of file +debug/ +*.laz +*.las +.*sh diff --git a/point-cloud-colorize/las_colorize.py b/point-cloud-colorize/las_colorize.py index be43a55..d3cf733 100644 --- a/point-cloud-colorize/las_colorize.py +++ b/point-cloud-colorize/las_colorize.py @@ -1,246 +1,359 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -import argparse -import json -from pathlib import Path - -import pdal - - -PDAL_PIPELINE = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.python", - "script": "{directory}/pdal_colorize.py", - "function": "las_colorize", - "module": "anything", - "pdalargs": "{pdalargs}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_file}" - }} - ] -}}""" - - -def run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size): - """ - Run the pdal pipeline using the given arguments. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - """ - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - - path = Path(__file__) - pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), - output_file=output_path.as_posix(), - srs=las_srs, - pdalargs=pdalargs_str, - directory=path.parent.as_posix()) - pipeline = pdal.Pipeline(pipeline_json) - pipeline.validate() - pipeline.execute() - - -def process_files(input_path, output_path, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size, verbose=False): - """ - Run the pdal pipeline for the input files. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - input_path = Path(input_path) - output_path = Path(output_path) - - if input_path.is_dir(): - for f in input_path.iterdir(): - if f.suffix == '.las' or f.suffix == '.laz': - - if output_path.is_dir(): - out = output_path / '{}_color{}'.format(f.stem, f.suffix) - else: - raise ValueError('Output path should be a directory if ' - 'the input path is a directory.') - - if verbose: - print('Colorizing {} ..'.format(f)) - print('Saving at {}.'.format(out)) - - run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - else: - if verbose: - print('Colorizing {} ..'.format(input_path)) - - if output_path.suffix == '.las' or output_path.suffix == '.laz': - if verbose: - print('Saving at {}.'.format(output_path)) - - run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - elif output_path.is_dir(): - out = output_path / '{}_color{}'.format(input_path.stem, - input_path.suffix) - - if verbose: - print('Saving at {}.'.format(out)) - - run_pdal(input_path, out, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - else: - raise ValueError('Specified output path not a LAS/LAZ file or ' - 'existing directory.') - - -def argument_parser(): - """ - Define and return the arguments. - """ - description = ('Colorize a las or laz file with a WMS service. ' - 'By default uses PDOK aerial photography.') - parser = argparse.ArgumentParser(description=description) - required_named = parser.add_argument_group('required named arguments') - required_named.add_argument('-i', '--input', - help='The input LAS/LAZ file or folder.', - required=True) - required_named.add_argument('-o', '--output', - help=('The output colorized LAS/LAZ ' - 'file or folder.'), - required=True) - parser.add_argument('-s', '--las_srs', - help=('The spatial reference system of the LAS data. ' - '(str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-w', '--wms_url', - help=('The url of the WMS service to use. ' - '(str, default: https://geodata. ' - 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), - required=False, - default=('https://geodata.nationaalgeoregister.nl' - '/luchtfoto/rgb/wms?')) - parser.add_argument('-l', '--wms_layer', - help=('The layer of the WMS service to use. ' - '(str, default: Actueel_ortho25)'), - required=False, - default='Actueel_ortho25') - parser.add_argument('-r', '--wms_srs', - help=('The spatial reference system of the WMS data ' - 'to request. (str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-f', '--wms_format', - help=('The image format of the WMS data to request. ' - '(str, default: image/png)'), - required=False, - default='image/png') - parser.add_argument('-v', '--wms_version', - help=('The version number of the WMS service. ' - '(str, default: 1.3.0)'), - required=False, - default='1.3.0') - parser.add_argument('-p', '--wms_pixel_size', - help=('The approximate desired pixel size of the ' - 'requested image. (float, default: 0.25)'), - required=False, - default=0.25) - parser.add_argument('-m', '--wms_max_image_size', - help=('The maximum size (in pixels) of the largest ' - 'side of the requested image. ' - '(int, default: 1000)'), - required=False, - default=1000) - parser.add_argument('-V', '--verbose', default=False, action="store_true", - help='Set verbose.') - args = parser.parse_args() - return args - - -def main(): - """ - Run the application. - """ - args = argument_parser() - process_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - - -if __name__ == '__main__': - main() +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas +""" + +import argparse +import json +from pathlib import Path +import shutil +import pdal +import datetime + + +PDAL_PIPELINE = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.python", + "script": "{directory}/pdal_colorize.py", + "function": "las_colorize", + "module": "anything", + "pdalargs": "{pdalargs}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_file}" + }} + ] +}}""" + + +def run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size): + """ + Run the pdal pipeline using the given arguments. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + """ + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') + path = Path(__file__) + pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), + output_file=output_path.as_posix(), + srs=las_srs, + pdalargs=pdalargs_str, + directory=path.parent.as_posix()) + pipeline = pdal.Pipeline(pipeline_json) + pipeline.validate() + pipeline.execute() + + +def process_files_parallel(input, output, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, + wms_pixel_size, wms_max_image_size, + verbose): + """ + :param input_path: + :param output_path: + :param las_srs: + :param verbose: + :return: + """ + print('colorizing parts') + print(datetime.datetime.now().isoformat()) + output_path = Path(output) + input_path = Path(input) + tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) + tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) + + for p in [tmp_div_path, tmp_col_path]: + if p.exists(): + shutil.rmtree(p) + p.mkdir(parents=True, exist_ok=True) + + divide_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.divider", + "count": "16" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_path}/tmp_#.laz" + }} + ]}}""" + + + merge_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type":"filters.merge" + }}, + {{ + "type":"writers.las", + "a_srs":"{srs}", + "filename":"{output_path}" + }} + ]}}""" + + + # create pipeline for dividing the las + + div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print('las is divided') + print(datetime.datetime.now().isoformat()) + + # for each of the created las-parts [PARRALEL] + for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): #todo: parrelelize + + # color the las just as the normal las would be colored + tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + + run_pdal(Path(f), tmp_col, + las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') + # merge all the colored lasses + merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), + srs=las_srs.replace('"', '\\"'), + output_path=output_path.as_posix()) + pipeline = pdal.Pipeline(merge_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print(datetime.datetime.now().isoformat()) + print('files merged') + print('process finished') + + +def process_files(input_path, output_path, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size, verbose=False): + """ + Run the pdal pipeline for the input files. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + input_path = Path(input_path) + output_path = Path(output_path) + + if input_path.is_dir(): + for f in input_path.iterdir(): + if f.suffix == '.las' or f.suffix == '.laz': + + if output_path.is_dir(): + out = output_path / '{}_color{}'.format(f.stem, f.suffix) + else: + raise ValueError('Output path should be a directory if ' + 'the input path is a directory.') + + if verbose: + print('Colorizing {} ..'.format(f)) + print('Saving at {}.'.format(out)) + + run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + else: + if verbose: + print('Colorizing {} ..'.format(input_path)) + + if output_path.suffix == '.las' or output_path.suffix == '.laz': + if verbose: + print('Saving at {}.'.format(output_path)) + + run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + elif output_path.is_dir(): + out = output_path / '{}_color{}'.format(input_path.stem, + input_path.suffix) + + if verbose: + print('Saving at {}.'.format(out)) + + run_pdal(input_path, out, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + else: + raise ValueError('Specified output path not a LAS/LAZ file or ' + 'existing directory.') + + +def argument_parser(): + """ + Define and return the arguments. + """ + description = ('Colorize a las or laz file with a WMS service. ' + 'By default uses PDOK aerial photography.') + parser = argparse.ArgumentParser(description=description) + required_named = parser.add_argument_group('required named arguments') + required_named.add_argument('-i', '--input', + help='The input LAS/LAZ file or folder.', + required=True) + required_named.add_argument('-o', '--output', + help=('The output colorized LAS/LAZ ' + 'file or folder.'), + required=True) + parser.add_argument('-s', '--las_srs', + help=('The spatial reference system of the LAS data. ' + '(str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-w', '--wms_url', + help=('The url of the WMS service to use. ' + '(str, default: https://geodata. ' + 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), + required=False, + default=('https://geodata.nationaalgeoregister.nl' + '/luchtfoto/rgb/wms?')) + parser.add_argument('-l', '--wms_layer', + help=('The layer of the WMS service to use. ' + '(str, default: Actueel_ortho25)'), + required=False, + default='Actueel_ortho25') + parser.add_argument('-r', '--wms_srs', + help=('The spatial reference system of the WMS data ' + 'to request. (str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-f', '--wms_format', + help=('The image format of the WMS data to request. ' + '(str, default: image/png)'), + required=False, + default='image/png') + parser.add_argument('-v', '--wms_version', + help=('The version number of the WMS service. ' + '(str, default: 1.3.0)'), + required=False, + default='1.3.0') + parser.add_argument('-p', '--wms_pixel_size', + help=('The approximate desired pixel size of the ' + 'requested image. (float, default: 0.25)'), + required=False, + default=0.25) + parser.add_argument('-m', '--wms_max_image_size', + help=('The maximum size (in pixels) of the largest ' + 'side of the requested image. ' + '(int, default: 1000)'), + required=False, + default=1000) + parser.add_argument('-d', '--divide', + default=5, + action = "store_true", + help='Divide the point cloud in a given number of ' + 'smaller areas which are colored seperately') + parser.add_argument('-V', '--verbose', + default=False, + action="store_true", + help='Set verbose.') + args = parser.parse_args() + return args + + +def main(): + """ + Run the application. + """ + args = argument_parser() + if args.divide: + process_files_parallel(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + else: + process_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + + +if __name__ == '__main__': + main() diff --git a/point-cloud-colorize/las_colorize.py.save b/point-cloud-colorize/las_colorize.py.save new file mode 100644 index 0000000..98762c6 --- /dev/null +++ b/point-cloud-colorize/las_colorize.py.save @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas +""" + +import argparse +import json +from pathlib import Path +import shutil +import pdal +import datetime + + +PDAL_PIPELINE = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.python", + "script": "{directory}/pdal_colorize.py", + "function": "las_colorize", + "module": "anything", + "pdalargs": "{pdalargs}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_file}" + }} + ] +}}""" + + +def run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size, n_div): + """ + Run the pdal pipeline using the given arguments. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + """ + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') + path = Path(__file__) + pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), + output_file=output_path.as_posix(), + srs=las_srs, + pdalargs=pdalargs_str, + directory=path.parent.as_posix()) + pipeline = pdal.Pipeline(pipeline_json) + pipeline.validate() + pipeline.execute() + + +def process_files_parallel(input, output, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, + wms_pixel_size, wms_max_image_size, + verbose): + """ + :param input_path: + :param output_path: + :param las_srs: + :param verbose: + :return: + """ + print('colorizing parts') + print(datetime.datetime.now().isoformat()) + output_path = Path(output) + input_path = Path(input) + tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) + tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) + + for p in [tmp_div_path, tmp_col_path]: + if p.exists(): + shutil.rmtree(p) + p.mkdir(parents=True, exist_ok=True) + + divide_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.divider", + "count": "{n_div}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_path}/tmp_#.laz" + }} + ]}}""" + + + merge_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type":"filters.merge" + }}, + {{ + "type":"writers.las", + "a_srs":"{srs}", + "filename":"{output_path}" + }} + ]}}""" + + + # create pipeline for dividing the las + + div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print('las is divided') + print(datetime.datetime.now().isoformat()) + + # for each of the created las-parts [PARRALEL] + for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): #todo: parrelelize + + # color the las just as the normal las would be colored + tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + + run_pdal(Path(f), tmp_col, + las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') + # merge all the colored lasses + merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), + srs=las_srs.replace('"', '\\"'), + output_path=output_path.as_posix()) + pipeline = pdal.Pipeline(merge_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print(datetime.datetime.now().isoformat()) + print('files merged') + print('process finished') + + +def process_files(input_path, output_path, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size, verbose=False): + """ + Run the pdal pipeline for the input files. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + input_path = Path(input_path) + output_path = Path(output_path) + + if input_path.is_dir(): + for f in input_path.iterdir(): + if f.suffix == '.las' or f.suffix == '.laz': + + if output_path.is_dir(): + out = output_path / '{}_color{}'.format(f.stem, f.suffix) + else: + raise ValueError('Output path should be a directory if ' + 'the input path is a directory.') + + if verbose: + print('Colorizing {} ..'.format(f)) + print('Saving at {}.'.format(out)) + + run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + else: + if verbose: + print('Colorizing {} ..'.format(input_path)) + + if output_path.suffix == '.las' or output_path.suffix == '.laz': + if verbose: + print('Saving at {}.'.format(output_path)) + + run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + elif output_path.is_dir(): + out = output_path / '{}_color{}'.format(input_path.stem, + input_path.suffix) + + if verbose: + print('Saving at {}.'.format(out)) + + run_pdal(input_path, out, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + else: + raise ValueError('Specified output path not a LAS/LAZ file or ' + 'existing directory.') + + +def argument_parser(): + """ + Define and return the arguments. + """ + description = ('Colorize a las or laz file with a WMS service. ' + 'By default uses PDOK aerial photography.') + parser = argparse.ArgumentParser(description=description) + required_named = parser.add_argument_group('required named arguments') + required_named.add_argument('-i', '--input', + help='The input LAS/LAZ file or folder.', + required=True) + required_named.add_argument('-o', '--output', + help=('The output colorized LAS/LAZ ' + 'file or folder.'), + required=True) + parser.add_argument('-s', '--las_srs', + help=('The spatial reference system of the LAS data. ' + '(str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-w', '--wms_url', + help=('The url of the WMS service to use. ' + '(str, default: https://geodata. ' + 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), + required=False, + default=('https://geodata.nationaalgeoregister.nl' + '/luchtfoto/rgb/wms?')) + parser.add_argument('-l', '--wms_layer', + help=('The layer of the WMS service to use. ' + '(str, default: Actueel_ortho25)'), + required=False, + default='Actueel_ortho25') + parser.add_argument('-r', '--wms_srs', + help=('The spatial reference system of the WMS data ' + 'to request. (str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-f', '--wms_format', + help=('The image format of the WMS data to request. ' + '(str, default: image/png)'), + required=False, + default='image/png') + parser.add_argument('-v', '--wms_version', + help=('The version number of the WMS service. ' + '(str, default: 1.3.0)'), + required=False, + default='1.3.0') + parser.add_argument('-p', '--wms_pixel_size', + help=('The approximate desired pixel size of the ' + 'requested image. (float, default: 0.25)'), + required=False, + default=0.25) + parser.add_argument('-m', '--wms_max_image_size', + help=('The maximum size (in pixels) of the largest ' + 'side of the requested image. ' + '(int, default: 1000)'), + required=False, + default=1000) + parser.add_argument('-d', '--divide', + action="store_true", + default=False, + help='Divide the point cloud in smaller areas' + 'which are colored in parallel') + parser.add_argument('-V', '--verbose', + default=False, + action="store_true", + help='Set verbose.') + args = parser.parse_args() + return args + + +def main(): + """ + Run the application. + """ + args = argument_parser() + if args.divide: + process_files_parallel(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + else: + process_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + + +if __name__ == '__main__': + main() diff --git a/point-cloud-colorize/pdal_colorize.py b/point-cloud-colorize/pdal_colorize.py index deea80e..60a9d60 100644 --- a/point-cloud-colorize/pdal_colorize.py +++ b/point-cloud-colorize/pdal_colorize.py @@ -1,209 +1,201 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -from io import BytesIO -import math -import numpy as np -import matplotlib.image as mpimg -import pyproj -from owslib.wms import WebMapService -from requests.exceptions import ReadTimeout - - -def request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries): - """ - Request an image from a WMS. - - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - size : list of int - The size of the image to be requested in pixels. [x, y] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - for i in range(retries): - try: - wms = WebMapService(wms_url, version=wms_version) - wms_img = wms.getmap(layers=[wms_layer], - srs=wms_srs, - bbox=bbox, - size=size, - format=wms_format, - transparent=True) - break - except ReadTimeout as e: - if i != retries-1: - print("ReadTimeout, trying again..") - else: - raise e - - img = mpimg.imread(BytesIO(wms_img.read()), 0) - - return img - - -def image_size(bbox, pixel_size=0.25): - """ - Compute the size of the image to be requested in pixels based on the - bounding box and the pixel size. - - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - pixel_size : float - The desired pixel size of the requested image. - - Returns - ------- - img_size : tuple of int - The size of the image to be requested in pixels. (x, y) - """ - dif_x = bbox[2] - bbox[0] - dif_y = bbox[3] - bbox[1] - aspect_ratio = dif_x / dif_y - resolution = int(dif_x * (1/pixel_size)) - img_size = (resolution, int(resolution / aspect_ratio)) - - return img_size - - -def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, - wms_format, pixel_size, max_image_size, retries=10): - """ - Retrieve the imagery from a WMS service for the given bounding box. - - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - pixel_size : float - The desired pixel size of the requested image. - max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - [xmin, ymin, xmax, ymax] = bbox - - x_range = xmax - xmin - y_range = ymax - ymin - longest_side = max([x_range, y_range]) - - if (longest_side * (1/pixel_size) > max_image_size): - length = max_image_size / (1/pixel_size) - length_pixels = max_image_size - - rows = int(math.ceil((ymax-ymin)/length)) - cols = int(math.ceil((xmax-xmin)/length)) - - img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) - - for col in range(cols): - for row in range(rows): - cell = [xmin+col*length, ymin+row*length, - xmin+(col+1)*length, ymin+(row+1)*length] - - img_part = request_image(cell, (length_pixels, length_pixels), - wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - img[((length_pixels*rows)-(row+1) * - length_pixels):(length_pixels*rows)-row*length_pixels, - col*length_pixels:(col+1)*length_pixels] = img_part - - img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, - :int(round(x_range*(1/pixel_size)))] - else: - size = image_size(bbox) - img = request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - return img - - -def las_colorize(ins, outs): - """ - PDAL python function. Adds RGB information to a LAS file by downloading - an orthophoto from a WMS service. - - Parameters - ---------- - ins : PDAL input - outs : PDAL output - """ - X = ins['X'] - Y = ins['Y'] - - [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] - - if pdalargs['las_srs'] != pdalargs['wms_srs']: - p1 = pyproj.Proj(init=pdalargs['las_srs']) - p2 = pyproj.Proj(init=pdalargs['wms_srs']) - - bbox = [0, 0, 0, 0] - bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) - bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) - else: - bbox = [xmin, ymin, xmax, ymax] - - img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], - pdalargs['wms_srs'], pdalargs['wms_version'], - pdalargs['wms_format'], - float(pdalargs['wms_pixel_size']), - int(pdalargs['wms_max_image_size'])) - - img_size = img.shape[:2] - - x_img = np.round(((X - xmin) / (xmax-xmin)) * - (img_size[1]-1)).astype(int) - y_img = np.round(((ymax - Y) / (ymax-ymin)) * - (img_size[0]-1)).astype(int) - - rgb = img[y_img, x_img] * 255 - - outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) - outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) - outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) - - return True +# -*- coding: utf-8 -*- +""" +Python3 +@author: Chris Lucas +""" + +from io import BytesIO +import math +import numpy as np +import matplotlib.image as mpimg +import pyproj +from owslib.wms import WebMapService +from requests.exceptions import ReadTimeout + + +def request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries): + """ + Request an image from a WMS. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + size : list of int + The size of the image to be requested in pixels. [x, y] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + for i in range(retries): + try: + wms = WebMapService(wms_url, version=wms_version) + wms_img = wms.getmap(layers=[wms_layer], + srs=wms_srs, + bbox=bbox, + size=size, + format=wms_format, + transparent=True) + break + except ReadTimeout as e: + if i != retries-1: + print("ReadTimeout, trying again..") + else: + raise e + + img = mpimg.imread(BytesIO(wms_img.read()), 0) + + return img + + +def image_size(bbox, pixel_size=0.25): + """ + Compute the size of the image to be requested in pixels based on the + bounding box and the pixel size. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + pixel_size : float + The desired pixel size of the requested image. + Returns + ------- + img_size : tuple of int + The size of the image to be requested in pixels. (x, y) + """ + dif_x = bbox[2] - bbox[0] + dif_y = bbox[3] - bbox[1] + aspect_ratio = dif_x / dif_y + resolution = int(dif_x * (1/pixel_size)) + img_size = (resolution, int(resolution / aspect_ratio)) + + return img_size + + +def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, + wms_format, pixel_size, max_image_size, retries=10): + """ + Retrieve the imagery from a WMS service for the given bounding box. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + pixel_size : float + The desired pixel size of the requested image. + max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + [xmin, ymin, xmax, ymax] = bbox + + x_range = xmax - xmin + y_range = ymax - ymin + longest_side = max([x_range, y_range]) + + if (longest_side * (1/pixel_size) > max_image_size): + length = max_image_size / (1/pixel_size) + length_pixels = max_image_size + + rows = int(math.ceil((ymax-ymin)/length)) + cols = int(math.ceil((xmax-xmin)/length)) + + img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) + + for col in range(cols): + for row in range(rows): + cell = [xmin+col*length, ymin+row*length, + xmin+(col+1)*length, ymin+(row+1)*length] + + img_part = request_image(cell, (length_pixels, length_pixels), + wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + img[((length_pixels*rows)-(row+1) * + length_pixels):(length_pixels*rows)-row*length_pixels, + col*length_pixels:(col+1)*length_pixels] = img_part + + img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, + :int(round(x_range*(1/pixel_size)))] + else: + size = image_size(bbox) + img = request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + return img + + +def las_colorize(ins, outs): + """ + PDAL python function. Adds RGB information to a LAS file by downloading + an orthophoto from a WMS service. + Parameters + ---------- + ins : PDAL input + outs : PDAL output + """ + X = ins['X'] + Y = ins['Y'] + + [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] + + if pdalargs['las_srs'] != pdalargs['wms_srs']: + p1 = pyproj.Proj(init=pdalargs['las_srs']) + p2 = pyproj.Proj(init=pdalargs['wms_srs']) + + bbox = [0, 0, 0, 0] + bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) + bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) + else: + bbox = [xmin, ymin, xmax, ymax] + + img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], + pdalargs['wms_srs'], pdalargs['wms_version'], + pdalargs['wms_format'], + float(pdalargs['wms_pixel_size']), + int(pdalargs['wms_max_image_size'])) + + img_size = img.shape[:2] + + x_img = np.round(((X - xmin) / (xmax-xmin)) * + (img_size[1]-1)).astype(int) + y_img = np.round(((ymax - Y) / (ymax-ymin)) * + (img_size[0]-1)).astype(int) + + rgb = img[y_img, x_img] * 255 + + outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) + outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) + outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) + + return True \ No newline at end of file diff --git a/tst2cesium.sh b/tst2cesium.sh new file mode 100644 index 0000000..0a40578 --- /dev/null +++ b/tst2cesium.sh @@ -0,0 +1,11 @@ +mkdir /var/data/arnot/div_color/ +python3 point-cloud-colorize/las_colorize.py -i /var/data/arnot/AHN/baarsjes.laz -o /var/data/arnot/div_color/c_10cn2_col.laz -V -d + +mkdir /var/data/arnot/div_color/build/ +mkdir /var/data/arnot/div_color/cesium/ +echo 'colored'; +entwine build -i /var/data/arnot/div_color/c_10cn2_col.laz -o /var/data/arnot/div_color/build/ -r EPSG:28992 EPSG:4978; +echo 'build'; +entwine convert -i /var/data/arnot/div_color/build/ -o /var/data/arnot/div_color/cesium/; +echo 'converted to cesium'; + diff --git a/tstpipeline.sh b/tstpipeline.sh new file mode 100644 index 0000000..684ceb3 --- /dev/null +++ b/tstpipeline.sh @@ -0,0 +1,14 @@ +{ + "pipeline":[ + {"type": "readers.las", + "filename":"/var/data/arnot/c_10cn2_color.laz", + "spatialreference":"EPSG:28992"}, + {"type":"filters.python", + "script":"normalize.py", + "function":"normalize"}, + {"type":"writers.las", + "filename":"/var/data/arnot/AHN/filtered_laz.laz"} +] +} + + From 87415fa91388196cb256d01349c5fa9bc4d7f740 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Thu, 7 Mar 2019 13:34:24 +0100 Subject: [PATCH 02/24] removed testfiles --- tst2cesium.sh | 11 ----------- tstpipeline.sh | 14 -------------- 2 files changed, 25 deletions(-) delete mode 100644 tst2cesium.sh delete mode 100644 tstpipeline.sh diff --git a/tst2cesium.sh b/tst2cesium.sh deleted file mode 100644 index 0a40578..0000000 --- a/tst2cesium.sh +++ /dev/null @@ -1,11 +0,0 @@ -mkdir /var/data/arnot/div_color/ -python3 point-cloud-colorize/las_colorize.py -i /var/data/arnot/AHN/baarsjes.laz -o /var/data/arnot/div_color/c_10cn2_col.laz -V -d - -mkdir /var/data/arnot/div_color/build/ -mkdir /var/data/arnot/div_color/cesium/ -echo 'colored'; -entwine build -i /var/data/arnot/div_color/c_10cn2_col.laz -o /var/data/arnot/div_color/build/ -r EPSG:28992 EPSG:4978; -echo 'build'; -entwine convert -i /var/data/arnot/div_color/build/ -o /var/data/arnot/div_color/cesium/; -echo 'converted to cesium'; - diff --git a/tstpipeline.sh b/tstpipeline.sh deleted file mode 100644 index 684ceb3..0000000 --- a/tstpipeline.sh +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pipeline":[ - {"type": "readers.las", - "filename":"/var/data/arnot/c_10cn2_color.laz", - "spatialreference":"EPSG:28992"}, - {"type":"filters.python", - "script":"normalize.py", - "function":"normalize"}, - {"type":"writers.las", - "filename":"/var/data/arnot/AHN/filtered_laz.laz"} -] -} - - From e3f1eba9f9ce76af8b92c9739b4a5664ab79f2f9 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Thu, 7 Mar 2019 13:37:05 +0100 Subject: [PATCH 03/24] remove test files --- .gitignore | 2 + point-cloud-colorize/las_colorize.py.save | 359 ---------------------- 2 files changed, 2 insertions(+), 359 deletions(-) delete mode 100644 point-cloud-colorize/las_colorize.py.save diff --git a/.gitignore b/.gitignore index 9b354ee..781a3b6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ debug/ *.laz *.las .*sh +.gitignore + diff --git a/point-cloud-colorize/las_colorize.py.save b/point-cloud-colorize/las_colorize.py.save deleted file mode 100644 index 98762c6..0000000 --- a/point-cloud-colorize/las_colorize.py.save +++ /dev/null @@ -1,359 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -import argparse -import json -from pathlib import Path -import shutil -import pdal -import datetime - - -PDAL_PIPELINE = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.python", - "script": "{directory}/pdal_colorize.py", - "function": "las_colorize", - "module": "anything", - "pdalargs": "{pdalargs}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_file}" - }} - ] -}}""" - - -def run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size, n_div): - """ - Run the pdal pipeline using the given arguments. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - """ - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - - pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - path = Path(__file__) - pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), - output_file=output_path.as_posix(), - srs=las_srs, - pdalargs=pdalargs_str, - directory=path.parent.as_posix()) - pipeline = pdal.Pipeline(pipeline_json) - pipeline.validate() - pipeline.execute() - - -def process_files_parallel(input, output, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, - wms_pixel_size, wms_max_image_size, - verbose): - """ - :param input_path: - :param output_path: - :param las_srs: - :param verbose: - :return: - """ - print('colorizing parts') - print(datetime.datetime.now().isoformat()) - output_path = Path(output) - input_path = Path(input) - tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) - tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) - - for p in [tmp_div_path, tmp_col_path]: - if p.exists(): - shutil.rmtree(p) - p.mkdir(parents=True, exist_ok=True) - - divide_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.divider", - "count": "{n_div}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_path}/tmp_#.laz" - }} - ]}}""" - - - merge_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type":"filters.merge" - }}, - {{ - "type":"writers.las", - "a_srs":"{srs}", - "filename":"{output_path}" - }} - ]}}""" - - - # create pipeline for dividing the las - - div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), - srs=las_srs, - output_path=tmp_div_path.as_posix()) - - pipeline = pdal.Pipeline(div_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print('las is divided') - print(datetime.datetime.now().isoformat()) - - # for each of the created las-parts [PARRALEL] - for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): #todo: parrelelize - - # color the las just as the normal las would be colored - tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) - - run_pdal(Path(f), tmp_col, - las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') - # merge all the colored lasses - merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), - srs=las_srs.replace('"', '\\"'), - output_path=output_path.as_posix()) - pipeline = pdal.Pipeline(merge_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print(datetime.datetime.now().isoformat()) - print('files merged') - print('process finished') - - -def process_files(input_path, output_path, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size, verbose=False): - """ - Run the pdal pipeline for the input files. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - input_path = Path(input_path) - output_path = Path(output_path) - - if input_path.is_dir(): - for f in input_path.iterdir(): - if f.suffix == '.las' or f.suffix == '.laz': - - if output_path.is_dir(): - out = output_path / '{}_color{}'.format(f.stem, f.suffix) - else: - raise ValueError('Output path should be a directory if ' - 'the input path is a directory.') - - if verbose: - print('Colorizing {} ..'.format(f)) - print('Saving at {}.'.format(out)) - - run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - else: - if verbose: - print('Colorizing {} ..'.format(input_path)) - - if output_path.suffix == '.las' or output_path.suffix == '.laz': - if verbose: - print('Saving at {}.'.format(output_path)) - - run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - elif output_path.is_dir(): - out = output_path / '{}_color{}'.format(input_path.stem, - input_path.suffix) - - if verbose: - print('Saving at {}.'.format(out)) - - run_pdal(input_path, out, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - else: - raise ValueError('Specified output path not a LAS/LAZ file or ' - 'existing directory.') - - -def argument_parser(): - """ - Define and return the arguments. - """ - description = ('Colorize a las or laz file with a WMS service. ' - 'By default uses PDOK aerial photography.') - parser = argparse.ArgumentParser(description=description) - required_named = parser.add_argument_group('required named arguments') - required_named.add_argument('-i', '--input', - help='The input LAS/LAZ file or folder.', - required=True) - required_named.add_argument('-o', '--output', - help=('The output colorized LAS/LAZ ' - 'file or folder.'), - required=True) - parser.add_argument('-s', '--las_srs', - help=('The spatial reference system of the LAS data. ' - '(str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-w', '--wms_url', - help=('The url of the WMS service to use. ' - '(str, default: https://geodata. ' - 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), - required=False, - default=('https://geodata.nationaalgeoregister.nl' - '/luchtfoto/rgb/wms?')) - parser.add_argument('-l', '--wms_layer', - help=('The layer of the WMS service to use. ' - '(str, default: Actueel_ortho25)'), - required=False, - default='Actueel_ortho25') - parser.add_argument('-r', '--wms_srs', - help=('The spatial reference system of the WMS data ' - 'to request. (str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-f', '--wms_format', - help=('The image format of the WMS data to request. ' - '(str, default: image/png)'), - required=False, - default='image/png') - parser.add_argument('-v', '--wms_version', - help=('The version number of the WMS service. ' - '(str, default: 1.3.0)'), - required=False, - default='1.3.0') - parser.add_argument('-p', '--wms_pixel_size', - help=('The approximate desired pixel size of the ' - 'requested image. (float, default: 0.25)'), - required=False, - default=0.25) - parser.add_argument('-m', '--wms_max_image_size', - help=('The maximum size (in pixels) of the largest ' - 'side of the requested image. ' - '(int, default: 1000)'), - required=False, - default=1000) - parser.add_argument('-d', '--divide', - action="store_true", - default=False, - help='Divide the point cloud in smaller areas' - 'which are colored in parallel') - parser.add_argument('-V', '--verbose', - default=False, - action="store_true", - help='Set verbose.') - args = parser.parse_args() - return args - - -def main(): - """ - Run the application. - """ - args = argument_parser() - if args.divide: - process_files_parallel(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - else: - process_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - - -if __name__ == '__main__': - main() From e5b4f3cd81a61cf023d051cc507cc9d71da1e2d5 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 08:53:56 +0100 Subject: [PATCH 04/24] parts are now colored in parallel --- .gitignore | 3 +- pipeline.json | 9 + point-cloud-colorize/las_colorize.py | 41 ++- point-cloud-colorize/pdal_colorize.py | 10 +- point-cloud-colorize_wip/las_colorize.py | 430 ++++++++++++++++++++++ point-cloud-colorize_wip/pdal_colorize.py | 201 ++++++++++ 6 files changed, 673 insertions(+), 21 deletions(-) create mode 100644 pipeline.json create mode 100644 point-cloud-colorize_wip/las_colorize.py create mode 100644 point-cloud-colorize_wip/pdal_colorize.py diff --git a/.gitignore b/.gitignore index 781a3b6..3ec54f8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ debug/ *.laz *.las -.*sh -.gitignore +*.sh diff --git a/pipeline.json b/pipeline.json new file mode 100644 index 0000000..0419877 --- /dev/null +++ b/pipeline.json @@ -0,0 +1,9 @@ +{ + "pipeline": [ + "/var/data/arnot/div_color/tmp_col/*.laz", + { + "type" : "filters.merge" + }, + "/var/data/arnot/div_color/c_10cn2_color.laz" + ] +} diff --git a/point-cloud-colorize/las_colorize.py b/point-cloud-colorize/las_colorize.py index d3cf733..399722b 100644 --- a/point-cloud-colorize/las_colorize.py +++ b/point-cloud-colorize/las_colorize.py @@ -11,6 +11,7 @@ import shutil import pdal import datetime +from joblib import Parallel, delayed PDAL_PIPELINE = """{{ @@ -86,6 +87,17 @@ def run_pdal(input_path, output_path, las_srs, wms_url, pipeline.validate() pipeline.execute() +def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col = 'tmp_col_{}.laz'): + tmp_col = Path(tmp_col_path.joinpath(tmp_col.format(i))) + run_pdal(Path(f), tmp_col, + pdalargs['las_srs'],pdalargs['wms_url'], + pdalargs['wms_layer'], pdalargs['wms_srs'], + pdalargs['wms_version'] , pdalargs['wms_format'], + pdalargs['wms_pixel_size'],pdalargs['wms_max_image_size']) + + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') def process_files_parallel(input, output, las_srs, wms_url, wms_layer, wms_srs, @@ -119,7 +131,7 @@ def process_files_parallel(input, output, las_srs, }}, {{ "type": "filters.divider", - "count": "16" + "count": "6" }}, {{ "type": "writers.las", @@ -147,7 +159,6 @@ def process_files_parallel(input, output, las_srs, # create pipeline for dividing the las - div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), srs=las_srs, output_path=tmp_div_path.as_posix()) @@ -159,19 +170,21 @@ def process_files_parallel(input, output, las_srs, print('las is divided') print(datetime.datetime.now().isoformat()) - # for each of the created las-parts [PARRALEL] - for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): #todo: parrelelize - - # color the las just as the normal las would be colored - tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} - run_pdal(Path(f), tmp_col, - las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') + # for each of the created las-parts + print('start parallel processing') + print(datetime.datetime.now().isoformat()) + Parallel(n_jobs=1)(delayed(parallel_coloring)(f, i, verbose, tmp_col_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) + print('finished parallel processing') + print(datetime.datetime.now().isoformat()) # merge all the colored lasses merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), srs=las_srs.replace('"', '\\"'), diff --git a/point-cloud-colorize/pdal_colorize.py b/point-cloud-colorize/pdal_colorize.py index 60a9d60..50ef352 100644 --- a/point-cloud-colorize/pdal_colorize.py +++ b/point-cloud-colorize/pdal_colorize.py @@ -194,8 +194,8 @@ def las_colorize(ins, outs): rgb = img[y_img, x_img] * 255 - outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) - outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) - outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) - - return True \ No newline at end of file + outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) >>8 + outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) >> 8 + outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) >> 8 + outs['Intensity'] = ins['Intensity'] >> 8 + return True diff --git a/point-cloud-colorize_wip/las_colorize.py b/point-cloud-colorize_wip/las_colorize.py new file mode 100644 index 0000000..48d1dd9 --- /dev/null +++ b/point-cloud-colorize_wip/las_colorize.py @@ -0,0 +1,430 @@ +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas +""" + +import argparse +import json +from pathlib import Path +import shutil +import pdal +import datetime + + +PDAL_PIPELINE = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.python", + "script": "{directory}/pdal_colorize.py", + "function": "las_colorize", + "module": "anything", + "pdalargs": "{pdalargs}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_file}" + }} + ] +}}""" + + +def run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size): + """ + Run the pdal pipeline using the given arguments. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + """ + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') + path = Path(__file__) + pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), + output_file=output_path.as_posix(), + srs=las_srs, + pdalargs=pdalargs_str, + directory=path.parent.as_posix()) + pipeline = pdal.Pipeline(pipeline_json) + pipeline.validate() + pipeline.execute() + + +def process_divided_files(input, output, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, + wms_pixel_size, wms_max_image_size, + verbose): + """ + Runs 3 pipelines, + dividing the pc in 16 smaller pcs, + loops over them to color each one seperately, + merges them together. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + + # todo: make it handle directories + print('colorizing parts') + print(datetime.datetime.now().isoformat()) + output_path = Path(output) + input_path = Path(input) + tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) + tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) + + for p in [tmp_div_path, tmp_col_path]: + if p.exists(): + shutil.rmtree(p) + p.mkdir(parents=True, exist_ok=True) + + divide_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.divider", + "count": "16" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_path}/tmp_#.laz" + }} + ]}}""" + + merge_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type":"filters.merge" + }}, + {{ + "type":"writers.las", + "a_srs":"{srs}", + "filename":"{output_path}" + }} + ]}}""" + if input_path.is_dir() and output_path.is_dir(): + for in_file in input_path.iterdir(): + if in_file.suffix == '.las' or in_file.suffix == '.laz': + # create pipeline for dividing the las + div_pipeline_json = divide_pipeline.format(input_file=in_file.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print('las is divided') + print(datetime.datetime.now().isoformat()) + + # for each of the created las-parts + for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): + + # color the las just as the normal las would be colored + tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + + run_pdal(Path(f), tmp_col, + las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') + # merge all the colored lasses + merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), + srs=las_srs.replace('"', '\\"'), + output_path=output_path.as_posix()) + pipeline = pdal.Pipeline(merge_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print(datetime.datetime.now().isoformat()) + print('files merged') + print('process finished') + + else: + print(f'{in_file} is not a las or a laz file') + elif not output_path.is_dir() and not input_path.is_dir(): + # create pipeline for dividing the las + div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print('las is divided') + print(datetime.datetime.now().isoformat()) + + # for each of the created las-parts + for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): + + # color the las just as the normal las would be colored + tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + + run_pdal(Path(f), tmp_col, + las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') + # merge all the colored lasses + merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), + srs=las_srs.replace('"', '\\"'), + output_path=output_path.as_posix()) + pipeline = pdal.Pipeline(merge_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print(datetime.datetime.now().isoformat()) + print('files merged') + print('process finished') + else: + print('input and output are not both folders or both files.') + + # :todo remove temporary directories + + +def process_files(input_path, output_path, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size, verbose=False): + """ + Run the pdal pipeline for the input files. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + input_path = Path(input_path) + output_path = Path(output_path) + + if input_path.is_dir(): + for f in input_path.iterdir(): + if f.suffix == '.las' or f.suffix == '.laz': + + if output_path.is_dir(): + out = output_path / '{}_color{}'.format(f.stem, f.suffix) + else: + raise ValueError('Output path should be a directory if ' + 'the input path is a directory.') + + if verbose: + print('Colorizing {} ..'.format(f)) + print('Saving at {}.'.format(out)) + + run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + else: + if verbose: + print('Colorizing {} ..'.format(input_path)) + + if output_path.suffix == '.las' or output_path.suffix == '.laz': + if verbose: + print('Saving at {}.'.format(output_path)) + + run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + elif output_path.is_dir(): + out = output_path / '{}_color{}'.format(input_path.stem, + input_path.suffix) + + if verbose: + print('Saving at {}.'.format(out)) + + run_pdal(input_path, out, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + else: + raise ValueError('Specified output path not a LAS/LAZ file or ' + 'existing directory.') + + +def argument_parser(): + """ + Define and return the arguments. + """ + description = ('Colorize a las or laz file with a WMS service. ' + 'By default uses PDOK aerial photography.') + parser = argparse.ArgumentParser(description=description) + required_named = parser.add_argument_group('required named arguments') + required_named.add_argument('-i', '--input', + help='The input LAS/LAZ file or folder.', + required=True) + required_named.add_argument('-o', '--output', + help=('The output colorized LAS/LAZ ' + 'file or folder.'), + required=True) + parser.add_argument('-s', '--las_srs', + help=('The spatial reference system of the LAS data. ' + '(str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-w', '--wms_url', + help=('The url of the WMS service to use. ' + '(str, default: https://geodata. ' + 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), + required=False, + default=('https://geodata.nationaalgeoregister.nl' + '/luchtfoto/rgb/wms?')) + parser.add_argument('-l', '--wms_layer', + help=('The layer of the WMS service to use. ' + '(str, default: Actueel_ortho25)'), + required=False, + default='Actueel_ortho25') + parser.add_argument('-r', '--wms_srs', + help=('The spatial reference system of the WMS data ' + 'to request. (str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-f', '--wms_format', + help=('The image format of the WMS data to request. ' + '(str, default: image/png)'), + required=False, + default='image/png') + parser.add_argument('-v', '--wms_version', + help=('The version number of the WMS service. ' + '(str, default: 1.3.0)'), + required=False, + default='1.3.0') + parser.add_argument('-p', '--wms_pixel_size', + help=('The approximate desired pixel size of the ' + 'requested image. (float, default: 0.25)'), + required=False, + default=0.25) + parser.add_argument('-m', '--wms_max_image_size', + help=('The maximum size (in pixels) of the largest ' + 'side of the requested image. ' + '(int, default: 1000)'), + required=False, + default=1000) + parser.add_argument('-d', '--divide', + default=5, + action = "store_true", + help='Divide the point cloud in a given number of ' + 'smaller areas which are colored seperately') + parser.add_argument('-V', '--verbose', + default=False, + action="store_true", + help='Set verbose.') + args = parser.parse_args() + return args + + +def main(): + """ + Run the application. + """ + args = argument_parser() + if args.divide: + process_divided_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + else: + process_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + + +if __name__ == '__main__': + main() diff --git a/point-cloud-colorize_wip/pdal_colorize.py b/point-cloud-colorize_wip/pdal_colorize.py new file mode 100644 index 0000000..60a9d60 --- /dev/null +++ b/point-cloud-colorize_wip/pdal_colorize.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +""" +Python3 +@author: Chris Lucas +""" + +from io import BytesIO +import math +import numpy as np +import matplotlib.image as mpimg +import pyproj +from owslib.wms import WebMapService +from requests.exceptions import ReadTimeout + + +def request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries): + """ + Request an image from a WMS. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + size : list of int + The size of the image to be requested in pixels. [x, y] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + for i in range(retries): + try: + wms = WebMapService(wms_url, version=wms_version) + wms_img = wms.getmap(layers=[wms_layer], + srs=wms_srs, + bbox=bbox, + size=size, + format=wms_format, + transparent=True) + break + except ReadTimeout as e: + if i != retries-1: + print("ReadTimeout, trying again..") + else: + raise e + + img = mpimg.imread(BytesIO(wms_img.read()), 0) + + return img + + +def image_size(bbox, pixel_size=0.25): + """ + Compute the size of the image to be requested in pixels based on the + bounding box and the pixel size. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + pixel_size : float + The desired pixel size of the requested image. + Returns + ------- + img_size : tuple of int + The size of the image to be requested in pixels. (x, y) + """ + dif_x = bbox[2] - bbox[0] + dif_y = bbox[3] - bbox[1] + aspect_ratio = dif_x / dif_y + resolution = int(dif_x * (1/pixel_size)) + img_size = (resolution, int(resolution / aspect_ratio)) + + return img_size + + +def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, + wms_format, pixel_size, max_image_size, retries=10): + """ + Retrieve the imagery from a WMS service for the given bounding box. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + pixel_size : float + The desired pixel size of the requested image. + max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + [xmin, ymin, xmax, ymax] = bbox + + x_range = xmax - xmin + y_range = ymax - ymin + longest_side = max([x_range, y_range]) + + if (longest_side * (1/pixel_size) > max_image_size): + length = max_image_size / (1/pixel_size) + length_pixels = max_image_size + + rows = int(math.ceil((ymax-ymin)/length)) + cols = int(math.ceil((xmax-xmin)/length)) + + img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) + + for col in range(cols): + for row in range(rows): + cell = [xmin+col*length, ymin+row*length, + xmin+(col+1)*length, ymin+(row+1)*length] + + img_part = request_image(cell, (length_pixels, length_pixels), + wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + img[((length_pixels*rows)-(row+1) * + length_pixels):(length_pixels*rows)-row*length_pixels, + col*length_pixels:(col+1)*length_pixels] = img_part + + img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, + :int(round(x_range*(1/pixel_size)))] + else: + size = image_size(bbox) + img = request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + return img + + +def las_colorize(ins, outs): + """ + PDAL python function. Adds RGB information to a LAS file by downloading + an orthophoto from a WMS service. + Parameters + ---------- + ins : PDAL input + outs : PDAL output + """ + X = ins['X'] + Y = ins['Y'] + + [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] + + if pdalargs['las_srs'] != pdalargs['wms_srs']: + p1 = pyproj.Proj(init=pdalargs['las_srs']) + p2 = pyproj.Proj(init=pdalargs['wms_srs']) + + bbox = [0, 0, 0, 0] + bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) + bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) + else: + bbox = [xmin, ymin, xmax, ymax] + + img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], + pdalargs['wms_srs'], pdalargs['wms_version'], + pdalargs['wms_format'], + float(pdalargs['wms_pixel_size']), + int(pdalargs['wms_max_image_size'])) + + img_size = img.shape[:2] + + x_img = np.round(((X - xmin) / (xmax-xmin)) * + (img_size[1]-1)).astype(int) + y_img = np.round(((ymax - Y) / (ymax-ymin)) * + (img_size[0]-1)).astype(int) + + rgb = img[y_img, x_img] * 255 + + outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) + outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) + outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) + + return True \ No newline at end of file From 343730008ff500e75e7fa8d0db94385c70ffc008 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 08:54:39 +0100 Subject: [PATCH 05/24] parts are now colored in parallel --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3ec54f8..d114197 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ debug/ *.laz *.las *.sh - +/point-cloud-colorize/* From c471debaa6e730998997c171b154550ee8f648e0 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 08:55:16 +0100 Subject: [PATCH 06/24] parts are now colored in parallel --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d114197..bbdc2b0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ debug/ *.laz *.las *.sh -/point-cloud-colorize/* +point-cloud-colorize/* From 9b50e0dace608333179c7f710b87a6c55d9c1995 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 08:56:06 +0100 Subject: [PATCH 07/24] parts are now colored in parallel --- point-cloud-colorize_wip/las_colorize.py | 430 ---------------------- point-cloud-colorize_wip/pdal_colorize.py | 201 ---------- 2 files changed, 631 deletions(-) delete mode 100644 point-cloud-colorize_wip/las_colorize.py delete mode 100644 point-cloud-colorize_wip/pdal_colorize.py diff --git a/point-cloud-colorize_wip/las_colorize.py b/point-cloud-colorize_wip/las_colorize.py deleted file mode 100644 index 48d1dd9..0000000 --- a/point-cloud-colorize_wip/las_colorize.py +++ /dev/null @@ -1,430 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -import argparse -import json -from pathlib import Path -import shutil -import pdal -import datetime - - -PDAL_PIPELINE = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.python", - "script": "{directory}/pdal_colorize.py", - "function": "las_colorize", - "module": "anything", - "pdalargs": "{pdalargs}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_file}" - }} - ] -}}""" - - -def run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size): - """ - Run the pdal pipeline using the given arguments. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - """ - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - - pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - path = Path(__file__) - pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), - output_file=output_path.as_posix(), - srs=las_srs, - pdalargs=pdalargs_str, - directory=path.parent.as_posix()) - pipeline = pdal.Pipeline(pipeline_json) - pipeline.validate() - pipeline.execute() - - -def process_divided_files(input, output, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, - wms_pixel_size, wms_max_image_size, - verbose): - """ - Runs 3 pipelines, - dividing the pc in 16 smaller pcs, - loops over them to color each one seperately, - merges them together. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - - # todo: make it handle directories - print('colorizing parts') - print(datetime.datetime.now().isoformat()) - output_path = Path(output) - input_path = Path(input) - tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) - tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) - - for p in [tmp_div_path, tmp_col_path]: - if p.exists(): - shutil.rmtree(p) - p.mkdir(parents=True, exist_ok=True) - - divide_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.divider", - "count": "16" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_path}/tmp_#.laz" - }} - ]}}""" - - merge_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type":"filters.merge" - }}, - {{ - "type":"writers.las", - "a_srs":"{srs}", - "filename":"{output_path}" - }} - ]}}""" - if input_path.is_dir() and output_path.is_dir(): - for in_file in input_path.iterdir(): - if in_file.suffix == '.las' or in_file.suffix == '.laz': - # create pipeline for dividing the las - div_pipeline_json = divide_pipeline.format(input_file=in_file.as_posix(), - srs=las_srs, - output_path=tmp_div_path.as_posix()) - - pipeline = pdal.Pipeline(div_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print('las is divided') - print(datetime.datetime.now().isoformat()) - - # for each of the created las-parts - for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): - - # color the las just as the normal las would be colored - tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) - - run_pdal(Path(f), tmp_col, - las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') - # merge all the colored lasses - merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), - srs=las_srs.replace('"', '\\"'), - output_path=output_path.as_posix()) - pipeline = pdal.Pipeline(merge_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print(datetime.datetime.now().isoformat()) - print('files merged') - print('process finished') - - else: - print(f'{in_file} is not a las or a laz file') - elif not output_path.is_dir() and not input_path.is_dir(): - # create pipeline for dividing the las - div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), - srs=las_srs, - output_path=tmp_div_path.as_posix()) - - pipeline = pdal.Pipeline(div_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print('las is divided') - print(datetime.datetime.now().isoformat()) - - # for each of the created las-parts - for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): - - # color the las just as the normal las would be colored - tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) - - run_pdal(Path(f), tmp_col, - las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') - # merge all the colored lasses - merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), - srs=las_srs.replace('"', '\\"'), - output_path=output_path.as_posix()) - pipeline = pdal.Pipeline(merge_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print(datetime.datetime.now().isoformat()) - print('files merged') - print('process finished') - else: - print('input and output are not both folders or both files.') - - # :todo remove temporary directories - - -def process_files(input_path, output_path, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size, verbose=False): - """ - Run the pdal pipeline for the input files. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - input_path = Path(input_path) - output_path = Path(output_path) - - if input_path.is_dir(): - for f in input_path.iterdir(): - if f.suffix == '.las' or f.suffix == '.laz': - - if output_path.is_dir(): - out = output_path / '{}_color{}'.format(f.stem, f.suffix) - else: - raise ValueError('Output path should be a directory if ' - 'the input path is a directory.') - - if verbose: - print('Colorizing {} ..'.format(f)) - print('Saving at {}.'.format(out)) - - run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - else: - if verbose: - print('Colorizing {} ..'.format(input_path)) - - if output_path.suffix == '.las' or output_path.suffix == '.laz': - if verbose: - print('Saving at {}.'.format(output_path)) - - run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - elif output_path.is_dir(): - out = output_path / '{}_color{}'.format(input_path.stem, - input_path.suffix) - - if verbose: - print('Saving at {}.'.format(out)) - - run_pdal(input_path, out, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - else: - raise ValueError('Specified output path not a LAS/LAZ file or ' - 'existing directory.') - - -def argument_parser(): - """ - Define and return the arguments. - """ - description = ('Colorize a las or laz file with a WMS service. ' - 'By default uses PDOK aerial photography.') - parser = argparse.ArgumentParser(description=description) - required_named = parser.add_argument_group('required named arguments') - required_named.add_argument('-i', '--input', - help='The input LAS/LAZ file or folder.', - required=True) - required_named.add_argument('-o', '--output', - help=('The output colorized LAS/LAZ ' - 'file or folder.'), - required=True) - parser.add_argument('-s', '--las_srs', - help=('The spatial reference system of the LAS data. ' - '(str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-w', '--wms_url', - help=('The url of the WMS service to use. ' - '(str, default: https://geodata. ' - 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), - required=False, - default=('https://geodata.nationaalgeoregister.nl' - '/luchtfoto/rgb/wms?')) - parser.add_argument('-l', '--wms_layer', - help=('The layer of the WMS service to use. ' - '(str, default: Actueel_ortho25)'), - required=False, - default='Actueel_ortho25') - parser.add_argument('-r', '--wms_srs', - help=('The spatial reference system of the WMS data ' - 'to request. (str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-f', '--wms_format', - help=('The image format of the WMS data to request. ' - '(str, default: image/png)'), - required=False, - default='image/png') - parser.add_argument('-v', '--wms_version', - help=('The version number of the WMS service. ' - '(str, default: 1.3.0)'), - required=False, - default='1.3.0') - parser.add_argument('-p', '--wms_pixel_size', - help=('The approximate desired pixel size of the ' - 'requested image. (float, default: 0.25)'), - required=False, - default=0.25) - parser.add_argument('-m', '--wms_max_image_size', - help=('The maximum size (in pixels) of the largest ' - 'side of the requested image. ' - '(int, default: 1000)'), - required=False, - default=1000) - parser.add_argument('-d', '--divide', - default=5, - action = "store_true", - help='Divide the point cloud in a given number of ' - 'smaller areas which are colored seperately') - parser.add_argument('-V', '--verbose', - default=False, - action="store_true", - help='Set verbose.') - args = parser.parse_args() - return args - - -def main(): - """ - Run the application. - """ - args = argument_parser() - if args.divide: - process_divided_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - else: - process_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - - -if __name__ == '__main__': - main() diff --git a/point-cloud-colorize_wip/pdal_colorize.py b/point-cloud-colorize_wip/pdal_colorize.py deleted file mode 100644 index 60a9d60..0000000 --- a/point-cloud-colorize_wip/pdal_colorize.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python3 -@author: Chris Lucas -""" - -from io import BytesIO -import math -import numpy as np -import matplotlib.image as mpimg -import pyproj -from owslib.wms import WebMapService -from requests.exceptions import ReadTimeout - - -def request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries): - """ - Request an image from a WMS. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - size : list of int - The size of the image to be requested in pixels. [x, y] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - for i in range(retries): - try: - wms = WebMapService(wms_url, version=wms_version) - wms_img = wms.getmap(layers=[wms_layer], - srs=wms_srs, - bbox=bbox, - size=size, - format=wms_format, - transparent=True) - break - except ReadTimeout as e: - if i != retries-1: - print("ReadTimeout, trying again..") - else: - raise e - - img = mpimg.imread(BytesIO(wms_img.read()), 0) - - return img - - -def image_size(bbox, pixel_size=0.25): - """ - Compute the size of the image to be requested in pixels based on the - bounding box and the pixel size. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - pixel_size : float - The desired pixel size of the requested image. - Returns - ------- - img_size : tuple of int - The size of the image to be requested in pixels. (x, y) - """ - dif_x = bbox[2] - bbox[0] - dif_y = bbox[3] - bbox[1] - aspect_ratio = dif_x / dif_y - resolution = int(dif_x * (1/pixel_size)) - img_size = (resolution, int(resolution / aspect_ratio)) - - return img_size - - -def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, - wms_format, pixel_size, max_image_size, retries=10): - """ - Retrieve the imagery from a WMS service for the given bounding box. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - pixel_size : float - The desired pixel size of the requested image. - max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - [xmin, ymin, xmax, ymax] = bbox - - x_range = xmax - xmin - y_range = ymax - ymin - longest_side = max([x_range, y_range]) - - if (longest_side * (1/pixel_size) > max_image_size): - length = max_image_size / (1/pixel_size) - length_pixels = max_image_size - - rows = int(math.ceil((ymax-ymin)/length)) - cols = int(math.ceil((xmax-xmin)/length)) - - img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) - - for col in range(cols): - for row in range(rows): - cell = [xmin+col*length, ymin+row*length, - xmin+(col+1)*length, ymin+(row+1)*length] - - img_part = request_image(cell, (length_pixels, length_pixels), - wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - img[((length_pixels*rows)-(row+1) * - length_pixels):(length_pixels*rows)-row*length_pixels, - col*length_pixels:(col+1)*length_pixels] = img_part - - img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, - :int(round(x_range*(1/pixel_size)))] - else: - size = image_size(bbox) - img = request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - return img - - -def las_colorize(ins, outs): - """ - PDAL python function. Adds RGB information to a LAS file by downloading - an orthophoto from a WMS service. - Parameters - ---------- - ins : PDAL input - outs : PDAL output - """ - X = ins['X'] - Y = ins['Y'] - - [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] - - if pdalargs['las_srs'] != pdalargs['wms_srs']: - p1 = pyproj.Proj(init=pdalargs['las_srs']) - p2 = pyproj.Proj(init=pdalargs['wms_srs']) - - bbox = [0, 0, 0, 0] - bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) - bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) - else: - bbox = [xmin, ymin, xmax, ymax] - - img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], - pdalargs['wms_srs'], pdalargs['wms_version'], - pdalargs['wms_format'], - float(pdalargs['wms_pixel_size']), - int(pdalargs['wms_max_image_size'])) - - img_size = img.shape[:2] - - x_img = np.round(((X - xmin) / (xmax-xmin)) * - (img_size[1]-1)).astype(int) - y_img = np.round(((ymax - Y) / (ymax-ymin)) * - (img_size[0]-1)).astype(int) - - rgb = img[y_img, x_img] * 255 - - outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) - outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) - outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) - - return True \ No newline at end of file From 5b85798f6da59368b2345f9d0d76f3e0f5ba0ea5 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 14:58:59 +0100 Subject: [PATCH 08/24] parallel is working, improved verbose --- .gitignore | 1 + point-cloud-colorize/las_colorize.py | 61 ++++++++-------------------- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index bbdc2b0..3187b05 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ debug/ *.las *.sh point-cloud-colorize/* +get_year.sql diff --git a/point-cloud-colorize/las_colorize.py b/point-cloud-colorize/las_colorize.py index 399722b..7fc5c50 100644 --- a/point-cloud-colorize/las_colorize.py +++ b/point-cloud-colorize/las_colorize.py @@ -96,8 +96,7 @@ def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col = 'tmp_col_ pdalargs['wms_pixel_size'],pdalargs['wms_max_image_size']) if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') + print(f'colored {i} parts of the las at {datetime.datetime.now()}') def process_files_parallel(input, output, las_srs, wms_url, wms_layer, wms_srs, @@ -111,17 +110,18 @@ def process_files_parallel(input, output, las_srs, :param verbose: :return: """ - print('colorizing parts') - print(datetime.datetime.now().isoformat()) + if verbose: + print(f'started colorizing parts at {datetime.datetime.now()}') output_path = Path(output) + if not output_path.is_dir(): + raise ValueError('Output should be a directory') + input_path = Path(input) tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) - tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) - for p in [tmp_div_path, tmp_col_path]: - if p.exists(): - shutil.rmtree(p) - p.mkdir(parents=True, exist_ok=True) + if tmp_div_path.exists(): + shutil.rmtree(tmp_div_path) + tmp_div_path.mkdir(parents=True, exist_ok=True) divide_pipeline = """{{ "pipeline":[ @@ -140,24 +140,6 @@ def process_files_parallel(input, output, las_srs, }} ]}}""" - - merge_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type":"filters.merge" - }}, - {{ - "type":"writers.las", - "a_srs":"{srs}", - "filename":"{output_path}" - }} - ]}}""" - - # create pipeline for dividing the las div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), srs=las_srs, @@ -167,8 +149,7 @@ def process_files_parallel(input, output, las_srs, pipeline.validate() pipeline.execute() if verbose: - print('las is divided') - print(datetime.datetime.now().isoformat()) + print(f'las is divided at {datetime.datetime.now()}') pdalargs = {'wms_url': wms_url, 'wms_layer': wms_layer, @@ -180,22 +161,14 @@ def process_files_parallel(input, output, las_srs, 'las_srs': las_srs} # for each of the created las-parts - print('start parallel processing') - print(datetime.datetime.now().isoformat()) - Parallel(n_jobs=1)(delayed(parallel_coloring)(f, i, verbose, tmp_col_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) - print('finished parallel processing') - print(datetime.datetime.now().isoformat()) - # merge all the colored lasses - merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), - srs=las_srs.replace('"', '\\"'), - output_path=output_path.as_posix()) - pipeline = pdal.Pipeline(merge_pipeline_json) - pipeline.validate() - pipeline.execute() if verbose: - print(datetime.datetime.now().isoformat()) - print('files merged') - print('process finished') + print(f'start parallel processing at {datetime.datetime.now()}') + + Parallel(n_jobs=6)(delayed(parallel_coloring)(f, i, verbose, output_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) + + if verbose: + print(f'finished parallel processing at {datetime.datetime.now()}') + print(f'colorizing in parts finished at {datetime.datetime.now()}') def process_files(input_path, output_path, las_srs, From 9ecd1d3b13762b41b61666f092330677e7759afb Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Thu, 22 Aug 2019 10:30:12 +0200 Subject: [PATCH 09/24] fix for __file__ --- point-cloud-colorize/las_colorize.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/point-cloud-colorize/las_colorize.py b/point-cloud-colorize/las_colorize.py index 7fc5c50..5be8e72 100644 --- a/point-cloud-colorize/las_colorize.py +++ b/point-cloud-colorize/las_colorize.py @@ -77,12 +77,15 @@ def run_pdal(input_path, output_path, las_srs, wms_url, 'las_srs': las_srs} pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - path = Path(__file__) + # path = Path(__file__) + path = Path(os.getcwd()) + print('{}'.format(path.parent.as_posix())) pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), output_file=output_path.as_posix(), srs=las_srs, pdalargs=pdalargs_str, - directory=path.parent.as_posix()) + # directory=path.parent.as_posix()) + directory=path.as_posix()) pipeline = pdal.Pipeline(pipeline_json) pipeline.validate() pipeline.execute() From 36dcea36e500eae3a8bb99a7362a239ef8cbfa49 Mon Sep 17 00:00:00 2001 From: Arno Date: Tue, 15 Oct 2019 10:09:07 +0200 Subject: [PATCH 10/24] added parralel coloring correctly --- .gitignore | 7 +- pipeline.json | 9 - point_cloud_colorize/__init__.py | 1 + .../las_colorize.py | 695 +++++++++--------- .../pdal_colorize.py | 410 ++++++----- setup.py | 18 + 6 files changed, 576 insertions(+), 564 deletions(-) delete mode 100644 pipeline.json create mode 100644 point_cloud_colorize/__init__.py rename {point-cloud-colorize => point_cloud_colorize}/las_colorize.py (95%) rename {point-cloud-colorize => point_cloud_colorize}/pdal_colorize.py (93%) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 3187b05..3583243 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,3 @@ .vscode/ .spyproject/ -debug/ -*.laz -*.las -*.sh -point-cloud-colorize/* -get_year.sql +debug/ \ No newline at end of file diff --git a/pipeline.json b/pipeline.json deleted file mode 100644 index 0419877..0000000 --- a/pipeline.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "pipeline": [ - "/var/data/arnot/div_color/tmp_col/*.laz", - { - "type" : "filters.merge" - }, - "/var/data/arnot/div_color/c_10cn2_color.laz" - ] -} diff --git a/point_cloud_colorize/__init__.py b/point_cloud_colorize/__init__.py new file mode 100644 index 0000000..da0d6b0 --- /dev/null +++ b/point_cloud_colorize/__init__.py @@ -0,0 +1 @@ +name = 'point_cloud_colorize' diff --git a/point-cloud-colorize/las_colorize.py b/point_cloud_colorize/las_colorize.py similarity index 95% rename from point-cloud-colorize/las_colorize.py rename to point_cloud_colorize/las_colorize.py index 5be8e72..48b5290 100644 --- a/point-cloud-colorize/las_colorize.py +++ b/point_cloud_colorize/las_colorize.py @@ -1,348 +1,347 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -import argparse -import json -from pathlib import Path -import shutil -import pdal -import datetime -from joblib import Parallel, delayed - - -PDAL_PIPELINE = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.python", - "script": "{directory}/pdal_colorize.py", - "function": "las_colorize", - "module": "anything", - "pdalargs": "{pdalargs}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_file}" - }} - ] -}}""" - - -def run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size): - """ - Run the pdal pipeline using the given arguments. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - """ - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - - pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - # path = Path(__file__) - path = Path(os.getcwd()) - print('{}'.format(path.parent.as_posix())) - pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), - output_file=output_path.as_posix(), - srs=las_srs, - pdalargs=pdalargs_str, - # directory=path.parent.as_posix()) - directory=path.as_posix()) - pipeline = pdal.Pipeline(pipeline_json) - pipeline.validate() - pipeline.execute() - -def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col = 'tmp_col_{}.laz'): - tmp_col = Path(tmp_col_path.joinpath(tmp_col.format(i))) - run_pdal(Path(f), tmp_col, - pdalargs['las_srs'],pdalargs['wms_url'], - pdalargs['wms_layer'], pdalargs['wms_srs'], - pdalargs['wms_version'] , pdalargs['wms_format'], - pdalargs['wms_pixel_size'],pdalargs['wms_max_image_size']) - - if verbose: - print(f'colored {i} parts of the las at {datetime.datetime.now()}') - -def process_files_parallel(input, output, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, - wms_pixel_size, wms_max_image_size, - verbose): - """ - :param input_path: - :param output_path: - :param las_srs: - :param verbose: - :return: - """ - if verbose: - print(f'started colorizing parts at {datetime.datetime.now()}') - output_path = Path(output) - if not output_path.is_dir(): - raise ValueError('Output should be a directory') - - input_path = Path(input) - tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) - - if tmp_div_path.exists(): - shutil.rmtree(tmp_div_path) - tmp_div_path.mkdir(parents=True, exist_ok=True) - - divide_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.divider", - "count": "6" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_path}/tmp_#.laz" - }} - ]}}""" - - # create pipeline for dividing the las - div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), - srs=las_srs, - output_path=tmp_div_path.as_posix()) - - pipeline = pdal.Pipeline(div_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print(f'las is divided at {datetime.datetime.now()}') - - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - - # for each of the created las-parts - if verbose: - print(f'start parallel processing at {datetime.datetime.now()}') - - Parallel(n_jobs=6)(delayed(parallel_coloring)(f, i, verbose, output_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) - - if verbose: - print(f'finished parallel processing at {datetime.datetime.now()}') - print(f'colorizing in parts finished at {datetime.datetime.now()}') - - -def process_files(input_path, output_path, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size, verbose=False): - """ - Run the pdal pipeline for the input files. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - input_path = Path(input_path) - output_path = Path(output_path) - - if input_path.is_dir(): - for f in input_path.iterdir(): - if f.suffix == '.las' or f.suffix == '.laz': - - if output_path.is_dir(): - out = output_path / '{}_color{}'.format(f.stem, f.suffix) - else: - raise ValueError('Output path should be a directory if ' - 'the input path is a directory.') - - if verbose: - print('Colorizing {} ..'.format(f)) - print('Saving at {}.'.format(out)) - - run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - else: - if verbose: - print('Colorizing {} ..'.format(input_path)) - - if output_path.suffix == '.las' or output_path.suffix == '.laz': - if verbose: - print('Saving at {}.'.format(output_path)) - - run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - elif output_path.is_dir(): - out = output_path / '{}_color{}'.format(input_path.stem, - input_path.suffix) - - if verbose: - print('Saving at {}.'.format(out)) - - run_pdal(input_path, out, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - else: - raise ValueError('Specified output path not a LAS/LAZ file or ' - 'existing directory.') - - -def argument_parser(): - """ - Define and return the arguments. - """ - description = ('Colorize a las or laz file with a WMS service. ' - 'By default uses PDOK aerial photography.') - parser = argparse.ArgumentParser(description=description) - required_named = parser.add_argument_group('required named arguments') - required_named.add_argument('-i', '--input', - help='The input LAS/LAZ file or folder.', - required=True) - required_named.add_argument('-o', '--output', - help=('The output colorized LAS/LAZ ' - 'file or folder.'), - required=True) - parser.add_argument('-s', '--las_srs', - help=('The spatial reference system of the LAS data. ' - '(str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-w', '--wms_url', - help=('The url of the WMS service to use. ' - '(str, default: https://geodata. ' - 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), - required=False, - default=('https://geodata.nationaalgeoregister.nl' - '/luchtfoto/rgb/wms?')) - parser.add_argument('-l', '--wms_layer', - help=('The layer of the WMS service to use. ' - '(str, default: Actueel_ortho25)'), - required=False, - default='Actueel_ortho25') - parser.add_argument('-r', '--wms_srs', - help=('The spatial reference system of the WMS data ' - 'to request. (str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-f', '--wms_format', - help=('The image format of the WMS data to request. ' - '(str, default: image/png)'), - required=False, - default='image/png') - parser.add_argument('-v', '--wms_version', - help=('The version number of the WMS service. ' - '(str, default: 1.3.0)'), - required=False, - default='1.3.0') - parser.add_argument('-p', '--wms_pixel_size', - help=('The approximate desired pixel size of the ' - 'requested image. (float, default: 0.25)'), - required=False, - default=0.25) - parser.add_argument('-m', '--wms_max_image_size', - help=('The maximum size (in pixels) of the largest ' - 'side of the requested image. ' - '(int, default: 1000)'), - required=False, - default=1000) - parser.add_argument('-d', '--divide', - default=5, - action = "store_true", - help='Divide the point cloud in a given number of ' - 'smaller areas which are colored seperately') - parser.add_argument('-V', '--verbose', - default=False, - action="store_true", - help='Set verbose.') - args = parser.parse_args() - return args - - -def main(): - """ - Run the application. - """ - args = argument_parser() - if args.divide: - process_files_parallel(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - else: - process_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - - -if __name__ == '__main__': - main() +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas, Arno Timmer +""" + +import argparse +import json +from pathlib import Path +import shutil +import pdal +import datetime +from joblib import Parallel, delayed + + +PDAL_PIPELINE = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.python", + "script": "{directory}/pdal_colorize.py", + "function": "las_colorize", + "module": "anything", + "pdalargs": "{pdalargs}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_file}" + }} + ] +}}""" + + +def run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size): + """ + Run the pdal pipeline using the given arguments. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + """ + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') + + path = Path(os.getcwd()) + + pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), + output_file=output_path.as_posix(), + srs=las_srs, + pdalargs=pdalargs_str, + directory=path.as_posix()) + pipeline = pdal.Pipeline(pipeline_json) + pipeline.validate() + pipeline.execute() + +def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col = 'tmp_col_{}.laz'): + tmp_col = Path(tmp_col_path.joinpath(tmp_col.format(i))) + run_pdal(Path(f), tmp_col, + pdalargs['las_srs'],pdalargs['wms_url'], + pdalargs['wms_layer'], pdalargs['wms_srs'], + pdalargs['wms_version'] , pdalargs['wms_format'], + pdalargs['wms_pixel_size'],pdalargs['wms_max_image_size']) + + if verbose: + print(f'colored {i} parts of the las at {datetime.datetime.now()}') + +def process_files_parallel(input, output, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, + wms_pixel_size, wms_max_image_size, + verbose): + """ + :param input_path: + :param output_path: + :param las_srs: + :param verbose: + :return: + """ + if verbose: + print(f'started colorizing parts at {datetime.datetime.now()}') + output_path = Path(output) + if not output_path.is_dir(): + raise ValueError('Output should be a directory') + + input_path = Path(input) + tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) + + if tmp_div_path.exists(): + if verbose: + print('Temporary path exists, deleting.') + shutil.rmtree(tmp_div_path) + tmp_div_path.mkdir(parents=True, exist_ok=True) + + divide_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.divider", + "count": "6" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_path}/tmp_#.laz" + }} + ]}}""" + + # create pipeline for dividing the las + div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + + if verbose: + print(f'las is divided at {datetime.datetime.now()}') + + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + # for each of the created las-parts + if verbose: + print(f'start parallel processing at {datetime.datetime.now()}') + + Parallel(n_jobs=6)(delayed(parallel_coloring)(f, i, verbose, output_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) + + if verbose: + print(f'colorizing in parts finished at {datetime.datetime.now()}') + + +def process_files(input_path, output_path, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size, verbose=False): + """ + Run the pdal pipeline for the input files. + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + input_path = Path(input_path) + output_path = Path(output_path) + + if input_path.is_dir(): + for f in input_path.iterdir(): + if f.suffix == '.las' or f.suffix == '.laz': + + if output_path.is_dir(): + out = output_path / '{}_color{}'.format(f.stem, f.suffix) + else: + raise ValueError('Output path should be a directory if ' + 'the input path is a directory.') + + if verbose: + print('Colorizing {} ..'.format(f)) + print('Saving at {}.'.format(out)) + + run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + else: + if verbose: + print('Colorizing {} ..'.format(input_path)) + + if output_path.suffix == '.las' or output_path.suffix == '.laz': + if verbose: + print('Saving at {}.'.format(output_path)) + + run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + elif output_path.is_dir(): + out = output_path / '{}_color{}'.format(input_path.stem, + input_path.suffix) + + if verbose: + print('Saving at {}.'.format(out)) + + run_pdal(input_path, out, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + else: + raise ValueError('Specified output path not a LAS/LAZ file or ' + 'existing directory.') + + +def argument_parser(): + """ + Define and return the arguments. + """ + description = ('Colorize a las or laz file with a WMS service. ' + 'By default uses PDOK aerial photography.') + parser = argparse.ArgumentParser(description=description) + required_named = parser.add_argument_group('required named arguments') + required_named.add_argument('-i', '--input', + help='The input LAS/LAZ file or folder.', + required=True) + required_named.add_argument('-o', '--output', + help=('The output colorized LAS/LAZ ' + 'file or folder.'), + required=True) + parser.add_argument('-s', '--las_srs', + help=('The spatial reference system of the LAS data. ' + '(str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-w', '--wms_url', + help=('The url of the WMS service to use. ' + '(str, default: https://geodata. ' + 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), + required=False, + default=('https://geodata.nationaalgeoregister.nl' + '/luchtfoto/rgb/wms?')) + parser.add_argument('-l', '--wms_layer', + help=('The layer of the WMS service to use. ' + '(str, default: Actueel_ortho25)'), + required=False, + default='Actueel_ortho25') + parser.add_argument('-r', '--wms_srs', + help=('The spatial reference system of the WMS data ' + 'to request. (str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-f', '--wms_format', + help=('The image format of the WMS data to request. ' + '(str, default: image/png)'), + required=False, + default='image/png') + parser.add_argument('-v', '--wms_version', + help=('The version number of the WMS service. ' + '(str, default: 1.3.0)'), + required=False, + default='1.3.0') + parser.add_argument('-p', '--wms_pixel_size', + help=('The approximate desired pixel size of the ' + 'requested image. (float, default: 0.25)'), + required=False, + default=0.25) + parser.add_argument('-m', '--wms_max_image_size', + help=('The maximum size (in pixels) of the largest ' + 'side of the requested image. ' + '(int, default: 1000)'), + required=False, + default=1000) + parser.add_argument('-d', '--divide', + default=5, + action = "store_true", + help='Divide the point cloud in a given number of ' + 'smaller areas which are colored seperately') + parser.add_argument('-V', '--verbose', + default=False, + action="store_true", + help='Set verbose.') + args = parser.parse_args() + return args + + +def main(): + """ + Run the application. + """ + args = argument_parser() + if args.divide: + process_files_parallel(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + else: + process_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + + +if __name__ == '__main__': + main() diff --git a/point-cloud-colorize/pdal_colorize.py b/point_cloud_colorize/pdal_colorize.py similarity index 93% rename from point-cloud-colorize/pdal_colorize.py rename to point_cloud_colorize/pdal_colorize.py index 50ef352..deea80e 100644 --- a/point-cloud-colorize/pdal_colorize.py +++ b/point_cloud_colorize/pdal_colorize.py @@ -1,201 +1,209 @@ -# -*- coding: utf-8 -*- -""" -Python3 -@author: Chris Lucas -""" - -from io import BytesIO -import math -import numpy as np -import matplotlib.image as mpimg -import pyproj -from owslib.wms import WebMapService -from requests.exceptions import ReadTimeout - - -def request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries): - """ - Request an image from a WMS. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - size : list of int - The size of the image to be requested in pixels. [x, y] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - for i in range(retries): - try: - wms = WebMapService(wms_url, version=wms_version) - wms_img = wms.getmap(layers=[wms_layer], - srs=wms_srs, - bbox=bbox, - size=size, - format=wms_format, - transparent=True) - break - except ReadTimeout as e: - if i != retries-1: - print("ReadTimeout, trying again..") - else: - raise e - - img = mpimg.imread(BytesIO(wms_img.read()), 0) - - return img - - -def image_size(bbox, pixel_size=0.25): - """ - Compute the size of the image to be requested in pixels based on the - bounding box and the pixel size. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - pixel_size : float - The desired pixel size of the requested image. - Returns - ------- - img_size : tuple of int - The size of the image to be requested in pixels. (x, y) - """ - dif_x = bbox[2] - bbox[0] - dif_y = bbox[3] - bbox[1] - aspect_ratio = dif_x / dif_y - resolution = int(dif_x * (1/pixel_size)) - img_size = (resolution, int(resolution / aspect_ratio)) - - return img_size - - -def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, - wms_format, pixel_size, max_image_size, retries=10): - """ - Retrieve the imagery from a WMS service for the given bounding box. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - pixel_size : float - The desired pixel size of the requested image. - max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - [xmin, ymin, xmax, ymax] = bbox - - x_range = xmax - xmin - y_range = ymax - ymin - longest_side = max([x_range, y_range]) - - if (longest_side * (1/pixel_size) > max_image_size): - length = max_image_size / (1/pixel_size) - length_pixels = max_image_size - - rows = int(math.ceil((ymax-ymin)/length)) - cols = int(math.ceil((xmax-xmin)/length)) - - img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) - - for col in range(cols): - for row in range(rows): - cell = [xmin+col*length, ymin+row*length, - xmin+(col+1)*length, ymin+(row+1)*length] - - img_part = request_image(cell, (length_pixels, length_pixels), - wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - img[((length_pixels*rows)-(row+1) * - length_pixels):(length_pixels*rows)-row*length_pixels, - col*length_pixels:(col+1)*length_pixels] = img_part - - img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, - :int(round(x_range*(1/pixel_size)))] - else: - size = image_size(bbox) - img = request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - return img - - -def las_colorize(ins, outs): - """ - PDAL python function. Adds RGB information to a LAS file by downloading - an orthophoto from a WMS service. - Parameters - ---------- - ins : PDAL input - outs : PDAL output - """ - X = ins['X'] - Y = ins['Y'] - - [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] - - if pdalargs['las_srs'] != pdalargs['wms_srs']: - p1 = pyproj.Proj(init=pdalargs['las_srs']) - p2 = pyproj.Proj(init=pdalargs['wms_srs']) - - bbox = [0, 0, 0, 0] - bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) - bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) - else: - bbox = [xmin, ymin, xmax, ymax] - - img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], - pdalargs['wms_srs'], pdalargs['wms_version'], - pdalargs['wms_format'], - float(pdalargs['wms_pixel_size']), - int(pdalargs['wms_max_image_size'])) - - img_size = img.shape[:2] - - x_img = np.round(((X - xmin) / (xmax-xmin)) * - (img_size[1]-1)).astype(int) - y_img = np.round(((ymax - Y) / (ymax-ymin)) * - (img_size[0]-1)).astype(int) - - rgb = img[y_img, x_img] * 255 - - outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) >>8 - outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) >> 8 - outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) >> 8 - outs['Intensity'] = ins['Intensity'] >> 8 - return True +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas +""" + +from io import BytesIO +import math +import numpy as np +import matplotlib.image as mpimg +import pyproj +from owslib.wms import WebMapService +from requests.exceptions import ReadTimeout + + +def request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries): + """ + Request an image from a WMS. + + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + size : list of int + The size of the image to be requested in pixels. [x, y] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + for i in range(retries): + try: + wms = WebMapService(wms_url, version=wms_version) + wms_img = wms.getmap(layers=[wms_layer], + srs=wms_srs, + bbox=bbox, + size=size, + format=wms_format, + transparent=True) + break + except ReadTimeout as e: + if i != retries-1: + print("ReadTimeout, trying again..") + else: + raise e + + img = mpimg.imread(BytesIO(wms_img.read()), 0) + + return img + + +def image_size(bbox, pixel_size=0.25): + """ + Compute the size of the image to be requested in pixels based on the + bounding box and the pixel size. + + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + pixel_size : float + The desired pixel size of the requested image. + + Returns + ------- + img_size : tuple of int + The size of the image to be requested in pixels. (x, y) + """ + dif_x = bbox[2] - bbox[0] + dif_y = bbox[3] - bbox[1] + aspect_ratio = dif_x / dif_y + resolution = int(dif_x * (1/pixel_size)) + img_size = (resolution, int(resolution / aspect_ratio)) + + return img_size + + +def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, + wms_format, pixel_size, max_image_size, retries=10): + """ + Retrieve the imagery from a WMS service for the given bounding box. + + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + pixel_size : float + The desired pixel size of the requested image. + max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + [xmin, ymin, xmax, ymax] = bbox + + x_range = xmax - xmin + y_range = ymax - ymin + longest_side = max([x_range, y_range]) + + if (longest_side * (1/pixel_size) > max_image_size): + length = max_image_size / (1/pixel_size) + length_pixels = max_image_size + + rows = int(math.ceil((ymax-ymin)/length)) + cols = int(math.ceil((xmax-xmin)/length)) + + img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) + + for col in range(cols): + for row in range(rows): + cell = [xmin+col*length, ymin+row*length, + xmin+(col+1)*length, ymin+(row+1)*length] + + img_part = request_image(cell, (length_pixels, length_pixels), + wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + img[((length_pixels*rows)-(row+1) * + length_pixels):(length_pixels*rows)-row*length_pixels, + col*length_pixels:(col+1)*length_pixels] = img_part + + img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, + :int(round(x_range*(1/pixel_size)))] + else: + size = image_size(bbox) + img = request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + return img + + +def las_colorize(ins, outs): + """ + PDAL python function. Adds RGB information to a LAS file by downloading + an orthophoto from a WMS service. + + Parameters + ---------- + ins : PDAL input + outs : PDAL output + """ + X = ins['X'] + Y = ins['Y'] + + [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] + + if pdalargs['las_srs'] != pdalargs['wms_srs']: + p1 = pyproj.Proj(init=pdalargs['las_srs']) + p2 = pyproj.Proj(init=pdalargs['wms_srs']) + + bbox = [0, 0, 0, 0] + bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) + bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) + else: + bbox = [xmin, ymin, xmax, ymax] + + img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], + pdalargs['wms_srs'], pdalargs['wms_version'], + pdalargs['wms_format'], + float(pdalargs['wms_pixel_size']), + int(pdalargs['wms_max_image_size'])) + + img_size = img.shape[:2] + + x_img = np.round(((X - xmin) / (xmax-xmin)) * + (img_size[1]-1)).astype(int) + y_img = np.round(((ymax - Y) / (ymax-ymin)) * + (img_size[0]-1)).astype(int) + + rgb = img[y_img, x_img] * 255 + + outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) + outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) + outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) + + return True diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..58fa5f1 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +import setuptools + +setuptools.setup( + name="point-cloud-colorize", + version="0.0.3", + author="Chris Lucas, Arno Timmer, Rein van 't Veer", + author_email="chris.lucas@geodan.nl, arno.timmer@geodan.nl, rein@geodan.nl", + description="Point cloud colorization library", + long_description="Colorizes point cloud .las or .laz files using aerial photography", + long_description_content_type="text/markdown", + url="https://github.com/Geodan/point-cloud-colorize", + packages=['point_cloud_colorize'], + classifiers=[ + "Programming Language :: Python :: 3", + "Operating System :: Windows", + ], + install_requires=[], +) From b3712b57d87a5a6488b0d20327cf504981dc9f87 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Thu, 7 Mar 2019 13:08:38 +0100 Subject: [PATCH 11/24] -d now colors 16 divided lasses --- .gitignore | 5 +- point-cloud-colorize/las_colorize.py.save | 359 +++++++++++++ point_cloud_colorize/las_colorize.py | 605 +++++++++++++--------- point_cloud_colorize/pdal_colorize.py | 410 +++++++-------- tst2cesium.sh | 11 + tstpipeline.sh | 14 + 6 files changed, 948 insertions(+), 456 deletions(-) create mode 100644 point-cloud-colorize/las_colorize.py.save create mode 100644 tst2cesium.sh create mode 100644 tstpipeline.sh diff --git a/.gitignore b/.gitignore index 3583243..9b354ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .vscode/ .spyproject/ -debug/ \ No newline at end of file +debug/ +*.laz +*.las +.*sh diff --git a/point-cloud-colorize/las_colorize.py.save b/point-cloud-colorize/las_colorize.py.save new file mode 100644 index 0000000..98762c6 --- /dev/null +++ b/point-cloud-colorize/las_colorize.py.save @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas +""" + +import argparse +import json +from pathlib import Path +import shutil +import pdal +import datetime + + +PDAL_PIPELINE = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.python", + "script": "{directory}/pdal_colorize.py", + "function": "las_colorize", + "module": "anything", + "pdalargs": "{pdalargs}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_file}" + }} + ] +}}""" + + +def run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size, n_div): + """ + Run the pdal pipeline using the given arguments. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + """ + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') + path = Path(__file__) + pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), + output_file=output_path.as_posix(), + srs=las_srs, + pdalargs=pdalargs_str, + directory=path.parent.as_posix()) + pipeline = pdal.Pipeline(pipeline_json) + pipeline.validate() + pipeline.execute() + + +def process_files_parallel(input, output, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, + wms_pixel_size, wms_max_image_size, + verbose): + """ + :param input_path: + :param output_path: + :param las_srs: + :param verbose: + :return: + """ + print('colorizing parts') + print(datetime.datetime.now().isoformat()) + output_path = Path(output) + input_path = Path(input) + tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) + tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) + + for p in [tmp_div_path, tmp_col_path]: + if p.exists(): + shutil.rmtree(p) + p.mkdir(parents=True, exist_ok=True) + + divide_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.divider", + "count": "{n_div}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_path}/tmp_#.laz" + }} + ]}}""" + + + merge_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type":"filters.merge" + }}, + {{ + "type":"writers.las", + "a_srs":"{srs}", + "filename":"{output_path}" + }} + ]}}""" + + + # create pipeline for dividing the las + + div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print('las is divided') + print(datetime.datetime.now().isoformat()) + + # for each of the created las-parts [PARRALEL] + for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): #todo: parrelelize + + # color the las just as the normal las would be colored + tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + + run_pdal(Path(f), tmp_col, + las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') + # merge all the colored lasses + merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), + srs=las_srs.replace('"', '\\"'), + output_path=output_path.as_posix()) + pipeline = pdal.Pipeline(merge_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print(datetime.datetime.now().isoformat()) + print('files merged') + print('process finished') + + +def process_files(input_path, output_path, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size, verbose=False): + """ + Run the pdal pipeline for the input files. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + input_path = Path(input_path) + output_path = Path(output_path) + + if input_path.is_dir(): + for f in input_path.iterdir(): + if f.suffix == '.las' or f.suffix == '.laz': + + if output_path.is_dir(): + out = output_path / '{}_color{}'.format(f.stem, f.suffix) + else: + raise ValueError('Output path should be a directory if ' + 'the input path is a directory.') + + if verbose: + print('Colorizing {} ..'.format(f)) + print('Saving at {}.'.format(out)) + + run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + else: + if verbose: + print('Colorizing {} ..'.format(input_path)) + + if output_path.suffix == '.las' or output_path.suffix == '.laz': + if verbose: + print('Saving at {}.'.format(output_path)) + + run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + elif output_path.is_dir(): + out = output_path / '{}_color{}'.format(input_path.stem, + input_path.suffix) + + if verbose: + print('Saving at {}.'.format(out)) + + run_pdal(input_path, out, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + else: + raise ValueError('Specified output path not a LAS/LAZ file or ' + 'existing directory.') + + +def argument_parser(): + """ + Define and return the arguments. + """ + description = ('Colorize a las or laz file with a WMS service. ' + 'By default uses PDOK aerial photography.') + parser = argparse.ArgumentParser(description=description) + required_named = parser.add_argument_group('required named arguments') + required_named.add_argument('-i', '--input', + help='The input LAS/LAZ file or folder.', + required=True) + required_named.add_argument('-o', '--output', + help=('The output colorized LAS/LAZ ' + 'file or folder.'), + required=True) + parser.add_argument('-s', '--las_srs', + help=('The spatial reference system of the LAS data. ' + '(str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-w', '--wms_url', + help=('The url of the WMS service to use. ' + '(str, default: https://geodata. ' + 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), + required=False, + default=('https://geodata.nationaalgeoregister.nl' + '/luchtfoto/rgb/wms?')) + parser.add_argument('-l', '--wms_layer', + help=('The layer of the WMS service to use. ' + '(str, default: Actueel_ortho25)'), + required=False, + default='Actueel_ortho25') + parser.add_argument('-r', '--wms_srs', + help=('The spatial reference system of the WMS data ' + 'to request. (str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-f', '--wms_format', + help=('The image format of the WMS data to request. ' + '(str, default: image/png)'), + required=False, + default='image/png') + parser.add_argument('-v', '--wms_version', + help=('The version number of the WMS service. ' + '(str, default: 1.3.0)'), + required=False, + default='1.3.0') + parser.add_argument('-p', '--wms_pixel_size', + help=('The approximate desired pixel size of the ' + 'requested image. (float, default: 0.25)'), + required=False, + default=0.25) + parser.add_argument('-m', '--wms_max_image_size', + help=('The maximum size (in pixels) of the largest ' + 'side of the requested image. ' + '(int, default: 1000)'), + required=False, + default=1000) + parser.add_argument('-d', '--divide', + action="store_true", + default=False, + help='Divide the point cloud in smaller areas' + 'which are colored in parallel') + parser.add_argument('-V', '--verbose', + default=False, + action="store_true", + help='Set verbose.') + args = parser.parse_args() + return args + + +def main(): + """ + Run the application. + """ + args = argument_parser() + if args.divide: + process_files_parallel(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + else: + process_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + + +if __name__ == '__main__': + main() diff --git a/point_cloud_colorize/las_colorize.py b/point_cloud_colorize/las_colorize.py index be43a55..d3cf733 100644 --- a/point_cloud_colorize/las_colorize.py +++ b/point_cloud_colorize/las_colorize.py @@ -1,246 +1,359 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -import argparse -import json -from pathlib import Path - -import pdal - - -PDAL_PIPELINE = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.python", - "script": "{directory}/pdal_colorize.py", - "function": "las_colorize", - "module": "anything", - "pdalargs": "{pdalargs}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_file}" - }} - ] -}}""" - - -def run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size): - """ - Run the pdal pipeline using the given arguments. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - """ - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - - path = Path(__file__) - pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), - output_file=output_path.as_posix(), - srs=las_srs, - pdalargs=pdalargs_str, - directory=path.parent.as_posix()) - pipeline = pdal.Pipeline(pipeline_json) - pipeline.validate() - pipeline.execute() - - -def process_files(input_path, output_path, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size, verbose=False): - """ - Run the pdal pipeline for the input files. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - input_path = Path(input_path) - output_path = Path(output_path) - - if input_path.is_dir(): - for f in input_path.iterdir(): - if f.suffix == '.las' or f.suffix == '.laz': - - if output_path.is_dir(): - out = output_path / '{}_color{}'.format(f.stem, f.suffix) - else: - raise ValueError('Output path should be a directory if ' - 'the input path is a directory.') - - if verbose: - print('Colorizing {} ..'.format(f)) - print('Saving at {}.'.format(out)) - - run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - else: - if verbose: - print('Colorizing {} ..'.format(input_path)) - - if output_path.suffix == '.las' or output_path.suffix == '.laz': - if verbose: - print('Saving at {}.'.format(output_path)) - - run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - elif output_path.is_dir(): - out = output_path / '{}_color{}'.format(input_path.stem, - input_path.suffix) - - if verbose: - print('Saving at {}.'.format(out)) - - run_pdal(input_path, out, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - else: - raise ValueError('Specified output path not a LAS/LAZ file or ' - 'existing directory.') - - -def argument_parser(): - """ - Define and return the arguments. - """ - description = ('Colorize a las or laz file with a WMS service. ' - 'By default uses PDOK aerial photography.') - parser = argparse.ArgumentParser(description=description) - required_named = parser.add_argument_group('required named arguments') - required_named.add_argument('-i', '--input', - help='The input LAS/LAZ file or folder.', - required=True) - required_named.add_argument('-o', '--output', - help=('The output colorized LAS/LAZ ' - 'file or folder.'), - required=True) - parser.add_argument('-s', '--las_srs', - help=('The spatial reference system of the LAS data. ' - '(str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-w', '--wms_url', - help=('The url of the WMS service to use. ' - '(str, default: https://geodata. ' - 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), - required=False, - default=('https://geodata.nationaalgeoregister.nl' - '/luchtfoto/rgb/wms?')) - parser.add_argument('-l', '--wms_layer', - help=('The layer of the WMS service to use. ' - '(str, default: Actueel_ortho25)'), - required=False, - default='Actueel_ortho25') - parser.add_argument('-r', '--wms_srs', - help=('The spatial reference system of the WMS data ' - 'to request. (str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-f', '--wms_format', - help=('The image format of the WMS data to request. ' - '(str, default: image/png)'), - required=False, - default='image/png') - parser.add_argument('-v', '--wms_version', - help=('The version number of the WMS service. ' - '(str, default: 1.3.0)'), - required=False, - default='1.3.0') - parser.add_argument('-p', '--wms_pixel_size', - help=('The approximate desired pixel size of the ' - 'requested image. (float, default: 0.25)'), - required=False, - default=0.25) - parser.add_argument('-m', '--wms_max_image_size', - help=('The maximum size (in pixels) of the largest ' - 'side of the requested image. ' - '(int, default: 1000)'), - required=False, - default=1000) - parser.add_argument('-V', '--verbose', default=False, action="store_true", - help='Set verbose.') - args = parser.parse_args() - return args - - -def main(): - """ - Run the application. - """ - args = argument_parser() - process_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - - -if __name__ == '__main__': - main() +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas +""" + +import argparse +import json +from pathlib import Path +import shutil +import pdal +import datetime + + +PDAL_PIPELINE = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.python", + "script": "{directory}/pdal_colorize.py", + "function": "las_colorize", + "module": "anything", + "pdalargs": "{pdalargs}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_file}" + }} + ] +}}""" + + +def run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size): + """ + Run the pdal pipeline using the given arguments. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + """ + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') + path = Path(__file__) + pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), + output_file=output_path.as_posix(), + srs=las_srs, + pdalargs=pdalargs_str, + directory=path.parent.as_posix()) + pipeline = pdal.Pipeline(pipeline_json) + pipeline.validate() + pipeline.execute() + + +def process_files_parallel(input, output, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, + wms_pixel_size, wms_max_image_size, + verbose): + """ + :param input_path: + :param output_path: + :param las_srs: + :param verbose: + :return: + """ + print('colorizing parts') + print(datetime.datetime.now().isoformat()) + output_path = Path(output) + input_path = Path(input) + tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) + tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) + + for p in [tmp_div_path, tmp_col_path]: + if p.exists(): + shutil.rmtree(p) + p.mkdir(parents=True, exist_ok=True) + + divide_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.divider", + "count": "16" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_path}/tmp_#.laz" + }} + ]}}""" + + + merge_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type":"filters.merge" + }}, + {{ + "type":"writers.las", + "a_srs":"{srs}", + "filename":"{output_path}" + }} + ]}}""" + + + # create pipeline for dividing the las + + div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print('las is divided') + print(datetime.datetime.now().isoformat()) + + # for each of the created las-parts [PARRALEL] + for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): #todo: parrelelize + + # color the las just as the normal las would be colored + tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + + run_pdal(Path(f), tmp_col, + las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') + # merge all the colored lasses + merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), + srs=las_srs.replace('"', '\\"'), + output_path=output_path.as_posix()) + pipeline = pdal.Pipeline(merge_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print(datetime.datetime.now().isoformat()) + print('files merged') + print('process finished') + + +def process_files(input_path, output_path, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size, verbose=False): + """ + Run the pdal pipeline for the input files. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + input_path = Path(input_path) + output_path = Path(output_path) + + if input_path.is_dir(): + for f in input_path.iterdir(): + if f.suffix == '.las' or f.suffix == '.laz': + + if output_path.is_dir(): + out = output_path / '{}_color{}'.format(f.stem, f.suffix) + else: + raise ValueError('Output path should be a directory if ' + 'the input path is a directory.') + + if verbose: + print('Colorizing {} ..'.format(f)) + print('Saving at {}.'.format(out)) + + run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + else: + if verbose: + print('Colorizing {} ..'.format(input_path)) + + if output_path.suffix == '.las' or output_path.suffix == '.laz': + if verbose: + print('Saving at {}.'.format(output_path)) + + run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + elif output_path.is_dir(): + out = output_path / '{}_color{}'.format(input_path.stem, + input_path.suffix) + + if verbose: + print('Saving at {}.'.format(out)) + + run_pdal(input_path, out, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + else: + raise ValueError('Specified output path not a LAS/LAZ file or ' + 'existing directory.') + + +def argument_parser(): + """ + Define and return the arguments. + """ + description = ('Colorize a las or laz file with a WMS service. ' + 'By default uses PDOK aerial photography.') + parser = argparse.ArgumentParser(description=description) + required_named = parser.add_argument_group('required named arguments') + required_named.add_argument('-i', '--input', + help='The input LAS/LAZ file or folder.', + required=True) + required_named.add_argument('-o', '--output', + help=('The output colorized LAS/LAZ ' + 'file or folder.'), + required=True) + parser.add_argument('-s', '--las_srs', + help=('The spatial reference system of the LAS data. ' + '(str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-w', '--wms_url', + help=('The url of the WMS service to use. ' + '(str, default: https://geodata. ' + 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), + required=False, + default=('https://geodata.nationaalgeoregister.nl' + '/luchtfoto/rgb/wms?')) + parser.add_argument('-l', '--wms_layer', + help=('The layer of the WMS service to use. ' + '(str, default: Actueel_ortho25)'), + required=False, + default='Actueel_ortho25') + parser.add_argument('-r', '--wms_srs', + help=('The spatial reference system of the WMS data ' + 'to request. (str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-f', '--wms_format', + help=('The image format of the WMS data to request. ' + '(str, default: image/png)'), + required=False, + default='image/png') + parser.add_argument('-v', '--wms_version', + help=('The version number of the WMS service. ' + '(str, default: 1.3.0)'), + required=False, + default='1.3.0') + parser.add_argument('-p', '--wms_pixel_size', + help=('The approximate desired pixel size of the ' + 'requested image. (float, default: 0.25)'), + required=False, + default=0.25) + parser.add_argument('-m', '--wms_max_image_size', + help=('The maximum size (in pixels) of the largest ' + 'side of the requested image. ' + '(int, default: 1000)'), + required=False, + default=1000) + parser.add_argument('-d', '--divide', + default=5, + action = "store_true", + help='Divide the point cloud in a given number of ' + 'smaller areas which are colored seperately') + parser.add_argument('-V', '--verbose', + default=False, + action="store_true", + help='Set verbose.') + args = parser.parse_args() + return args + + +def main(): + """ + Run the application. + """ + args = argument_parser() + if args.divide: + process_files_parallel(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + else: + process_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + + +if __name__ == '__main__': + main() diff --git a/point_cloud_colorize/pdal_colorize.py b/point_cloud_colorize/pdal_colorize.py index deea80e..60a9d60 100644 --- a/point_cloud_colorize/pdal_colorize.py +++ b/point_cloud_colorize/pdal_colorize.py @@ -1,209 +1,201 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -from io import BytesIO -import math -import numpy as np -import matplotlib.image as mpimg -import pyproj -from owslib.wms import WebMapService -from requests.exceptions import ReadTimeout - - -def request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries): - """ - Request an image from a WMS. - - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - size : list of int - The size of the image to be requested in pixels. [x, y] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - for i in range(retries): - try: - wms = WebMapService(wms_url, version=wms_version) - wms_img = wms.getmap(layers=[wms_layer], - srs=wms_srs, - bbox=bbox, - size=size, - format=wms_format, - transparent=True) - break - except ReadTimeout as e: - if i != retries-1: - print("ReadTimeout, trying again..") - else: - raise e - - img = mpimg.imread(BytesIO(wms_img.read()), 0) - - return img - - -def image_size(bbox, pixel_size=0.25): - """ - Compute the size of the image to be requested in pixels based on the - bounding box and the pixel size. - - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - pixel_size : float - The desired pixel size of the requested image. - - Returns - ------- - img_size : tuple of int - The size of the image to be requested in pixels. (x, y) - """ - dif_x = bbox[2] - bbox[0] - dif_y = bbox[3] - bbox[1] - aspect_ratio = dif_x / dif_y - resolution = int(dif_x * (1/pixel_size)) - img_size = (resolution, int(resolution / aspect_ratio)) - - return img_size - - -def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, - wms_format, pixel_size, max_image_size, retries=10): - """ - Retrieve the imagery from a WMS service for the given bounding box. - - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - pixel_size : float - The desired pixel size of the requested image. - max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - [xmin, ymin, xmax, ymax] = bbox - - x_range = xmax - xmin - y_range = ymax - ymin - longest_side = max([x_range, y_range]) - - if (longest_side * (1/pixel_size) > max_image_size): - length = max_image_size / (1/pixel_size) - length_pixels = max_image_size - - rows = int(math.ceil((ymax-ymin)/length)) - cols = int(math.ceil((xmax-xmin)/length)) - - img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) - - for col in range(cols): - for row in range(rows): - cell = [xmin+col*length, ymin+row*length, - xmin+(col+1)*length, ymin+(row+1)*length] - - img_part = request_image(cell, (length_pixels, length_pixels), - wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - img[((length_pixels*rows)-(row+1) * - length_pixels):(length_pixels*rows)-row*length_pixels, - col*length_pixels:(col+1)*length_pixels] = img_part - - img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, - :int(round(x_range*(1/pixel_size)))] - else: - size = image_size(bbox) - img = request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - return img - - -def las_colorize(ins, outs): - """ - PDAL python function. Adds RGB information to a LAS file by downloading - an orthophoto from a WMS service. - - Parameters - ---------- - ins : PDAL input - outs : PDAL output - """ - X = ins['X'] - Y = ins['Y'] - - [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] - - if pdalargs['las_srs'] != pdalargs['wms_srs']: - p1 = pyproj.Proj(init=pdalargs['las_srs']) - p2 = pyproj.Proj(init=pdalargs['wms_srs']) - - bbox = [0, 0, 0, 0] - bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) - bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) - else: - bbox = [xmin, ymin, xmax, ymax] - - img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], - pdalargs['wms_srs'], pdalargs['wms_version'], - pdalargs['wms_format'], - float(pdalargs['wms_pixel_size']), - int(pdalargs['wms_max_image_size'])) - - img_size = img.shape[:2] - - x_img = np.round(((X - xmin) / (xmax-xmin)) * - (img_size[1]-1)).astype(int) - y_img = np.round(((ymax - Y) / (ymax-ymin)) * - (img_size[0]-1)).astype(int) - - rgb = img[y_img, x_img] * 255 - - outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) - outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) - outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) - - return True +# -*- coding: utf-8 -*- +""" +Python3 +@author: Chris Lucas +""" + +from io import BytesIO +import math +import numpy as np +import matplotlib.image as mpimg +import pyproj +from owslib.wms import WebMapService +from requests.exceptions import ReadTimeout + + +def request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries): + """ + Request an image from a WMS. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + size : list of int + The size of the image to be requested in pixels. [x, y] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + for i in range(retries): + try: + wms = WebMapService(wms_url, version=wms_version) + wms_img = wms.getmap(layers=[wms_layer], + srs=wms_srs, + bbox=bbox, + size=size, + format=wms_format, + transparent=True) + break + except ReadTimeout as e: + if i != retries-1: + print("ReadTimeout, trying again..") + else: + raise e + + img = mpimg.imread(BytesIO(wms_img.read()), 0) + + return img + + +def image_size(bbox, pixel_size=0.25): + """ + Compute the size of the image to be requested in pixels based on the + bounding box and the pixel size. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + pixel_size : float + The desired pixel size of the requested image. + Returns + ------- + img_size : tuple of int + The size of the image to be requested in pixels. (x, y) + """ + dif_x = bbox[2] - bbox[0] + dif_y = bbox[3] - bbox[1] + aspect_ratio = dif_x / dif_y + resolution = int(dif_x * (1/pixel_size)) + img_size = (resolution, int(resolution / aspect_ratio)) + + return img_size + + +def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, + wms_format, pixel_size, max_image_size, retries=10): + """ + Retrieve the imagery from a WMS service for the given bounding box. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + pixel_size : float + The desired pixel size of the requested image. + max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + [xmin, ymin, xmax, ymax] = bbox + + x_range = xmax - xmin + y_range = ymax - ymin + longest_side = max([x_range, y_range]) + + if (longest_side * (1/pixel_size) > max_image_size): + length = max_image_size / (1/pixel_size) + length_pixels = max_image_size + + rows = int(math.ceil((ymax-ymin)/length)) + cols = int(math.ceil((xmax-xmin)/length)) + + img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) + + for col in range(cols): + for row in range(rows): + cell = [xmin+col*length, ymin+row*length, + xmin+(col+1)*length, ymin+(row+1)*length] + + img_part = request_image(cell, (length_pixels, length_pixels), + wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + img[((length_pixels*rows)-(row+1) * + length_pixels):(length_pixels*rows)-row*length_pixels, + col*length_pixels:(col+1)*length_pixels] = img_part + + img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, + :int(round(x_range*(1/pixel_size)))] + else: + size = image_size(bbox) + img = request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + return img + + +def las_colorize(ins, outs): + """ + PDAL python function. Adds RGB information to a LAS file by downloading + an orthophoto from a WMS service. + Parameters + ---------- + ins : PDAL input + outs : PDAL output + """ + X = ins['X'] + Y = ins['Y'] + + [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] + + if pdalargs['las_srs'] != pdalargs['wms_srs']: + p1 = pyproj.Proj(init=pdalargs['las_srs']) + p2 = pyproj.Proj(init=pdalargs['wms_srs']) + + bbox = [0, 0, 0, 0] + bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) + bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) + else: + bbox = [xmin, ymin, xmax, ymax] + + img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], + pdalargs['wms_srs'], pdalargs['wms_version'], + pdalargs['wms_format'], + float(pdalargs['wms_pixel_size']), + int(pdalargs['wms_max_image_size'])) + + img_size = img.shape[:2] + + x_img = np.round(((X - xmin) / (xmax-xmin)) * + (img_size[1]-1)).astype(int) + y_img = np.round(((ymax - Y) / (ymax-ymin)) * + (img_size[0]-1)).astype(int) + + rgb = img[y_img, x_img] * 255 + + outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) + outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) + outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) + + return True \ No newline at end of file diff --git a/tst2cesium.sh b/tst2cesium.sh new file mode 100644 index 0000000..0a40578 --- /dev/null +++ b/tst2cesium.sh @@ -0,0 +1,11 @@ +mkdir /var/data/arnot/div_color/ +python3 point-cloud-colorize/las_colorize.py -i /var/data/arnot/AHN/baarsjes.laz -o /var/data/arnot/div_color/c_10cn2_col.laz -V -d + +mkdir /var/data/arnot/div_color/build/ +mkdir /var/data/arnot/div_color/cesium/ +echo 'colored'; +entwine build -i /var/data/arnot/div_color/c_10cn2_col.laz -o /var/data/arnot/div_color/build/ -r EPSG:28992 EPSG:4978; +echo 'build'; +entwine convert -i /var/data/arnot/div_color/build/ -o /var/data/arnot/div_color/cesium/; +echo 'converted to cesium'; + diff --git a/tstpipeline.sh b/tstpipeline.sh new file mode 100644 index 0000000..684ceb3 --- /dev/null +++ b/tstpipeline.sh @@ -0,0 +1,14 @@ +{ + "pipeline":[ + {"type": "readers.las", + "filename":"/var/data/arnot/c_10cn2_color.laz", + "spatialreference":"EPSG:28992"}, + {"type":"filters.python", + "script":"normalize.py", + "function":"normalize"}, + {"type":"writers.las", + "filename":"/var/data/arnot/AHN/filtered_laz.laz"} +] +} + + From 7f9a78e79c9a4d7baba0d79aa14e588a58287be9 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Thu, 7 Mar 2019 13:34:24 +0100 Subject: [PATCH 12/24] removed testfiles --- tst2cesium.sh | 11 ----------- tstpipeline.sh | 14 -------------- 2 files changed, 25 deletions(-) delete mode 100644 tst2cesium.sh delete mode 100644 tstpipeline.sh diff --git a/tst2cesium.sh b/tst2cesium.sh deleted file mode 100644 index 0a40578..0000000 --- a/tst2cesium.sh +++ /dev/null @@ -1,11 +0,0 @@ -mkdir /var/data/arnot/div_color/ -python3 point-cloud-colorize/las_colorize.py -i /var/data/arnot/AHN/baarsjes.laz -o /var/data/arnot/div_color/c_10cn2_col.laz -V -d - -mkdir /var/data/arnot/div_color/build/ -mkdir /var/data/arnot/div_color/cesium/ -echo 'colored'; -entwine build -i /var/data/arnot/div_color/c_10cn2_col.laz -o /var/data/arnot/div_color/build/ -r EPSG:28992 EPSG:4978; -echo 'build'; -entwine convert -i /var/data/arnot/div_color/build/ -o /var/data/arnot/div_color/cesium/; -echo 'converted to cesium'; - diff --git a/tstpipeline.sh b/tstpipeline.sh deleted file mode 100644 index 684ceb3..0000000 --- a/tstpipeline.sh +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pipeline":[ - {"type": "readers.las", - "filename":"/var/data/arnot/c_10cn2_color.laz", - "spatialreference":"EPSG:28992"}, - {"type":"filters.python", - "script":"normalize.py", - "function":"normalize"}, - {"type":"writers.las", - "filename":"/var/data/arnot/AHN/filtered_laz.laz"} -] -} - - From a3b808971fe8689690a1f588be535fc871fecc6d Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Thu, 7 Mar 2019 13:37:05 +0100 Subject: [PATCH 13/24] remove test files --- .gitignore | 2 + point-cloud-colorize/las_colorize.py.save | 359 ---------------------- 2 files changed, 2 insertions(+), 359 deletions(-) delete mode 100644 point-cloud-colorize/las_colorize.py.save diff --git a/.gitignore b/.gitignore index 9b354ee..781a3b6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ debug/ *.laz *.las .*sh +.gitignore + diff --git a/point-cloud-colorize/las_colorize.py.save b/point-cloud-colorize/las_colorize.py.save deleted file mode 100644 index 98762c6..0000000 --- a/point-cloud-colorize/las_colorize.py.save +++ /dev/null @@ -1,359 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -import argparse -import json -from pathlib import Path -import shutil -import pdal -import datetime - - -PDAL_PIPELINE = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.python", - "script": "{directory}/pdal_colorize.py", - "function": "las_colorize", - "module": "anything", - "pdalargs": "{pdalargs}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_file}" - }} - ] -}}""" - - -def run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size, n_div): - """ - Run the pdal pipeline using the given arguments. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - """ - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - - pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - path = Path(__file__) - pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), - output_file=output_path.as_posix(), - srs=las_srs, - pdalargs=pdalargs_str, - directory=path.parent.as_posix()) - pipeline = pdal.Pipeline(pipeline_json) - pipeline.validate() - pipeline.execute() - - -def process_files_parallel(input, output, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, - wms_pixel_size, wms_max_image_size, - verbose): - """ - :param input_path: - :param output_path: - :param las_srs: - :param verbose: - :return: - """ - print('colorizing parts') - print(datetime.datetime.now().isoformat()) - output_path = Path(output) - input_path = Path(input) - tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) - tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) - - for p in [tmp_div_path, tmp_col_path]: - if p.exists(): - shutil.rmtree(p) - p.mkdir(parents=True, exist_ok=True) - - divide_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.divider", - "count": "{n_div}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_path}/tmp_#.laz" - }} - ]}}""" - - - merge_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type":"filters.merge" - }}, - {{ - "type":"writers.las", - "a_srs":"{srs}", - "filename":"{output_path}" - }} - ]}}""" - - - # create pipeline for dividing the las - - div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), - srs=las_srs, - output_path=tmp_div_path.as_posix()) - - pipeline = pdal.Pipeline(div_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print('las is divided') - print(datetime.datetime.now().isoformat()) - - # for each of the created las-parts [PARRALEL] - for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): #todo: parrelelize - - # color the las just as the normal las would be colored - tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) - - run_pdal(Path(f), tmp_col, - las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') - # merge all the colored lasses - merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), - srs=las_srs.replace('"', '\\"'), - output_path=output_path.as_posix()) - pipeline = pdal.Pipeline(merge_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print(datetime.datetime.now().isoformat()) - print('files merged') - print('process finished') - - -def process_files(input_path, output_path, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size, verbose=False): - """ - Run the pdal pipeline for the input files. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - input_path = Path(input_path) - output_path = Path(output_path) - - if input_path.is_dir(): - for f in input_path.iterdir(): - if f.suffix == '.las' or f.suffix == '.laz': - - if output_path.is_dir(): - out = output_path / '{}_color{}'.format(f.stem, f.suffix) - else: - raise ValueError('Output path should be a directory if ' - 'the input path is a directory.') - - if verbose: - print('Colorizing {} ..'.format(f)) - print('Saving at {}.'.format(out)) - - run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - else: - if verbose: - print('Colorizing {} ..'.format(input_path)) - - if output_path.suffix == '.las' or output_path.suffix == '.laz': - if verbose: - print('Saving at {}.'.format(output_path)) - - run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - elif output_path.is_dir(): - out = output_path / '{}_color{}'.format(input_path.stem, - input_path.suffix) - - if verbose: - print('Saving at {}.'.format(out)) - - run_pdal(input_path, out, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - else: - raise ValueError('Specified output path not a LAS/LAZ file or ' - 'existing directory.') - - -def argument_parser(): - """ - Define and return the arguments. - """ - description = ('Colorize a las or laz file with a WMS service. ' - 'By default uses PDOK aerial photography.') - parser = argparse.ArgumentParser(description=description) - required_named = parser.add_argument_group('required named arguments') - required_named.add_argument('-i', '--input', - help='The input LAS/LAZ file or folder.', - required=True) - required_named.add_argument('-o', '--output', - help=('The output colorized LAS/LAZ ' - 'file or folder.'), - required=True) - parser.add_argument('-s', '--las_srs', - help=('The spatial reference system of the LAS data. ' - '(str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-w', '--wms_url', - help=('The url of the WMS service to use. ' - '(str, default: https://geodata. ' - 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), - required=False, - default=('https://geodata.nationaalgeoregister.nl' - '/luchtfoto/rgb/wms?')) - parser.add_argument('-l', '--wms_layer', - help=('The layer of the WMS service to use. ' - '(str, default: Actueel_ortho25)'), - required=False, - default='Actueel_ortho25') - parser.add_argument('-r', '--wms_srs', - help=('The spatial reference system of the WMS data ' - 'to request. (str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-f', '--wms_format', - help=('The image format of the WMS data to request. ' - '(str, default: image/png)'), - required=False, - default='image/png') - parser.add_argument('-v', '--wms_version', - help=('The version number of the WMS service. ' - '(str, default: 1.3.0)'), - required=False, - default='1.3.0') - parser.add_argument('-p', '--wms_pixel_size', - help=('The approximate desired pixel size of the ' - 'requested image. (float, default: 0.25)'), - required=False, - default=0.25) - parser.add_argument('-m', '--wms_max_image_size', - help=('The maximum size (in pixels) of the largest ' - 'side of the requested image. ' - '(int, default: 1000)'), - required=False, - default=1000) - parser.add_argument('-d', '--divide', - action="store_true", - default=False, - help='Divide the point cloud in smaller areas' - 'which are colored in parallel') - parser.add_argument('-V', '--verbose', - default=False, - action="store_true", - help='Set verbose.') - args = parser.parse_args() - return args - - -def main(): - """ - Run the application. - """ - args = argument_parser() - if args.divide: - process_files_parallel(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - else: - process_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - - -if __name__ == '__main__': - main() From 0a63ccb62d41f980984ef44fb187668a8a891f34 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 08:53:56 +0100 Subject: [PATCH 14/24] parts are now colored in parallel --- .gitignore | 3 +- pipeline.json | 9 + point-cloud-colorize_wip/las_colorize.py | 430 ++++++++++++++++++++++ point-cloud-colorize_wip/pdal_colorize.py | 201 ++++++++++ point_cloud_colorize/las_colorize.py | 41 ++- point_cloud_colorize/pdal_colorize.py | 10 +- 6 files changed, 673 insertions(+), 21 deletions(-) create mode 100644 pipeline.json create mode 100644 point-cloud-colorize_wip/las_colorize.py create mode 100644 point-cloud-colorize_wip/pdal_colorize.py diff --git a/.gitignore b/.gitignore index 781a3b6..3ec54f8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ debug/ *.laz *.las -.*sh -.gitignore +*.sh diff --git a/pipeline.json b/pipeline.json new file mode 100644 index 0000000..0419877 --- /dev/null +++ b/pipeline.json @@ -0,0 +1,9 @@ +{ + "pipeline": [ + "/var/data/arnot/div_color/tmp_col/*.laz", + { + "type" : "filters.merge" + }, + "/var/data/arnot/div_color/c_10cn2_color.laz" + ] +} diff --git a/point-cloud-colorize_wip/las_colorize.py b/point-cloud-colorize_wip/las_colorize.py new file mode 100644 index 0000000..48d1dd9 --- /dev/null +++ b/point-cloud-colorize_wip/las_colorize.py @@ -0,0 +1,430 @@ +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas +""" + +import argparse +import json +from pathlib import Path +import shutil +import pdal +import datetime + + +PDAL_PIPELINE = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.python", + "script": "{directory}/pdal_colorize.py", + "function": "las_colorize", + "module": "anything", + "pdalargs": "{pdalargs}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_file}" + }} + ] +}}""" + + +def run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size): + """ + Run the pdal pipeline using the given arguments. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + """ + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') + path = Path(__file__) + pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), + output_file=output_path.as_posix(), + srs=las_srs, + pdalargs=pdalargs_str, + directory=path.parent.as_posix()) + pipeline = pdal.Pipeline(pipeline_json) + pipeline.validate() + pipeline.execute() + + +def process_divided_files(input, output, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, + wms_pixel_size, wms_max_image_size, + verbose): + """ + Runs 3 pipelines, + dividing the pc in 16 smaller pcs, + loops over them to color each one seperately, + merges them together. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + + # todo: make it handle directories + print('colorizing parts') + print(datetime.datetime.now().isoformat()) + output_path = Path(output) + input_path = Path(input) + tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) + tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) + + for p in [tmp_div_path, tmp_col_path]: + if p.exists(): + shutil.rmtree(p) + p.mkdir(parents=True, exist_ok=True) + + divide_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.divider", + "count": "16" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_path}/tmp_#.laz" + }} + ]}}""" + + merge_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type":"filters.merge" + }}, + {{ + "type":"writers.las", + "a_srs":"{srs}", + "filename":"{output_path}" + }} + ]}}""" + if input_path.is_dir() and output_path.is_dir(): + for in_file in input_path.iterdir(): + if in_file.suffix == '.las' or in_file.suffix == '.laz': + # create pipeline for dividing the las + div_pipeline_json = divide_pipeline.format(input_file=in_file.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print('las is divided') + print(datetime.datetime.now().isoformat()) + + # for each of the created las-parts + for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): + + # color the las just as the normal las would be colored + tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + + run_pdal(Path(f), tmp_col, + las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') + # merge all the colored lasses + merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), + srs=las_srs.replace('"', '\\"'), + output_path=output_path.as_posix()) + pipeline = pdal.Pipeline(merge_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print(datetime.datetime.now().isoformat()) + print('files merged') + print('process finished') + + else: + print(f'{in_file} is not a las or a laz file') + elif not output_path.is_dir() and not input_path.is_dir(): + # create pipeline for dividing the las + div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print('las is divided') + print(datetime.datetime.now().isoformat()) + + # for each of the created las-parts + for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): + + # color the las just as the normal las would be colored + tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + + run_pdal(Path(f), tmp_col, + las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') + # merge all the colored lasses + merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), + srs=las_srs.replace('"', '\\"'), + output_path=output_path.as_posix()) + pipeline = pdal.Pipeline(merge_pipeline_json) + pipeline.validate() + pipeline.execute() + if verbose: + print(datetime.datetime.now().isoformat()) + print('files merged') + print('process finished') + else: + print('input and output are not both folders or both files.') + + # :todo remove temporary directories + + +def process_files(input_path, output_path, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size, verbose=False): + """ + Run the pdal pipeline for the input files. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + input_path = Path(input_path) + output_path = Path(output_path) + + if input_path.is_dir(): + for f in input_path.iterdir(): + if f.suffix == '.las' or f.suffix == '.laz': + + if output_path.is_dir(): + out = output_path / '{}_color{}'.format(f.stem, f.suffix) + else: + raise ValueError('Output path should be a directory if ' + 'the input path is a directory.') + + if verbose: + print('Colorizing {} ..'.format(f)) + print('Saving at {}.'.format(out)) + + run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + else: + if verbose: + print('Colorizing {} ..'.format(input_path)) + + if output_path.suffix == '.las' or output_path.suffix == '.laz': + if verbose: + print('Saving at {}.'.format(output_path)) + + run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + elif output_path.is_dir(): + out = output_path / '{}_color{}'.format(input_path.stem, + input_path.suffix) + + if verbose: + print('Saving at {}.'.format(out)) + + run_pdal(input_path, out, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + else: + raise ValueError('Specified output path not a LAS/LAZ file or ' + 'existing directory.') + + +def argument_parser(): + """ + Define and return the arguments. + """ + description = ('Colorize a las or laz file with a WMS service. ' + 'By default uses PDOK aerial photography.') + parser = argparse.ArgumentParser(description=description) + required_named = parser.add_argument_group('required named arguments') + required_named.add_argument('-i', '--input', + help='The input LAS/LAZ file or folder.', + required=True) + required_named.add_argument('-o', '--output', + help=('The output colorized LAS/LAZ ' + 'file or folder.'), + required=True) + parser.add_argument('-s', '--las_srs', + help=('The spatial reference system of the LAS data. ' + '(str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-w', '--wms_url', + help=('The url of the WMS service to use. ' + '(str, default: https://geodata. ' + 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), + required=False, + default=('https://geodata.nationaalgeoregister.nl' + '/luchtfoto/rgb/wms?')) + parser.add_argument('-l', '--wms_layer', + help=('The layer of the WMS service to use. ' + '(str, default: Actueel_ortho25)'), + required=False, + default='Actueel_ortho25') + parser.add_argument('-r', '--wms_srs', + help=('The spatial reference system of the WMS data ' + 'to request. (str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-f', '--wms_format', + help=('The image format of the WMS data to request. ' + '(str, default: image/png)'), + required=False, + default='image/png') + parser.add_argument('-v', '--wms_version', + help=('The version number of the WMS service. ' + '(str, default: 1.3.0)'), + required=False, + default='1.3.0') + parser.add_argument('-p', '--wms_pixel_size', + help=('The approximate desired pixel size of the ' + 'requested image. (float, default: 0.25)'), + required=False, + default=0.25) + parser.add_argument('-m', '--wms_max_image_size', + help=('The maximum size (in pixels) of the largest ' + 'side of the requested image. ' + '(int, default: 1000)'), + required=False, + default=1000) + parser.add_argument('-d', '--divide', + default=5, + action = "store_true", + help='Divide the point cloud in a given number of ' + 'smaller areas which are colored seperately') + parser.add_argument('-V', '--verbose', + default=False, + action="store_true", + help='Set verbose.') + args = parser.parse_args() + return args + + +def main(): + """ + Run the application. + """ + args = argument_parser() + if args.divide: + process_divided_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + else: + process_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + + +if __name__ == '__main__': + main() diff --git a/point-cloud-colorize_wip/pdal_colorize.py b/point-cloud-colorize_wip/pdal_colorize.py new file mode 100644 index 0000000..60a9d60 --- /dev/null +++ b/point-cloud-colorize_wip/pdal_colorize.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +""" +Python3 +@author: Chris Lucas +""" + +from io import BytesIO +import math +import numpy as np +import matplotlib.image as mpimg +import pyproj +from owslib.wms import WebMapService +from requests.exceptions import ReadTimeout + + +def request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries): + """ + Request an image from a WMS. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + size : list of int + The size of the image to be requested in pixels. [x, y] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + for i in range(retries): + try: + wms = WebMapService(wms_url, version=wms_version) + wms_img = wms.getmap(layers=[wms_layer], + srs=wms_srs, + bbox=bbox, + size=size, + format=wms_format, + transparent=True) + break + except ReadTimeout as e: + if i != retries-1: + print("ReadTimeout, trying again..") + else: + raise e + + img = mpimg.imread(BytesIO(wms_img.read()), 0) + + return img + + +def image_size(bbox, pixel_size=0.25): + """ + Compute the size of the image to be requested in pixels based on the + bounding box and the pixel size. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + pixel_size : float + The desired pixel size of the requested image. + Returns + ------- + img_size : tuple of int + The size of the image to be requested in pixels. (x, y) + """ + dif_x = bbox[2] - bbox[0] + dif_y = bbox[3] - bbox[1] + aspect_ratio = dif_x / dif_y + resolution = int(dif_x * (1/pixel_size)) + img_size = (resolution, int(resolution / aspect_ratio)) + + return img_size + + +def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, + wms_format, pixel_size, max_image_size, retries=10): + """ + Retrieve the imagery from a WMS service for the given bounding box. + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + pixel_size : float + The desired pixel size of the requested image. + max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + [xmin, ymin, xmax, ymax] = bbox + + x_range = xmax - xmin + y_range = ymax - ymin + longest_side = max([x_range, y_range]) + + if (longest_side * (1/pixel_size) > max_image_size): + length = max_image_size / (1/pixel_size) + length_pixels = max_image_size + + rows = int(math.ceil((ymax-ymin)/length)) + cols = int(math.ceil((xmax-xmin)/length)) + + img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) + + for col in range(cols): + for row in range(rows): + cell = [xmin+col*length, ymin+row*length, + xmin+(col+1)*length, ymin+(row+1)*length] + + img_part = request_image(cell, (length_pixels, length_pixels), + wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + img[((length_pixels*rows)-(row+1) * + length_pixels):(length_pixels*rows)-row*length_pixels, + col*length_pixels:(col+1)*length_pixels] = img_part + + img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, + :int(round(x_range*(1/pixel_size)))] + else: + size = image_size(bbox) + img = request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + return img + + +def las_colorize(ins, outs): + """ + PDAL python function. Adds RGB information to a LAS file by downloading + an orthophoto from a WMS service. + Parameters + ---------- + ins : PDAL input + outs : PDAL output + """ + X = ins['X'] + Y = ins['Y'] + + [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] + + if pdalargs['las_srs'] != pdalargs['wms_srs']: + p1 = pyproj.Proj(init=pdalargs['las_srs']) + p2 = pyproj.Proj(init=pdalargs['wms_srs']) + + bbox = [0, 0, 0, 0] + bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) + bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) + else: + bbox = [xmin, ymin, xmax, ymax] + + img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], + pdalargs['wms_srs'], pdalargs['wms_version'], + pdalargs['wms_format'], + float(pdalargs['wms_pixel_size']), + int(pdalargs['wms_max_image_size'])) + + img_size = img.shape[:2] + + x_img = np.round(((X - xmin) / (xmax-xmin)) * + (img_size[1]-1)).astype(int) + y_img = np.round(((ymax - Y) / (ymax-ymin)) * + (img_size[0]-1)).astype(int) + + rgb = img[y_img, x_img] * 255 + + outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) + outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) + outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) + + return True \ No newline at end of file diff --git a/point_cloud_colorize/las_colorize.py b/point_cloud_colorize/las_colorize.py index d3cf733..399722b 100644 --- a/point_cloud_colorize/las_colorize.py +++ b/point_cloud_colorize/las_colorize.py @@ -11,6 +11,7 @@ import shutil import pdal import datetime +from joblib import Parallel, delayed PDAL_PIPELINE = """{{ @@ -86,6 +87,17 @@ def run_pdal(input_path, output_path, las_srs, wms_url, pipeline.validate() pipeline.execute() +def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col = 'tmp_col_{}.laz'): + tmp_col = Path(tmp_col_path.joinpath(tmp_col.format(i))) + run_pdal(Path(f), tmp_col, + pdalargs['las_srs'],pdalargs['wms_url'], + pdalargs['wms_layer'], pdalargs['wms_srs'], + pdalargs['wms_version'] , pdalargs['wms_format'], + pdalargs['wms_pixel_size'],pdalargs['wms_max_image_size']) + + if verbose: + print(datetime.datetime.now().isoformat()) + print(f'colored {i} parts of the las') def process_files_parallel(input, output, las_srs, wms_url, wms_layer, wms_srs, @@ -119,7 +131,7 @@ def process_files_parallel(input, output, las_srs, }}, {{ "type": "filters.divider", - "count": "16" + "count": "6" }}, {{ "type": "writers.las", @@ -147,7 +159,6 @@ def process_files_parallel(input, output, las_srs, # create pipeline for dividing the las - div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), srs=las_srs, output_path=tmp_div_path.as_posix()) @@ -159,19 +170,21 @@ def process_files_parallel(input, output, las_srs, print('las is divided') print(datetime.datetime.now().isoformat()) - # for each of the created las-parts [PARRALEL] - for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): #todo: parrelelize - - # color the las just as the normal las would be colored - tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} - run_pdal(Path(f), tmp_col, - las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') + # for each of the created las-parts + print('start parallel processing') + print(datetime.datetime.now().isoformat()) + Parallel(n_jobs=1)(delayed(parallel_coloring)(f, i, verbose, tmp_col_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) + print('finished parallel processing') + print(datetime.datetime.now().isoformat()) # merge all the colored lasses merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), srs=las_srs.replace('"', '\\"'), diff --git a/point_cloud_colorize/pdal_colorize.py b/point_cloud_colorize/pdal_colorize.py index 60a9d60..50ef352 100644 --- a/point_cloud_colorize/pdal_colorize.py +++ b/point_cloud_colorize/pdal_colorize.py @@ -194,8 +194,8 @@ def las_colorize(ins, outs): rgb = img[y_img, x_img] * 255 - outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) - outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) - outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) - - return True \ No newline at end of file + outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) >>8 + outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) >> 8 + outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) >> 8 + outs['Intensity'] = ins['Intensity'] >> 8 + return True From 9c14b261153ad593165d7393b8638cdcd46e8ac5 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 08:54:39 +0100 Subject: [PATCH 15/24] parts are now colored in parallel --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3ec54f8..d114197 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ debug/ *.laz *.las *.sh - +/point-cloud-colorize/* From 205808eb710d1ac40f51c87711869eb9d0918542 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 08:55:16 +0100 Subject: [PATCH 16/24] parts are now colored in parallel --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d114197..bbdc2b0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ debug/ *.laz *.las *.sh -/point-cloud-colorize/* +point-cloud-colorize/* From 2574c8f93034be788825515f116660eb4d38dc43 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 08:56:06 +0100 Subject: [PATCH 17/24] parts are now colored in parallel --- point-cloud-colorize_wip/las_colorize.py | 430 ---------------------- point-cloud-colorize_wip/pdal_colorize.py | 201 ---------- 2 files changed, 631 deletions(-) delete mode 100644 point-cloud-colorize_wip/las_colorize.py delete mode 100644 point-cloud-colorize_wip/pdal_colorize.py diff --git a/point-cloud-colorize_wip/las_colorize.py b/point-cloud-colorize_wip/las_colorize.py deleted file mode 100644 index 48d1dd9..0000000 --- a/point-cloud-colorize_wip/las_colorize.py +++ /dev/null @@ -1,430 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -import argparse -import json -from pathlib import Path -import shutil -import pdal -import datetime - - -PDAL_PIPELINE = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.python", - "script": "{directory}/pdal_colorize.py", - "function": "las_colorize", - "module": "anything", - "pdalargs": "{pdalargs}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_file}" - }} - ] -}}""" - - -def run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size): - """ - Run the pdal pipeline using the given arguments. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - """ - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - - pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - path = Path(__file__) - pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), - output_file=output_path.as_posix(), - srs=las_srs, - pdalargs=pdalargs_str, - directory=path.parent.as_posix()) - pipeline = pdal.Pipeline(pipeline_json) - pipeline.validate() - pipeline.execute() - - -def process_divided_files(input, output, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, - wms_pixel_size, wms_max_image_size, - verbose): - """ - Runs 3 pipelines, - dividing the pc in 16 smaller pcs, - loops over them to color each one seperately, - merges them together. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - - # todo: make it handle directories - print('colorizing parts') - print(datetime.datetime.now().isoformat()) - output_path = Path(output) - input_path = Path(input) - tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) - tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) - - for p in [tmp_div_path, tmp_col_path]: - if p.exists(): - shutil.rmtree(p) - p.mkdir(parents=True, exist_ok=True) - - divide_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.divider", - "count": "16" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_path}/tmp_#.laz" - }} - ]}}""" - - merge_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type":"filters.merge" - }}, - {{ - "type":"writers.las", - "a_srs":"{srs}", - "filename":"{output_path}" - }} - ]}}""" - if input_path.is_dir() and output_path.is_dir(): - for in_file in input_path.iterdir(): - if in_file.suffix == '.las' or in_file.suffix == '.laz': - # create pipeline for dividing the las - div_pipeline_json = divide_pipeline.format(input_file=in_file.as_posix(), - srs=las_srs, - output_path=tmp_div_path.as_posix()) - - pipeline = pdal.Pipeline(div_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print('las is divided') - print(datetime.datetime.now().isoformat()) - - # for each of the created las-parts - for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): - - # color the las just as the normal las would be colored - tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) - - run_pdal(Path(f), tmp_col, - las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') - # merge all the colored lasses - merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), - srs=las_srs.replace('"', '\\"'), - output_path=output_path.as_posix()) - pipeline = pdal.Pipeline(merge_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print(datetime.datetime.now().isoformat()) - print('files merged') - print('process finished') - - else: - print(f'{in_file} is not a las or a laz file') - elif not output_path.is_dir() and not input_path.is_dir(): - # create pipeline for dividing the las - div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), - srs=las_srs, - output_path=tmp_div_path.as_posix()) - - pipeline = pdal.Pipeline(div_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print('las is divided') - print(datetime.datetime.now().isoformat()) - - # for each of the created las-parts - for i, f in enumerate(Path(tmp_div_path).iterdir(), 1): - - # color the las just as the normal las would be colored - tmp_col = Path(tmp_col_path.joinpath(f'tmp_col_{i}.laz')) - - run_pdal(Path(f), tmp_col, - las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') - # merge all the colored lasses - merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), - srs=las_srs.replace('"', '\\"'), - output_path=output_path.as_posix()) - pipeline = pdal.Pipeline(merge_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print(datetime.datetime.now().isoformat()) - print('files merged') - print('process finished') - else: - print('input and output are not both folders or both files.') - - # :todo remove temporary directories - - -def process_files(input_path, output_path, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size, verbose=False): - """ - Run the pdal pipeline for the input files. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - input_path = Path(input_path) - output_path = Path(output_path) - - if input_path.is_dir(): - for f in input_path.iterdir(): - if f.suffix == '.las' or f.suffix == '.laz': - - if output_path.is_dir(): - out = output_path / '{}_color{}'.format(f.stem, f.suffix) - else: - raise ValueError('Output path should be a directory if ' - 'the input path is a directory.') - - if verbose: - print('Colorizing {} ..'.format(f)) - print('Saving at {}.'.format(out)) - - run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - else: - if verbose: - print('Colorizing {} ..'.format(input_path)) - - if output_path.suffix == '.las' or output_path.suffix == '.laz': - if verbose: - print('Saving at {}.'.format(output_path)) - - run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - elif output_path.is_dir(): - out = output_path / '{}_color{}'.format(input_path.stem, - input_path.suffix) - - if verbose: - print('Saving at {}.'.format(out)) - - run_pdal(input_path, out, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - else: - raise ValueError('Specified output path not a LAS/LAZ file or ' - 'existing directory.') - - -def argument_parser(): - """ - Define and return the arguments. - """ - description = ('Colorize a las or laz file with a WMS service. ' - 'By default uses PDOK aerial photography.') - parser = argparse.ArgumentParser(description=description) - required_named = parser.add_argument_group('required named arguments') - required_named.add_argument('-i', '--input', - help='The input LAS/LAZ file or folder.', - required=True) - required_named.add_argument('-o', '--output', - help=('The output colorized LAS/LAZ ' - 'file or folder.'), - required=True) - parser.add_argument('-s', '--las_srs', - help=('The spatial reference system of the LAS data. ' - '(str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-w', '--wms_url', - help=('The url of the WMS service to use. ' - '(str, default: https://geodata. ' - 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), - required=False, - default=('https://geodata.nationaalgeoregister.nl' - '/luchtfoto/rgb/wms?')) - parser.add_argument('-l', '--wms_layer', - help=('The layer of the WMS service to use. ' - '(str, default: Actueel_ortho25)'), - required=False, - default='Actueel_ortho25') - parser.add_argument('-r', '--wms_srs', - help=('The spatial reference system of the WMS data ' - 'to request. (str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-f', '--wms_format', - help=('The image format of the WMS data to request. ' - '(str, default: image/png)'), - required=False, - default='image/png') - parser.add_argument('-v', '--wms_version', - help=('The version number of the WMS service. ' - '(str, default: 1.3.0)'), - required=False, - default='1.3.0') - parser.add_argument('-p', '--wms_pixel_size', - help=('The approximate desired pixel size of the ' - 'requested image. (float, default: 0.25)'), - required=False, - default=0.25) - parser.add_argument('-m', '--wms_max_image_size', - help=('The maximum size (in pixels) of the largest ' - 'side of the requested image. ' - '(int, default: 1000)'), - required=False, - default=1000) - parser.add_argument('-d', '--divide', - default=5, - action = "store_true", - help='Divide the point cloud in a given number of ' - 'smaller areas which are colored seperately') - parser.add_argument('-V', '--verbose', - default=False, - action="store_true", - help='Set verbose.') - args = parser.parse_args() - return args - - -def main(): - """ - Run the application. - """ - args = argument_parser() - if args.divide: - process_divided_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - else: - process_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - - -if __name__ == '__main__': - main() diff --git a/point-cloud-colorize_wip/pdal_colorize.py b/point-cloud-colorize_wip/pdal_colorize.py deleted file mode 100644 index 60a9d60..0000000 --- a/point-cloud-colorize_wip/pdal_colorize.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python3 -@author: Chris Lucas -""" - -from io import BytesIO -import math -import numpy as np -import matplotlib.image as mpimg -import pyproj -from owslib.wms import WebMapService -from requests.exceptions import ReadTimeout - - -def request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries): - """ - Request an image from a WMS. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - size : list of int - The size of the image to be requested in pixels. [x, y] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - for i in range(retries): - try: - wms = WebMapService(wms_url, version=wms_version) - wms_img = wms.getmap(layers=[wms_layer], - srs=wms_srs, - bbox=bbox, - size=size, - format=wms_format, - transparent=True) - break - except ReadTimeout as e: - if i != retries-1: - print("ReadTimeout, trying again..") - else: - raise e - - img = mpimg.imread(BytesIO(wms_img.read()), 0) - - return img - - -def image_size(bbox, pixel_size=0.25): - """ - Compute the size of the image to be requested in pixels based on the - bounding box and the pixel size. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - pixel_size : float - The desired pixel size of the requested image. - Returns - ------- - img_size : tuple of int - The size of the image to be requested in pixels. (x, y) - """ - dif_x = bbox[2] - bbox[0] - dif_y = bbox[3] - bbox[1] - aspect_ratio = dif_x / dif_y - resolution = int(dif_x * (1/pixel_size)) - img_size = (resolution, int(resolution / aspect_ratio)) - - return img_size - - -def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, - wms_format, pixel_size, max_image_size, retries=10): - """ - Retrieve the imagery from a WMS service for the given bounding box. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - pixel_size : float - The desired pixel size of the requested image. - max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - [xmin, ymin, xmax, ymax] = bbox - - x_range = xmax - xmin - y_range = ymax - ymin - longest_side = max([x_range, y_range]) - - if (longest_side * (1/pixel_size) > max_image_size): - length = max_image_size / (1/pixel_size) - length_pixels = max_image_size - - rows = int(math.ceil((ymax-ymin)/length)) - cols = int(math.ceil((xmax-xmin)/length)) - - img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) - - for col in range(cols): - for row in range(rows): - cell = [xmin+col*length, ymin+row*length, - xmin+(col+1)*length, ymin+(row+1)*length] - - img_part = request_image(cell, (length_pixels, length_pixels), - wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - img[((length_pixels*rows)-(row+1) * - length_pixels):(length_pixels*rows)-row*length_pixels, - col*length_pixels:(col+1)*length_pixels] = img_part - - img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, - :int(round(x_range*(1/pixel_size)))] - else: - size = image_size(bbox) - img = request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - return img - - -def las_colorize(ins, outs): - """ - PDAL python function. Adds RGB information to a LAS file by downloading - an orthophoto from a WMS service. - Parameters - ---------- - ins : PDAL input - outs : PDAL output - """ - X = ins['X'] - Y = ins['Y'] - - [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] - - if pdalargs['las_srs'] != pdalargs['wms_srs']: - p1 = pyproj.Proj(init=pdalargs['las_srs']) - p2 = pyproj.Proj(init=pdalargs['wms_srs']) - - bbox = [0, 0, 0, 0] - bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) - bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) - else: - bbox = [xmin, ymin, xmax, ymax] - - img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], - pdalargs['wms_srs'], pdalargs['wms_version'], - pdalargs['wms_format'], - float(pdalargs['wms_pixel_size']), - int(pdalargs['wms_max_image_size'])) - - img_size = img.shape[:2] - - x_img = np.round(((X - xmin) / (xmax-xmin)) * - (img_size[1]-1)).astype(int) - y_img = np.round(((ymax - Y) / (ymax-ymin)) * - (img_size[0]-1)).astype(int) - - rgb = img[y_img, x_img] * 255 - - outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) - outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) - outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) - - return True \ No newline at end of file From 796208f4e288e694b64b6dd12be719ea63fb6104 Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Fri, 8 Mar 2019 14:58:59 +0100 Subject: [PATCH 18/24] parallel is working, improved verbose --- .gitignore | 1 + point_cloud_colorize/las_colorize.py | 61 ++++++++-------------------- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index bbdc2b0..3187b05 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ debug/ *.las *.sh point-cloud-colorize/* +get_year.sql diff --git a/point_cloud_colorize/las_colorize.py b/point_cloud_colorize/las_colorize.py index 399722b..7fc5c50 100644 --- a/point_cloud_colorize/las_colorize.py +++ b/point_cloud_colorize/las_colorize.py @@ -96,8 +96,7 @@ def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col = 'tmp_col_ pdalargs['wms_pixel_size'],pdalargs['wms_max_image_size']) if verbose: - print(datetime.datetime.now().isoformat()) - print(f'colored {i} parts of the las') + print(f'colored {i} parts of the las at {datetime.datetime.now()}') def process_files_parallel(input, output, las_srs, wms_url, wms_layer, wms_srs, @@ -111,17 +110,18 @@ def process_files_parallel(input, output, las_srs, :param verbose: :return: """ - print('colorizing parts') - print(datetime.datetime.now().isoformat()) + if verbose: + print(f'started colorizing parts at {datetime.datetime.now()}') output_path = Path(output) + if not output_path.is_dir(): + raise ValueError('Output should be a directory') + input_path = Path(input) tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) - tmp_col_path = Path(output_path.parent.joinpath('tmp_col')) - for p in [tmp_div_path, tmp_col_path]: - if p.exists(): - shutil.rmtree(p) - p.mkdir(parents=True, exist_ok=True) + if tmp_div_path.exists(): + shutil.rmtree(tmp_div_path) + tmp_div_path.mkdir(parents=True, exist_ok=True) divide_pipeline = """{{ "pipeline":[ @@ -140,24 +140,6 @@ def process_files_parallel(input, output, las_srs, }} ]}}""" - - merge_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type":"filters.merge" - }}, - {{ - "type":"writers.las", - "a_srs":"{srs}", - "filename":"{output_path}" - }} - ]}}""" - - # create pipeline for dividing the las div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), srs=las_srs, @@ -167,8 +149,7 @@ def process_files_parallel(input, output, las_srs, pipeline.validate() pipeline.execute() if verbose: - print('las is divided') - print(datetime.datetime.now().isoformat()) + print(f'las is divided at {datetime.datetime.now()}') pdalargs = {'wms_url': wms_url, 'wms_layer': wms_layer, @@ -180,22 +161,14 @@ def process_files_parallel(input, output, las_srs, 'las_srs': las_srs} # for each of the created las-parts - print('start parallel processing') - print(datetime.datetime.now().isoformat()) - Parallel(n_jobs=1)(delayed(parallel_coloring)(f, i, verbose, tmp_col_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) - print('finished parallel processing') - print(datetime.datetime.now().isoformat()) - # merge all the colored lasses - merge_pipeline_json = merge_pipeline.format(input_file=tmp_col_path.joinpath('*').as_posix(), - srs=las_srs.replace('"', '\\"'), - output_path=output_path.as_posix()) - pipeline = pdal.Pipeline(merge_pipeline_json) - pipeline.validate() - pipeline.execute() if verbose: - print(datetime.datetime.now().isoformat()) - print('files merged') - print('process finished') + print(f'start parallel processing at {datetime.datetime.now()}') + + Parallel(n_jobs=6)(delayed(parallel_coloring)(f, i, verbose, output_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) + + if verbose: + print(f'finished parallel processing at {datetime.datetime.now()}') + print(f'colorizing in parts finished at {datetime.datetime.now()}') def process_files(input_path, output_path, las_srs, From 1d480d7fa181d54d34acc90f1a14c79617132d3e Mon Sep 17 00:00:00 2001 From: Arno Timmer Date: Thu, 22 Aug 2019 10:30:12 +0200 Subject: [PATCH 19/24] fix for __file__ --- point_cloud_colorize/las_colorize.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/point_cloud_colorize/las_colorize.py b/point_cloud_colorize/las_colorize.py index 7fc5c50..5be8e72 100644 --- a/point_cloud_colorize/las_colorize.py +++ b/point_cloud_colorize/las_colorize.py @@ -77,12 +77,15 @@ def run_pdal(input_path, output_path, las_srs, wms_url, 'las_srs': las_srs} pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - path = Path(__file__) + # path = Path(__file__) + path = Path(os.getcwd()) + print('{}'.format(path.parent.as_posix())) pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), output_file=output_path.as_posix(), srs=las_srs, pdalargs=pdalargs_str, - directory=path.parent.as_posix()) + # directory=path.parent.as_posix()) + directory=path.as_posix()) pipeline = pdal.Pipeline(pipeline_json) pipeline.validate() pipeline.execute() From e9a4b3e0d0e5fba0e6f3241c0244c6d1cf18fd02 Mon Sep 17 00:00:00 2001 From: Arno Date: Tue, 15 Oct 2019 10:09:07 +0200 Subject: [PATCH 20/24] added parralel coloring correctly --- .gitignore | 7 +- pipeline.json | 9 - point_cloud_colorize/las_colorize.py | 695 +++++++++++++------------- point_cloud_colorize/pdal_colorize.py | 410 +++++++-------- 4 files changed, 557 insertions(+), 564 deletions(-) delete mode 100644 pipeline.json diff --git a/.gitignore b/.gitignore index 3187b05..3583243 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,3 @@ .vscode/ .spyproject/ -debug/ -*.laz -*.las -*.sh -point-cloud-colorize/* -get_year.sql +debug/ \ No newline at end of file diff --git a/pipeline.json b/pipeline.json deleted file mode 100644 index 0419877..0000000 --- a/pipeline.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "pipeline": [ - "/var/data/arnot/div_color/tmp_col/*.laz", - { - "type" : "filters.merge" - }, - "/var/data/arnot/div_color/c_10cn2_color.laz" - ] -} diff --git a/point_cloud_colorize/las_colorize.py b/point_cloud_colorize/las_colorize.py index 5be8e72..48b5290 100644 --- a/point_cloud_colorize/las_colorize.py +++ b/point_cloud_colorize/las_colorize.py @@ -1,348 +1,347 @@ -# -*- coding: utf-8 -*- -""" -Python3 - -@author: Chris Lucas -""" - -import argparse -import json -from pathlib import Path -import shutil -import pdal -import datetime -from joblib import Parallel, delayed - - -PDAL_PIPELINE = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.python", - "script": "{directory}/pdal_colorize.py", - "function": "las_colorize", - "module": "anything", - "pdalargs": "{pdalargs}" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_file}" - }} - ] -}}""" - - -def run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size): - """ - Run the pdal pipeline using the given arguments. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - """ - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - - pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') - # path = Path(__file__) - path = Path(os.getcwd()) - print('{}'.format(path.parent.as_posix())) - pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), - output_file=output_path.as_posix(), - srs=las_srs, - pdalargs=pdalargs_str, - # directory=path.parent.as_posix()) - directory=path.as_posix()) - pipeline = pdal.Pipeline(pipeline_json) - pipeline.validate() - pipeline.execute() - -def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col = 'tmp_col_{}.laz'): - tmp_col = Path(tmp_col_path.joinpath(tmp_col.format(i))) - run_pdal(Path(f), tmp_col, - pdalargs['las_srs'],pdalargs['wms_url'], - pdalargs['wms_layer'], pdalargs['wms_srs'], - pdalargs['wms_version'] , pdalargs['wms_format'], - pdalargs['wms_pixel_size'],pdalargs['wms_max_image_size']) - - if verbose: - print(f'colored {i} parts of the las at {datetime.datetime.now()}') - -def process_files_parallel(input, output, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, - wms_pixel_size, wms_max_image_size, - verbose): - """ - :param input_path: - :param output_path: - :param las_srs: - :param verbose: - :return: - """ - if verbose: - print(f'started colorizing parts at {datetime.datetime.now()}') - output_path = Path(output) - if not output_path.is_dir(): - raise ValueError('Output should be a directory') - - input_path = Path(input) - tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) - - if tmp_div_path.exists(): - shutil.rmtree(tmp_div_path) - tmp_div_path.mkdir(parents=True, exist_ok=True) - - divide_pipeline = """{{ - "pipeline":[ - {{ - "type": "readers.las", - "filename": "{input_file}" - }}, - {{ - "type": "filters.divider", - "count": "6" - }}, - {{ - "type": "writers.las", - "a_srs": "{srs}", - "filename": "{output_path}/tmp_#.laz" - }} - ]}}""" - - # create pipeline for dividing the las - div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), - srs=las_srs, - output_path=tmp_div_path.as_posix()) - - pipeline = pdal.Pipeline(div_pipeline_json) - pipeline.validate() - pipeline.execute() - if verbose: - print(f'las is divided at {datetime.datetime.now()}') - - pdalargs = {'wms_url': wms_url, - 'wms_layer': wms_layer, - 'wms_srs': wms_srs, - 'wms_version': wms_version, - 'wms_format': wms_format, - 'wms_pixel_size': wms_pixel_size, - 'wms_max_image_size': wms_max_image_size, - 'las_srs': las_srs} - - # for each of the created las-parts - if verbose: - print(f'start parallel processing at {datetime.datetime.now()}') - - Parallel(n_jobs=6)(delayed(parallel_coloring)(f, i, verbose, output_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) - - if verbose: - print(f'finished parallel processing at {datetime.datetime.now()}') - print(f'colorizing in parts finished at {datetime.datetime.now()}') - - -def process_files(input_path, output_path, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size, verbose=False): - """ - Run the pdal pipeline for the input files. - - Parameters - ---------- - input_path : str - The path to the input LAS/LAZ file or directory containing LAS/LAZ - files. - output_path : str - The path to the output LAS/LAZ file or directory. - las_srs : str - The spatial reference system of the LAS data. - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - wms_pixel_size : float - The approximate desired pixel size of the requested image. - wms_max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - verbose : bool - Set verbose. - """ - input_path = Path(input_path) - output_path = Path(output_path) - - if input_path.is_dir(): - for f in input_path.iterdir(): - if f.suffix == '.las' or f.suffix == '.laz': - - if output_path.is_dir(): - out = output_path / '{}_color{}'.format(f.stem, f.suffix) - else: - raise ValueError('Output path should be a directory if ' - 'the input path is a directory.') - - if verbose: - print('Colorizing {} ..'.format(f)) - print('Saving at {}.'.format(out)) - - run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, - wms_version, wms_format, wms_pixel_size, - wms_max_image_size) - else: - if verbose: - print('Colorizing {} ..'.format(input_path)) - - if output_path.suffix == '.las' or output_path.suffix == '.laz': - if verbose: - print('Saving at {}.'.format(output_path)) - - run_pdal(input_path, output_path, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - elif output_path.is_dir(): - out = output_path / '{}_color{}'.format(input_path.stem, - input_path.suffix) - - if verbose: - print('Saving at {}.'.format(out)) - - run_pdal(input_path, out, las_srs, wms_url, - wms_layer, wms_srs, wms_version, wms_format, - wms_pixel_size, wms_max_image_size) - else: - raise ValueError('Specified output path not a LAS/LAZ file or ' - 'existing directory.') - - -def argument_parser(): - """ - Define and return the arguments. - """ - description = ('Colorize a las or laz file with a WMS service. ' - 'By default uses PDOK aerial photography.') - parser = argparse.ArgumentParser(description=description) - required_named = parser.add_argument_group('required named arguments') - required_named.add_argument('-i', '--input', - help='The input LAS/LAZ file or folder.', - required=True) - required_named.add_argument('-o', '--output', - help=('The output colorized LAS/LAZ ' - 'file or folder.'), - required=True) - parser.add_argument('-s', '--las_srs', - help=('The spatial reference system of the LAS data. ' - '(str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-w', '--wms_url', - help=('The url of the WMS service to use. ' - '(str, default: https://geodata. ' - 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), - required=False, - default=('https://geodata.nationaalgeoregister.nl' - '/luchtfoto/rgb/wms?')) - parser.add_argument('-l', '--wms_layer', - help=('The layer of the WMS service to use. ' - '(str, default: Actueel_ortho25)'), - required=False, - default='Actueel_ortho25') - parser.add_argument('-r', '--wms_srs', - help=('The spatial reference system of the WMS data ' - 'to request. (str, default: EPSG:28992)'), - required=False, - default='EPSG:28992') - parser.add_argument('-f', '--wms_format', - help=('The image format of the WMS data to request. ' - '(str, default: image/png)'), - required=False, - default='image/png') - parser.add_argument('-v', '--wms_version', - help=('The version number of the WMS service. ' - '(str, default: 1.3.0)'), - required=False, - default='1.3.0') - parser.add_argument('-p', '--wms_pixel_size', - help=('The approximate desired pixel size of the ' - 'requested image. (float, default: 0.25)'), - required=False, - default=0.25) - parser.add_argument('-m', '--wms_max_image_size', - help=('The maximum size (in pixels) of the largest ' - 'side of the requested image. ' - '(int, default: 1000)'), - required=False, - default=1000) - parser.add_argument('-d', '--divide', - default=5, - action = "store_true", - help='Divide the point cloud in a given number of ' - 'smaller areas which are colored seperately') - parser.add_argument('-V', '--verbose', - default=False, - action="store_true", - help='Set verbose.') - args = parser.parse_args() - return args - - -def main(): - """ - Run the application. - """ - args = argument_parser() - if args.divide: - process_files_parallel(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - else: - process_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) - - -if __name__ == '__main__': - main() +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas, Arno Timmer +""" + +import argparse +import json +from pathlib import Path +import shutil +import pdal +import datetime +from joblib import Parallel, delayed + + +PDAL_PIPELINE = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.python", + "script": "{directory}/pdal_colorize.py", + "function": "las_colorize", + "module": "anything", + "pdalargs": "{pdalargs}" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_file}" + }} + ] +}}""" + + +def run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size): + """ + Run the pdal pipeline using the given arguments. + + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + """ + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + pdalargs_str = json.dumps(pdalargs).replace('"', '\\"') + + path = Path(os.getcwd()) + + pipeline_json = PDAL_PIPELINE.format(input_file=input_path.as_posix(), + output_file=output_path.as_posix(), + srs=las_srs, + pdalargs=pdalargs_str, + directory=path.as_posix()) + pipeline = pdal.Pipeline(pipeline_json) + pipeline.validate() + pipeline.execute() + +def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col = 'tmp_col_{}.laz'): + tmp_col = Path(tmp_col_path.joinpath(tmp_col.format(i))) + run_pdal(Path(f), tmp_col, + pdalargs['las_srs'],pdalargs['wms_url'], + pdalargs['wms_layer'], pdalargs['wms_srs'], + pdalargs['wms_version'] , pdalargs['wms_format'], + pdalargs['wms_pixel_size'],pdalargs['wms_max_image_size']) + + if verbose: + print(f'colored {i} parts of the las at {datetime.datetime.now()}') + +def process_files_parallel(input, output, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, + wms_pixel_size, wms_max_image_size, + verbose): + """ + :param input_path: + :param output_path: + :param las_srs: + :param verbose: + :return: + """ + if verbose: + print(f'started colorizing parts at {datetime.datetime.now()}') + output_path = Path(output) + if not output_path.is_dir(): + raise ValueError('Output should be a directory') + + input_path = Path(input) + tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) + + if tmp_div_path.exists(): + if verbose: + print('Temporary path exists, deleting.') + shutil.rmtree(tmp_div_path) + tmp_div_path.mkdir(parents=True, exist_ok=True) + + divide_pipeline = """{{ + "pipeline":[ + {{ + "type": "readers.las", + "filename": "{input_file}" + }}, + {{ + "type": "filters.divider", + "count": "6" + }}, + {{ + "type": "writers.las", + "a_srs": "{srs}", + "filename": "{output_path}/tmp_#.laz" + }} + ]}}""" + + # create pipeline for dividing the las + div_pipeline_json = divide_pipeline.format(input_file=input_path.as_posix(), + srs=las_srs, + output_path=tmp_div_path.as_posix()) + pipeline = pdal.Pipeline(div_pipeline_json) + pipeline.validate() + pipeline.execute() + + if verbose: + print(f'las is divided at {datetime.datetime.now()}') + + pdalargs = {'wms_url': wms_url, + 'wms_layer': wms_layer, + 'wms_srs': wms_srs, + 'wms_version': wms_version, + 'wms_format': wms_format, + 'wms_pixel_size': wms_pixel_size, + 'wms_max_image_size': wms_max_image_size, + 'las_srs': las_srs} + + # for each of the created las-parts + if verbose: + print(f'start parallel processing at {datetime.datetime.now()}') + + Parallel(n_jobs=6)(delayed(parallel_coloring)(f, i, verbose, output_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) + + if verbose: + print(f'colorizing in parts finished at {datetime.datetime.now()}') + + +def process_files(input_path, output_path, las_srs, + wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size, verbose=False): + """ + Run the pdal pipeline for the input files. + Parameters + ---------- + input_path : str + The path to the input LAS/LAZ file or directory containing LAS/LAZ + files. + output_path : str + The path to the output LAS/LAZ file or directory. + las_srs : str + The spatial reference system of the LAS data. + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + wms_pixel_size : float + The approximate desired pixel size of the requested image. + wms_max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + verbose : bool + Set verbose. + """ + input_path = Path(input_path) + output_path = Path(output_path) + + if input_path.is_dir(): + for f in input_path.iterdir(): + if f.suffix == '.las' or f.suffix == '.laz': + + if output_path.is_dir(): + out = output_path / '{}_color{}'.format(f.stem, f.suffix) + else: + raise ValueError('Output path should be a directory if ' + 'the input path is a directory.') + + if verbose: + print('Colorizing {} ..'.format(f)) + print('Saving at {}.'.format(out)) + + run_pdal(f, out, las_srs, wms_url, wms_layer, wms_srs, + wms_version, wms_format, wms_pixel_size, + wms_max_image_size) + else: + if verbose: + print('Colorizing {} ..'.format(input_path)) + + if output_path.suffix == '.las' or output_path.suffix == '.laz': + if verbose: + print('Saving at {}.'.format(output_path)) + + run_pdal(input_path, output_path, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + elif output_path.is_dir(): + out = output_path / '{}_color{}'.format(input_path.stem, + input_path.suffix) + + if verbose: + print('Saving at {}.'.format(out)) + + run_pdal(input_path, out, las_srs, wms_url, + wms_layer, wms_srs, wms_version, wms_format, + wms_pixel_size, wms_max_image_size) + else: + raise ValueError('Specified output path not a LAS/LAZ file or ' + 'existing directory.') + + +def argument_parser(): + """ + Define and return the arguments. + """ + description = ('Colorize a las or laz file with a WMS service. ' + 'By default uses PDOK aerial photography.') + parser = argparse.ArgumentParser(description=description) + required_named = parser.add_argument_group('required named arguments') + required_named.add_argument('-i', '--input', + help='The input LAS/LAZ file or folder.', + required=True) + required_named.add_argument('-o', '--output', + help=('The output colorized LAS/LAZ ' + 'file or folder.'), + required=True) + parser.add_argument('-s', '--las_srs', + help=('The spatial reference system of the LAS data. ' + '(str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-w', '--wms_url', + help=('The url of the WMS service to use. ' + '(str, default: https://geodata. ' + 'nationaalgeoregister.nl/luchtfoto/rgb/wms?)'), + required=False, + default=('https://geodata.nationaalgeoregister.nl' + '/luchtfoto/rgb/wms?')) + parser.add_argument('-l', '--wms_layer', + help=('The layer of the WMS service to use. ' + '(str, default: Actueel_ortho25)'), + required=False, + default='Actueel_ortho25') + parser.add_argument('-r', '--wms_srs', + help=('The spatial reference system of the WMS data ' + 'to request. (str, default: EPSG:28992)'), + required=False, + default='EPSG:28992') + parser.add_argument('-f', '--wms_format', + help=('The image format of the WMS data to request. ' + '(str, default: image/png)'), + required=False, + default='image/png') + parser.add_argument('-v', '--wms_version', + help=('The version number of the WMS service. ' + '(str, default: 1.3.0)'), + required=False, + default='1.3.0') + parser.add_argument('-p', '--wms_pixel_size', + help=('The approximate desired pixel size of the ' + 'requested image. (float, default: 0.25)'), + required=False, + default=0.25) + parser.add_argument('-m', '--wms_max_image_size', + help=('The maximum size (in pixels) of the largest ' + 'side of the requested image. ' + '(int, default: 1000)'), + required=False, + default=1000) + parser.add_argument('-d', '--divide', + default=5, + action = "store_true", + help='Divide the point cloud in a given number of ' + 'smaller areas which are colored seperately') + parser.add_argument('-V', '--verbose', + default=False, + action="store_true", + help='Set verbose.') + args = parser.parse_args() + return args + + +def main(): + """ + Run the application. + """ + args = argument_parser() + if args.divide: + process_files_parallel(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + else: + process_files(args.input, args.output, args.las_srs, + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) + + +if __name__ == '__main__': + main() diff --git a/point_cloud_colorize/pdal_colorize.py b/point_cloud_colorize/pdal_colorize.py index 50ef352..deea80e 100644 --- a/point_cloud_colorize/pdal_colorize.py +++ b/point_cloud_colorize/pdal_colorize.py @@ -1,201 +1,209 @@ -# -*- coding: utf-8 -*- -""" -Python3 -@author: Chris Lucas -""" - -from io import BytesIO -import math -import numpy as np -import matplotlib.image as mpimg -import pyproj -from owslib.wms import WebMapService -from requests.exceptions import ReadTimeout - - -def request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries): - """ - Request an image from a WMS. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - size : list of int - The size of the image to be requested in pixels. [x, y] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - for i in range(retries): - try: - wms = WebMapService(wms_url, version=wms_version) - wms_img = wms.getmap(layers=[wms_layer], - srs=wms_srs, - bbox=bbox, - size=size, - format=wms_format, - transparent=True) - break - except ReadTimeout as e: - if i != retries-1: - print("ReadTimeout, trying again..") - else: - raise e - - img = mpimg.imread(BytesIO(wms_img.read()), 0) - - return img - - -def image_size(bbox, pixel_size=0.25): - """ - Compute the size of the image to be requested in pixels based on the - bounding box and the pixel size. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - pixel_size : float - The desired pixel size of the requested image. - Returns - ------- - img_size : tuple of int - The size of the image to be requested in pixels. (x, y) - """ - dif_x = bbox[2] - bbox[0] - dif_y = bbox[3] - bbox[1] - aspect_ratio = dif_x / dif_y - resolution = int(dif_x * (1/pixel_size)) - img_size = (resolution, int(resolution / aspect_ratio)) - - return img_size - - -def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, - wms_format, pixel_size, max_image_size, retries=10): - """ - Retrieve the imagery from a WMS service for the given bounding box. - Parameters - ---------- - bbox : list of float - The coordinates of the bounding box. [xmin, ymin, xmax, ymax] - wms_url : str - The url of the WMS service to use. - wms_layer : str - The layer of the WMS service to use. - wms_srs : str - The spatial reference system of the WMS data to request. - wms_version : str - The image format of the WMS data to request. - wms_format : str - The version number of the WMS service. - pixel_size : float - The desired pixel size of the requested image. - max_image_size : int - The maximum size (in pixels) of the largest side of the requested - image. - retries : int - Amount of times to retry retrieving an image from the WMS if it - fails. - Returns - ------- - img : (MxNx3) array - The RGB values of each pixel - """ - [xmin, ymin, xmax, ymax] = bbox - - x_range = xmax - xmin - y_range = ymax - ymin - longest_side = max([x_range, y_range]) - - if (longest_side * (1/pixel_size) > max_image_size): - length = max_image_size / (1/pixel_size) - length_pixels = max_image_size - - rows = int(math.ceil((ymax-ymin)/length)) - cols = int(math.ceil((xmax-xmin)/length)) - - img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) - - for col in range(cols): - for row in range(rows): - cell = [xmin+col*length, ymin+row*length, - xmin+(col+1)*length, ymin+(row+1)*length] - - img_part = request_image(cell, (length_pixels, length_pixels), - wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - img[((length_pixels*rows)-(row+1) * - length_pixels):(length_pixels*rows)-row*length_pixels, - col*length_pixels:(col+1)*length_pixels] = img_part - - img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, - :int(round(x_range*(1/pixel_size)))] - else: - size = image_size(bbox) - img = request_image(bbox, size, wms_url, wms_layer, wms_srs, - wms_version, wms_format, retries) - - return img - - -def las_colorize(ins, outs): - """ - PDAL python function. Adds RGB information to a LAS file by downloading - an orthophoto from a WMS service. - Parameters - ---------- - ins : PDAL input - outs : PDAL output - """ - X = ins['X'] - Y = ins['Y'] - - [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] - - if pdalargs['las_srs'] != pdalargs['wms_srs']: - p1 = pyproj.Proj(init=pdalargs['las_srs']) - p2 = pyproj.Proj(init=pdalargs['wms_srs']) - - bbox = [0, 0, 0, 0] - bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) - bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) - else: - bbox = [xmin, ymin, xmax, ymax] - - img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], - pdalargs['wms_srs'], pdalargs['wms_version'], - pdalargs['wms_format'], - float(pdalargs['wms_pixel_size']), - int(pdalargs['wms_max_image_size'])) - - img_size = img.shape[:2] - - x_img = np.round(((X - xmin) / (xmax-xmin)) * - (img_size[1]-1)).astype(int) - y_img = np.round(((ymax - Y) / (ymax-ymin)) * - (img_size[0]-1)).astype(int) - - rgb = img[y_img, x_img] * 255 - - outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) >>8 - outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) >> 8 - outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) >> 8 - outs['Intensity'] = ins['Intensity'] >> 8 - return True +# -*- coding: utf-8 -*- +""" +Python3 + +@author: Chris Lucas +""" + +from io import BytesIO +import math +import numpy as np +import matplotlib.image as mpimg +import pyproj +from owslib.wms import WebMapService +from requests.exceptions import ReadTimeout + + +def request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries): + """ + Request an image from a WMS. + + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + size : list of int + The size of the image to be requested in pixels. [x, y] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + for i in range(retries): + try: + wms = WebMapService(wms_url, version=wms_version) + wms_img = wms.getmap(layers=[wms_layer], + srs=wms_srs, + bbox=bbox, + size=size, + format=wms_format, + transparent=True) + break + except ReadTimeout as e: + if i != retries-1: + print("ReadTimeout, trying again..") + else: + raise e + + img = mpimg.imread(BytesIO(wms_img.read()), 0) + + return img + + +def image_size(bbox, pixel_size=0.25): + """ + Compute the size of the image to be requested in pixels based on the + bounding box and the pixel size. + + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + pixel_size : float + The desired pixel size of the requested image. + + Returns + ------- + img_size : tuple of int + The size of the image to be requested in pixels. (x, y) + """ + dif_x = bbox[2] - bbox[0] + dif_y = bbox[3] - bbox[1] + aspect_ratio = dif_x / dif_y + resolution = int(dif_x * (1/pixel_size)) + img_size = (resolution, int(resolution / aspect_ratio)) + + return img_size + + +def retrieve_image(bbox, wms_url, wms_layer, wms_srs, wms_version, + wms_format, pixel_size, max_image_size, retries=10): + """ + Retrieve the imagery from a WMS service for the given bounding box. + + Parameters + ---------- + bbox : list of float + The coordinates of the bounding box. [xmin, ymin, xmax, ymax] + wms_url : str + The url of the WMS service to use. + wms_layer : str + The layer of the WMS service to use. + wms_srs : str + The spatial reference system of the WMS data to request. + wms_version : str + The image format of the WMS data to request. + wms_format : str + The version number of the WMS service. + pixel_size : float + The desired pixel size of the requested image. + max_image_size : int + The maximum size (in pixels) of the largest side of the requested + image. + retries : int + Amount of times to retry retrieving an image from the WMS if it + fails. + + Returns + ------- + img : (MxNx3) array + The RGB values of each pixel + """ + [xmin, ymin, xmax, ymax] = bbox + + x_range = xmax - xmin + y_range = ymax - ymin + longest_side = max([x_range, y_range]) + + if (longest_side * (1/pixel_size) > max_image_size): + length = max_image_size / (1/pixel_size) + length_pixels = max_image_size + + rows = int(math.ceil((ymax-ymin)/length)) + cols = int(math.ceil((xmax-xmin)/length)) + + img = np.zeros((length_pixels*rows, length_pixels*cols, 3)) + + for col in range(cols): + for row in range(rows): + cell = [xmin+col*length, ymin+row*length, + xmin+(col+1)*length, ymin+(row+1)*length] + + img_part = request_image(cell, (length_pixels, length_pixels), + wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + img[((length_pixels*rows)-(row+1) * + length_pixels):(length_pixels*rows)-row*length_pixels, + col*length_pixels:(col+1)*length_pixels] = img_part + + img = img[(length_pixels*rows)-int(y_range*(1/pixel_size)):, + :int(round(x_range*(1/pixel_size)))] + else: + size = image_size(bbox) + img = request_image(bbox, size, wms_url, wms_layer, wms_srs, + wms_version, wms_format, retries) + + return img + + +def las_colorize(ins, outs): + """ + PDAL python function. Adds RGB information to a LAS file by downloading + an orthophoto from a WMS service. + + Parameters + ---------- + ins : PDAL input + outs : PDAL output + """ + X = ins['X'] + Y = ins['Y'] + + [xmin, ymin, xmax, ymax] = [min(X), min(Y), max(X), max(Y)] + + if pdalargs['las_srs'] != pdalargs['wms_srs']: + p1 = pyproj.Proj(init=pdalargs['las_srs']) + p2 = pyproj.Proj(init=pdalargs['wms_srs']) + + bbox = [0, 0, 0, 0] + bbox[:2] = pyproj.transform(p1, p2, xmin, ymin) + bbox[2:] = pyproj.transform(p1, p2, xmax, ymax) + else: + bbox = [xmin, ymin, xmax, ymax] + + img = retrieve_image(bbox, pdalargs['wms_url'], pdalargs['wms_layer'], + pdalargs['wms_srs'], pdalargs['wms_version'], + pdalargs['wms_format'], + float(pdalargs['wms_pixel_size']), + int(pdalargs['wms_max_image_size'])) + + img_size = img.shape[:2] + + x_img = np.round(((X - xmin) / (xmax-xmin)) * + (img_size[1]-1)).astype(int) + y_img = np.round(((ymax - Y) / (ymax-ymin)) * + (img_size[0]-1)).astype(int) + + rgb = img[y_img, x_img] * 255 + + outs['Red'] = np.array(rgb[:, 0], dtype=np.uint16) + outs['Green'] = np.array(rgb[:, 1], dtype=np.uint16) + outs['Blue'] = np.array(rgb[:, 2], dtype=np.uint16) + + return True From b496bb6596917c9994954696d8c4ff29f3d63bae Mon Sep 17 00:00:00 2001 From: Arno Date: Tue, 15 Oct 2019 13:19:12 +0200 Subject: [PATCH 21/24] created tests --- point_cloud_colorize/las_colorize.py | 47 ++++++++++++++------------- tests/create_test_data.py | 26 +++++++++++++++ tests/test.laz | Bin 0 -> 280087 bytes tests/test_colorize.py | 43 ++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 tests/create_test_data.py create mode 100644 tests/test.laz create mode 100644 tests/test_colorize.py diff --git a/point_cloud_colorize/las_colorize.py b/point_cloud_colorize/las_colorize.py index 48b5290..c545e65 100644 --- a/point_cloud_colorize/las_colorize.py +++ b/point_cloud_colorize/las_colorize.py @@ -12,7 +12,7 @@ import pdal import datetime from joblib import Parallel, delayed - +import os PDAL_PIPELINE = """{{ "pipeline":[ @@ -89,22 +89,24 @@ def run_pdal(input_path, output_path, las_srs, wms_url, pipeline.validate() pipeline.execute() -def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col = 'tmp_col_{}.laz'): + +def parallel_coloring(f, i, verbose, tmp_col_path, pdalargs, tmp_col='tmp_col_{}.laz'): tmp_col = Path(tmp_col_path.joinpath(tmp_col.format(i))) run_pdal(Path(f), tmp_col, - pdalargs['las_srs'],pdalargs['wms_url'], + pdalargs['las_srs'], pdalargs['wms_url'], pdalargs['wms_layer'], pdalargs['wms_srs'], - pdalargs['wms_version'] , pdalargs['wms_format'], - pdalargs['wms_pixel_size'],pdalargs['wms_max_image_size']) + pdalargs['wms_version'], pdalargs['wms_format'], + pdalargs['wms_pixel_size'], pdalargs['wms_max_image_size']) if verbose: print(f'colored {i} parts of the las at {datetime.datetime.now()}') + def process_files_parallel(input, output, las_srs, - wms_url, wms_layer, wms_srs, - wms_version, wms_format, - wms_pixel_size, wms_max_image_size, - verbose): + wms_url, wms_layer, wms_srs, + wms_version, wms_format, + wms_pixel_size, wms_max_image_size, + verbose): """ :param input_path: :param output_path: @@ -122,8 +124,8 @@ def process_files_parallel(input, output, las_srs, tmp_div_path = Path(output_path.parent.joinpath('tmp_div')) if tmp_div_path.exists(): - if verbose: - print('Temporary path exists, deleting.') + if verbose: + print('Temporary path exists, deleting.') shutil.rmtree(tmp_div_path) tmp_div_path.mkdir(parents=True, exist_ok=True) @@ -151,7 +153,7 @@ def process_files_parallel(input, output, las_srs, pipeline = pdal.Pipeline(div_pipeline_json) pipeline.validate() pipeline.execute() - + if verbose: print(f'las is divided at {datetime.datetime.now()}') @@ -168,7 +170,8 @@ def process_files_parallel(input, output, las_srs, if verbose: print(f'start parallel processing at {datetime.datetime.now()}') - Parallel(n_jobs=6)(delayed(parallel_coloring)(f, i, verbose, output_path, pdalargs) for i, f in enumerate(Path(tmp_div_path).iterdir(), 1)) + Parallel(n_jobs=6)(delayed(parallel_coloring)(f, i, verbose, output_path, pdalargs) for i, f in + enumerate(Path(tmp_div_path).iterdir(), 1)) if verbose: print(f'colorizing in parts finished at {datetime.datetime.now()}') @@ -313,7 +316,7 @@ def argument_parser(): default=1000) parser.add_argument('-d', '--divide', default=5, - action = "store_true", + action="store_true", help='Divide the point cloud in a given number of ' 'smaller areas which are colored seperately') parser.add_argument('-V', '--verbose', @@ -331,16 +334,16 @@ def main(): args = argument_parser() if args.divide: process_files_parallel(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) else: process_files(args.input, args.output, args.las_srs, - args.wms_url, args.wms_layer, args.wms_srs, - args.wms_version, args.wms_format, - args.wms_pixel_size, args.wms_max_image_size, - args.verbose) + args.wms_url, args.wms_layer, args.wms_srs, + args.wms_version, args.wms_format, + args.wms_pixel_size, args.wms_max_image_size, + args.verbose) if __name__ == '__main__': diff --git a/tests/create_test_data.py b/tests/create_test_data.py new file mode 100644 index 0000000..cf2c6ac --- /dev/null +++ b/tests/create_test_data.py @@ -0,0 +1,26 @@ +import json +from typing import Tuple + +import pdal +import wget + + +def crop_tile(bounds: Tuple[float, float, float, float] = (139703.2, 556571.8, 139717.1, 556581.8)) -> None: + x_min, y_min, x_max, y_max = bounds + pdal_pipeline = { + "pipeline": [ + "09HN2.LAZ", + {"type": "filters.crop", "bounds": f"([{x_min}, {y_min}], [{x_max}, {y_max}])"}, + {"type": "writers.las", "filename": "test.laz"} + ] + } + pipeline = pdal.Pipeline(json.dumps(pdal_pipeline)) + pipeline.validate() + pipeline.execute() + + +if __name__ == '__main__': + url = 'https://geodata.nationaalgeoregister.nl/ahn3/extract/ahn3_laz/C_09HN2.LAZ' + wget.download(url, '09HN2.LAZ') + + crop_tile() diff --git a/tests/test.laz b/tests/test.laz new file mode 100644 index 0000000000000000000000000000000000000000..52d57cdd9ed0d74553bfca26449f8af9090e1f4a GIT binary patch literal 280087 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%0D5zB$J+!>qvRIDYx)_3 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 literal 0 HcmV?d00001 diff --git a/tests/test_colorize.py b/tests/test_colorize.py new file mode 100644 index 0000000..8403e98 --- /dev/null +++ b/tests/test_colorize.py @@ -0,0 +1,43 @@ +import os +import unittest + +from point_cloud_colorize.las_colorize import process_files_parallel, process_files + + +class TestColorizePointcloud(unittest.TestCase): + def test_unparrallel_processing(self) -> None: + with self.subTest('It fails if the colorizing fails'): + input_laz = 'test.laz' + output_laz = 'test_out.laz' + + process_files(input_path=input_laz, + output_path=output_laz, + las_srs='EPSG:28992', + wms_url='https://geodata.nationaalgeoregister.nl/luchtfoto/rgb/wms?', + wms_layer='Actueel_ortho25', + wms_srs='EPSG:28992', + wms_version='1.3.0', + wms_format='image/png', + wms_pixel_size=0.25, + wms_max_image_size=1000, + verbose=False) + + def test_parrallel_processing(self) -> None: + with self.subTest('It fails if the colorizing fails'): + input_laz = 'test.laz' + output_laz = 'test_out_dir' + + process_files_parallel(input=input_laz, + output=output_laz, + las_srs='EPSG:28992', + wms_url='https://geodata.nationaalgeoregister.nl/luchtfoto/rgb/wms?', + wms_layer='Actueel_ortho25', + wms_srs='EPSG:28992', + wms_version='1.3.0', + wms_format='image/png', + wms_pixel_size=0.25, + wms_max_image_size=1000, + verbose=False) + +if __name__ == '__main__': + unittest.main() From 6959a56e1184a7e91c9e3d56d2804e9da451d290 Mon Sep 17 00:00:00 2001 From: Arno Date: Tue, 15 Oct 2019 13:20:46 +0200 Subject: [PATCH 22/24] created tests --- tests/data/input/README.md | 2 ++ tests/data/input/simple.laz | Bin 0 -> 22149 bytes tests/data/output/README.md | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 tests/data/input/README.md create mode 100644 tests/data/input/simple.laz create mode 100644 tests/data/output/README.md diff --git a/tests/data/input/README.md b/tests/data/input/README.md new file mode 100644 index 0000000..fab9743 --- /dev/null +++ b/tests/data/input/README.md @@ -0,0 +1,2 @@ +# Input data directory +This is the directory for storing the input data \ No newline at end of file diff --git a/tests/data/input/simple.laz b/tests/data/input/simple.laz new file mode 100644 index 0000000000000000000000000000000000000000..a867f279ab32e0172c939590606ecb9717ac21ed GIT binary patch literal 22149 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 bhWZ44@^U@%8`UejQGn3cM@i3jJ^%m!Me4f{ literal 0 HcmV?d00001 diff --git a/tests/data/output/README.md b/tests/data/output/README.md new file mode 100644 index 0000000..3ceef65 --- /dev/null +++ b/tests/data/output/README.md @@ -0,0 +1,2 @@ +# Output data directory +This is the directory for storing the output data \ No newline at end of file From 0108d1c39e5b1d5f972dc84cf842b80cc1c50c97 Mon Sep 17 00:00:00 2001 From: Arno Date: Tue, 15 Oct 2019 14:15:56 +0200 Subject: [PATCH 23/24] moved tests directory --- point_cloud_colorize/tests/__init__.py | 0 .../tests}/create_test_data.py | 0 .../tests}/data/input/README.md | 0 .../tests}/data/input/simple.laz | Bin .../tests}/data/output/README.md | 0 {tests => point_cloud_colorize/tests}/test.laz | Bin .../tests}/test_colorize.py | 2 +- 7 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 point_cloud_colorize/tests/__init__.py rename {tests => point_cloud_colorize/tests}/create_test_data.py (100%) rename {tests => point_cloud_colorize/tests}/data/input/README.md (100%) rename {tests => point_cloud_colorize/tests}/data/input/simple.laz (100%) rename {tests => point_cloud_colorize/tests}/data/output/README.md (100%) rename {tests => point_cloud_colorize/tests}/test.laz (100%) rename {tests => point_cloud_colorize/tests}/test_colorize.py (95%) diff --git a/point_cloud_colorize/tests/__init__.py b/point_cloud_colorize/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/create_test_data.py b/point_cloud_colorize/tests/create_test_data.py similarity index 100% rename from tests/create_test_data.py rename to point_cloud_colorize/tests/create_test_data.py diff --git a/tests/data/input/README.md b/point_cloud_colorize/tests/data/input/README.md similarity index 100% rename from tests/data/input/README.md rename to point_cloud_colorize/tests/data/input/README.md diff --git a/tests/data/input/simple.laz b/point_cloud_colorize/tests/data/input/simple.laz similarity index 100% rename from tests/data/input/simple.laz rename to point_cloud_colorize/tests/data/input/simple.laz diff --git a/tests/data/output/README.md b/point_cloud_colorize/tests/data/output/README.md similarity index 100% rename from tests/data/output/README.md rename to point_cloud_colorize/tests/data/output/README.md diff --git a/tests/test.laz b/point_cloud_colorize/tests/test.laz similarity index 100% rename from tests/test.laz rename to point_cloud_colorize/tests/test.laz diff --git a/tests/test_colorize.py b/point_cloud_colorize/tests/test_colorize.py similarity index 95% rename from tests/test_colorize.py rename to point_cloud_colorize/tests/test_colorize.py index 8403e98..3358744 100644 --- a/tests/test_colorize.py +++ b/point_cloud_colorize/tests/test_colorize.py @@ -1,7 +1,7 @@ import os import unittest -from point_cloud_colorize.las_colorize import process_files_parallel, process_files +from point_cloud_colorize.las_colorize import process_files, process_files_parallel class TestColorizePointcloud(unittest.TestCase): From 06a1bc306b4e34a044306aba01cdb4961903de35 Mon Sep 17 00:00:00 2001 From: Arno Date: Tue, 15 Oct 2019 14:30:37 +0200 Subject: [PATCH 24/24] moved tests directory --- point_cloud_colorize/tests/test_colorize.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/point_cloud_colorize/tests/test_colorize.py b/point_cloud_colorize/tests/test_colorize.py index 3358744..7a4079c 100644 --- a/point_cloud_colorize/tests/test_colorize.py +++ b/point_cloud_colorize/tests/test_colorize.py @@ -3,11 +3,10 @@ from point_cloud_colorize.las_colorize import process_files, process_files_parallel - class TestColorizePointcloud(unittest.TestCase): def test_unparrallel_processing(self) -> None: with self.subTest('It fails if the colorizing fails'): - input_laz = 'test.laz' + input_laz = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test.laz') output_laz = 'test_out.laz' process_files(input_path=input_laz, @@ -24,7 +23,8 @@ def test_unparrallel_processing(self) -> None: def test_parrallel_processing(self) -> None: with self.subTest('It fails if the colorizing fails'): - input_laz = 'test.laz' + + input_laz = os.path.join(os.path.dirname(os.path.abspath(__file__)),'test.laz') output_laz = 'test_out_dir' process_files_parallel(input=input_laz, @@ -39,5 +39,4 @@ def test_parrallel_processing(self) -> None: wms_max_image_size=1000, verbose=False) -if __name__ == '__main__': - unittest.main() +