From 46bdb258245352f0ba4b6ea7bcccc88d2eead9e3 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Jan 2025 16:42:47 -0500 Subject: [PATCH] Add point cloud resampling export --- app/api/tiler.py | 13 ++++++++-- app/pointcloud_utils.py | 9 +++++-- .../app/js/components/ExportAssetPanel.jsx | 24 ++++++++++++++++--- app/tests/test_api_export.py | 3 ++- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/app/api/tiler.py b/app/api/tiler.py index d04c7d1a..7695f1ee 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -524,6 +524,7 @@ class Export(TaskNestedView): epsg = request.data.get('epsg') color_map = request.data.get('color_map') hillshade = request.data.get('hillshade') + resample = request.data.get('resample') if formula == '': formula = None if bands == '': bands = None @@ -531,6 +532,7 @@ class Export(TaskNestedView): if epsg == '': epsg = None if color_map == '': color_map = None if hillshade == '': hillshade = None + if resample == '': resample = None expr = None @@ -552,6 +554,12 @@ class Export(TaskNestedView): except InvalidColorMapName: raise exceptions.ValidationError(_("Not a valid color_map value")) + if resample is not None: + try: + resample = float(resample) + except ValueError: + raise exceptions.ValidationError(_("Invalid resample value: %(value)s") % {'value': resample}) + if epsg is not None: try: epsg = int(epsg) @@ -627,9 +635,10 @@ class Export(TaskNestedView): return Response({'celery_task_id': celery_task_id, 'filename': filename}) elif asset_type == 'georeferenced_model': # Shortcut the process if no processing is required - if export_format == 'laz' and (epsg == task.epsg or epsg is None): + if export_format == 'laz' and (epsg == task.epsg or epsg is None) and (resample is None or resample == 0): return Response({'url': '/api/projects/{}/tasks/{}/download/{}.laz'.format(task.project.id, task.id, asset_type), 'filename': filename}) else: celery_task_id = export_pointcloud.delay(url, epsg=epsg, - format=export_format).task_id + format=export_format, + resample=resample).task_id return Response({'celery_task_id': celery_task_id, 'filename': filename}) diff --git a/app/pointcloud_utils.py b/app/pointcloud_utils.py index 8960a6dd..6f4e886b 100644 --- a/app/pointcloud_utils.py +++ b/app/pointcloud_utils.py @@ -10,19 +10,24 @@ logger = logging.getLogger('app.logger') def export_pointcloud(input, output, **opts): epsg = opts.get('epsg') export_format = opts.get('format') + resample = float(opts.get('resample', 0)) + resample_args = [] reprojection_args = [] extra_args = [] if epsg: reprojection_args = ["reprojection", "--filters.reprojection.out_srs=%s" % double_quote("EPSG:" + str(epsg))] - + if export_format == "ply": extra_args = ['--writers.ply.sized_types', 'false', '--writers.ply.storage_mode', 'little endian'] - subprocess.check_output(["pdal", "translate", input, output] + reprojection_args + extra_args) + if resample > 0: + resample_args = ['sample', '--filters.sample.radius=%s' % resample] + + subprocess.check_output(["pdal", "translate", input, output] + resample_args + reprojection_args + extra_args) def is_pointcloud_georeferenced(laz_path): diff --git a/app/static/app/js/components/ExportAssetPanel.jsx b/app/static/app/js/components/ExportAssetPanel.jsx index 51bd22e0..483b5f57 100644 --- a/app/static/app/js/components/ExportAssetPanel.jsx +++ b/app/static/app/js/components/ExportAssetPanel.jsx @@ -75,6 +75,7 @@ export default class ExportAssetPanel extends React.Component { format: props.exportFormats[0], epsg: this.props.task.epsg || null, customEpsg: Storage.getItem("last_export_custom_epsg") || "4326", + resample: 0, exporting: false } } @@ -97,6 +98,10 @@ export default class ExportAssetPanel extends React.Component { this.setState({customEpsg: e.target.value}); } + handleChangeResample = e => { + this.setState({resample: parseFloat(e.target.value) || 0}); + } + getExportParams = (format) => { let params = {}; @@ -111,9 +116,15 @@ export default class ExportAssetPanel extends React.Component { const epsg = this.getEpsg(); if (epsg) params.epsg = this.getEpsg(); + if (this.state.resample > 0) params.resample = this.state.resample; + return params; } + isPointCloud = () => { + return this.props.asset == "georeferenced_model"; + } + handleExport = (format) => { if (!format) format = this.state.format; @@ -171,7 +182,7 @@ export default class ExportAssetPanel extends React.Component { } render(){ - const {epsg, customEpsg, exporting, format } = this.state; + const {epsg, customEpsg, exporting, format, resample } = this.state; const { exportFormats } = this.props; const utmEPSG = this.props.task.epsg; @@ -200,14 +211,21 @@ export default class ExportAssetPanel extends React.Component { let exportSelector = null; if (this.props.selectorOnly){ - exportSelector = (
+ exportSelector = [
-
); +
, + this.isPointCloud() ?
+ +
+ +
+
+ : ""]; }else{ exportSelector = (
diff --git a/app/tests/test_api_export.py b/app/tests/test_api_export.py index 826b3867..3a83d3d4 100644 --- a/app/tests/test_api_export.py +++ b/app/tests/test_api_export.py @@ -82,7 +82,8 @@ class TestApiTask(BootTransactionTestCase): ('orthophoto', {'formula': 'NDVI', 'bands': 'RGN'}, status.HTTP_200_OK), ('dsm', {'epsg': 4326}, status.HTTP_200_OK), ('dtm', {'epsg': 4326}, status.HTTP_200_OK), - ('georeferenced_model', {'epsg': 4326}, status.HTTP_200_OK) + ('georeferenced_model', {'epsg': 4326}, status.HTTP_200_OK), + ('georeferenced_model', {'epsg': 4326, 'resample': 2.5}, status.HTTP_200_OK) ] # Cannot export stuff