import logging
import os
import pdal
import laspy
import json
from laserfarm.grid import Grid
from laserfarm.pipeline_remote_data import PipelineRemoteData
from laserfarm.utils import check_file_exists, check_dir_exists
logger = logging.getLogger(__name__)
[docs]
class Retiler(PipelineRemoteData):
""" Split point cloud data into smaller tiles on a regular grid. """
def __init__(self, input_file=None, label=None):
self.pipeline = ('set_grid', 'split_and_redistribute', 'validate')
self.grid = Grid()
if input_file is not None:
self.input_path = input_file
if label is not None:
self.label = label
[docs]
def set_grid(self, min_x, min_y, max_x, max_y, n_tiles_side):
"""
Setup the grid to which the input file is retiled.
:param min_x: min x value of tiling schema
:param min_y: max y value of tiling schema
:param max_x: min x value of tiling schema
:param max_y: max y value of tiling schema
:param n_tiles_side: number of tiles along axis. Tiling MUST be square
(enforced)
"""
logger.info('Setting up the target grid')
self.grid.setup(min_x, min_y, max_x, max_y, n_tiles_side)
return self
[docs]
def split_and_redistribute(self, override_srs=None):
"""
Split the input file using PDAL and organize the tiles in subfolders
using the location on the input grid as naming scheme.
"""
self._check_input()
logger.info('Splitting file {} with PDAL ...'.format(self.input_path))
_run_PDAL_splitter(self.input_path, self.output_folder,
self.grid.grid_mins, self.grid.grid_maxs,
self.grid.n_tiles_side, override_srs)
logger.info('... splitting completed.')
tiles = [f for f in self.output_folder.iterdir()
if (f.is_file()
and f.suffix.lower() == self.input_path.suffix.lower()
and f.stem.startswith(self.input_path.stem)
and f.name != self.input_path.name)]
logger.info('Redistributing files to tiles ...')
for tile in tiles:
(_, tile_mins, tile_maxs, _, _) = _get_details_pc_file(str(tile))
# Get central point to identify associated tile
cpX = tile_mins[0] + ((tile_maxs[0] - tile_mins[0]) / 2.)
cpY = tile_mins[1] + ((tile_maxs[1] - tile_mins[1]) / 2.)
tile_id = _get_tile_name(*self.grid.get_tile_index(cpX, cpY))
retiled_folder = self.output_folder.joinpath(tile_id)
check_dir_exists(retiled_folder, should_exist=True, mkdir=True)
logger.info('... file {} to {}'.format(tile.name, tile_id))
tile.rename(retiled_folder.joinpath(tile.name))
logger.info('... redistributing completed.')
return self
[docs]
def validate(self, write_record_to_file=True):
"""
Validate the produced output by checking consistency in the number
of input and output points.
"""
self._check_input()
logger.info('Validating split ...')
(parent_points, _, _, _, _) = _get_details_pc_file(self.input_path.as_posix())
logger.info('... {} points in parent file'.format(parent_points))
valid_split = False
split_points = 0
redistributed_to = []
tiles = self.output_folder.glob('tile_*/{}*'.format(self.input_path.stem))
for tile in tiles:
if tile.is_file():
(tile_points, _, _, _, _) = _get_details_pc_file(tile.as_posix())
logger.info('... {} points in {}'.format(tile_points,
tile.name))
split_points += tile_points
redistributed_to.append(tile.parent.name)
if parent_points == split_points:
logger.info('... split validation completed.')
valid_split = True
else:
logger.error('Number of points in parent and child tiles differ!')
retile_record = {'file': self.input_path.as_posix(),
'redistributed_to': redistributed_to,
'validated': valid_split}
if write_record_to_file:
_write_record(self.input_path.stem,
self.output_folder,
retile_record)
return self
def _check_input(self):
if not self.grid.is_set:
raise ValueError('The grid has not been set!')
check_file_exists(self.input_path, should_exist=True)
check_dir_exists(self.output_folder, should_exist=True)
def _get_details_pc_file(filename):
try:
with laspy.open(filename) as file:
count = file.header.point_count
mins = file.header.mins
maxs = file.header.maxs
scales = file.header.scales
offsets = file.header.offsets
return (count, mins, maxs, scales, offsets)
except IOError:
logger.error('failure to open {}'.format(filename))
return None
def _get_tile_name(x_index, y_index):
return 'tile_{}_{}'.format(int(x_index), int(y_index))
def _run_PDAL_splitter(filename, tiled_temp_folder, tiling_mins, tiling_maxs,
n_tiles_side, override_srs=None):
length_PDAL_tile = ((tiling_maxs[0] - tiling_mins[0]) /
float(n_tiles_side))
outfile_with_placeholder = "_#".join([filename.stem, filename.suffix])
outfilepath = tiled_temp_folder.joinpath(outfile_with_placeholder)
if override_srs is not None:
# Reading LAS files with point format 6-10 will throw an error if WKT
# flag is not set - we can bypass it here via overriding the SRS
readers = {
"type": "readers.las",
"filename": filename.as_posix(),
"nosrs": True,
"override_srs": override_srs,
}
else:
# If no options are needed, only the filepath is sufficient
readers = filename.as_posix()
PDAL_pipeline_dict = {
"pipeline": [
readers,
{
"type": "filters.splitter",
"origin_x": "{}".format(tiling_mins[0]),
"origin_y": "{}".format(tiling_mins[1]),
"length": "{}".format(length_PDAL_tile)
},
{
"type": "writers.las",
"filename": outfilepath.as_posix(),
"forward": ["scale_x", "scale_y", "scale_z"],
"offset_x": "auto",
"offset_y": "auto",
"offset_z": "auto"
}
]
}
_print_PDAL_pipeline_dict(PDAL_pipeline_dict)
PDAL_pipeline = pdal.Pipeline(json.dumps(PDAL_pipeline_dict))
logger.debug("... running PDAL:")
PDAL_pipeline.execute()
def _print_PDAL_pipeline_dict(dictionary):
logger.debug('... PDAL input:')
for el in dictionary.get("pipeline"):
if isinstance(el, dict):
_dict = el.copy()
logger.debug("... type = {}".format(_dict.pop('type')))
for key, val in _dict.items():
logger.debug("... {} = {}".format(key, val))
else:
logger.debug("... {}".format(el))
logger.debug('... PDAL input end.')
def _write_record(input_tile, temp_folder, retile_record):
record_file = os.path.join(temp_folder, os.path.splitext(
input_tile)[0] + '_retile_record.js')
with open(record_file, 'w') as recfile:
recfile.write(json.dumps(retile_record, indent=4, sort_keys=True))