Export point clouds

pull/1086/head
Piero Toffanin 2021-11-04 13:27:36 -04:00
rodzic 3378e67a2a
commit b6ce0ae3cd
11 zmienionych plików z 119 dodań i 44 usunięć

Wyświetl plik

@ -14,8 +14,8 @@ WORKDIR /webodm
RUN apt-get -qq update && apt-get -qq install -y --no-install-recommends wget curl && \ RUN apt-get -qq update && apt-get -qq install -y --no-install-recommends wget curl && \
wget --no-check-certificate https://deb.nodesource.com/setup_12.x -O /tmp/node.sh && bash /tmp/node.sh && \ wget --no-check-certificate https://deb.nodesource.com/setup_12.x -O /tmp/node.sh && bash /tmp/node.sh && \
apt-get -qq update && apt-get -qq install -y nodejs && \ apt-get -qq update && apt-get -qq install -y nodejs && \
# Install Python3, GDAL, nginx, letsencrypt, psql # Install Python3, GDAL, PDAL, nginx, letsencrypt, psql
apt-get -qq update && apt-get -qq install -y --no-install-recommends python3 python3-pip python3-setuptools python3-wheel git g++ python3-dev python2.7-dev libpq-dev binutils libproj-dev gdal-bin libgdal-dev python3-gdal nginx certbot grass-core gettext-base cron postgresql-client-13 gettext tzdata && \ apt-get -qq update && apt-get -qq install -y --no-install-recommends python3 python3-pip python3-setuptools python3-wheel git g++ python3-dev python2.7-dev libpq-dev binutils libproj-dev gdal-bin pdal libgdal-dev python3-gdal nginx certbot grass-core gettext-base cron postgresql-client-13 gettext tzdata && \
update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 && update-alternatives --install /usr/bin/python python /usr/bin/python3.9 2 && \ update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 && update-alternatives --install /usr/bin/python python /usr/bin/python3.9 2 && \
# Install pip reqs # Install pip reqs
pip install -U pip && pip install -r requirements.txt "boto3==1.14.14" && \ pip install -U pip && pip install -r requirements.txt "boto3==1.14.14" && \

Wyświetl plik

@ -20,14 +20,14 @@ from rio_tiler.io import COGReader
from rio_tiler.errors import InvalidColorMapName from rio_tiler.errors import InvalidColorMapName
import numpy as np import numpy as np
from .custom_colormaps_helper import custom_colormaps from .custom_colormaps_helper import custom_colormaps
from app.raster_utils import export_raster, extension_for_export_format, ZOOM_EXTRA_LEVELS from app.raster_utils import extension_for_export_format, ZOOM_EXTRA_LEVELS
from .hsvblend import hsv_blend from .hsvblend import hsv_blend
from .hillshade import LightSource from .hillshade import LightSource
from .formulas import lookup_formula, get_algorithm_list from .formulas import lookup_formula, get_algorithm_list
from .tasks import TaskNestedView from .tasks import TaskNestedView
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.response import Response from rest_framework.response import Response
from worker.tasks import export_raster from worker.tasks import export_raster, export_pointcloud
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -76,6 +76,9 @@ def get_extent(task, tile_type):
def get_raster_path(task, tile_type): def get_raster_path(task, tile_type):
return task.get_asset_download_path(tile_type + ".tif") return task.get_asset_download_path(tile_type + ".tif")
def get_pointcloud_path(task):
return task.get_asset_download_path("georeferenced_model.laz")
class TileJson(TaskNestedView): class TileJson(TaskNestedView):
def get(self, request, pk=None, project_pk=None, tile_type=""): def get(self, request, pk=None, project_pk=None, tile_type=""):
@ -491,7 +494,9 @@ class Export(TaskNestedView):
expr = None expr = None
if not export_format in ['gtiff', 'gtiff-rgb', 'jpg', 'png', 'kmz']: if asset_type in ['orthophoto', 'dsm', 'dtm'] and not export_format in ['gtiff', 'gtiff-rgb', 'jpg', 'png', 'kmz']:
raise exceptions.ValidationError(_("Unsupported format: %(value)s") % {'value': export_format})
if asset_type == 'georeferenced_model' and not export_format in ['laz', 'las', 'ply', 'csv']:
raise exceptions.ValidationError(_("Unsupported format: %(value)s") % {'value': export_format}) raise exceptions.ValidationError(_("Unsupported format: %(value)s") % {'value': export_format})
if epsg is not None: if epsg is not None:
@ -528,7 +533,10 @@ class Export(TaskNestedView):
except: except:
raise exceptions.ValidationError(_("Invalid hillshade value: %(value)s") % {'value': hillshade}) raise exceptions.ValidationError(_("Invalid hillshade value: %(value)s") % {'value': hillshade})
url = get_raster_path(task, asset_type) if asset_type == 'georeferenced_model':
url = get_pointcloud_path(task)
else:
url = get_raster_path(task, asset_type)
if not os.path.isfile(url): if not os.path.isfile(url):
raise exceptions.NotFound() raise exceptions.NotFound()
@ -541,16 +549,25 @@ class Export(TaskNestedView):
extension extension
) )
# Shortcut the process if no processing is required if asset_type in ['orthophoto', 'dsm', 'dtm']:
if export_format == 'gtiff' and (epsg == task.epsg or epsg is None) and expr is None: # Shortcut the process if no processing is required
return Response({'url': '/api/projects/{}/tasks/{}/download/{}.tif'.format(task.project.id, task.id, asset_type), 'filename': filename}) if export_format == 'gtiff' and (epsg == task.epsg or epsg is None) and expr is None:
else: return Response({'url': '/api/projects/{}/tasks/{}/download/{}.tif'.format(task.project.id, task.id, asset_type), 'filename': filename})
celery_task_id = export_raster.delay(url, epsg=epsg, else:
expression=expr, celery_task_id = export_raster.delay(url, epsg=epsg,
format=export_format, expression=expr,
rescale=rescale, format=export_format,
color_map=color_map, rescale=rescale,
hillshade=hillshade, color_map=color_map,
asset_type=asset_type, hillshade=hillshade,
name=task.name).task_id asset_type=asset_type,
return Response({'celery_task_id': celery_task_id, 'filename': filename}) name=task.name).task_id
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):
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
return Response({'celery_task_id': celery_task_id, 'filename': filename})

Wyświetl plik

@ -39,7 +39,7 @@ urlpatterns = [
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/metadata$', Metadata.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/metadata$', Metadata.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)\.png$', Tiles.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)\.png$', Tiles.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)@(?P<scale>[\d]+)x\.png$', Tiles.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)@(?P<scale>[\d]+)x\.png$', Tiles.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<asset_type>orthophoto|dsm|dtm)/export$', Export.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<asset_type>orthophoto|dsm|dtm|georeferenced_model)/export$', Export.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()),

Wyświetl plik

@ -0,0 +1,23 @@
import logging
import os
import subprocess
from app.security import double_quote
logger = logging.getLogger('app.logger')
def export_pointcloud(input, output, **opts):
epsg = opts.get('epsg')
export_format = opts.get('format')
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)

Wyświetl plik

@ -1,4 +1,3 @@
# Export a raster index after applying a band expression
import rasterio import rasterio
import re import re
import logging import logging
@ -23,11 +22,8 @@ def extension_for_export_format(export_format):
extensions = { extensions = {
'gtiff': 'tif', 'gtiff': 'tif',
'gtiff-rgb': 'tif', 'gtiff-rgb': 'tif',
'jpg': 'jpg',
'png': 'png',
'kmz': 'kmz'
} }
return extensions.get(export_format, 'tif') return extensions.get(export_format, export_format)
def export_raster(input, output, **opts): def export_raster(input, output, **opts):
epsg = opts.get('epsg') epsg = opts.get('epsg')

Wyświetl plik

@ -1,11 +1,12 @@
import { _ } from './gettext'; import { _ } from './gettext';
class AssetDownload{ class AssetDownload{
constructor(label, asset, icon, exportFormats = null){ constructor(label, asset, icon, exportFormats = null, exportParams = {}){
this.label = label; this.label = label;
this.asset = asset; this.asset = asset;
this.icon = icon; this.icon = icon;
this.exportFormats = exportFormats; this.exportFormats = exportFormats;
this.exportParams = exportParams;
} }
downloadUrl(project_id, task_id){ downloadUrl(project_id, task_id){
@ -36,24 +37,24 @@ class AssetDownloadSeparator extends AssetDownload{
} }
} }
const tiffExportFormats = ["gtiff", "gtiff-rgb", "jpg", "png", "kmz"];
const elevationExportParams = {'hillshade': 6, "color_map": "viridis"};
const api = { const api = {
all: function() { all: function() {
return [ return [
new AssetDownload(_("Orthophoto"),"orthophoto.tif","far fa-image", ["gtiff", "gtiff-rgb", "jpg", "png", "kmz"]), new AssetDownload(_("Orthophoto"),"orthophoto.tif","far fa-image", tiffExportFormats),
new AssetDownload(_("Orthophoto (MBTiles)"),"orthophoto.mbtiles","far fa-image"), new AssetDownload(_("Orthophoto (MBTiles)"),"orthophoto.mbtiles","far fa-image"),
new AssetDownload(_("Orthophoto (Tiles)"),"orthophoto_tiles.zip","fa fa-table"), new AssetDownload(_("Orthophoto (Tiles)"),"orthophoto_tiles.zip","fa fa-table"),
new AssetDownload(_("Terrain Model"),"dtm.tif","fa fa-chart-area"), new AssetDownload(_("Terrain Model"),"dtm.tif","fa fa-chart-area", tiffExportFormats, elevationExportParams),
new AssetDownload(_("Terrain Model (Tiles)"),"dtm_tiles.zip","fa fa-table"), new AssetDownload(_("Terrain Model (Tiles)"),"dtm_tiles.zip","fa fa-table"),
new AssetDownload(_("Surface Model"),"dsm.tif","fa fa-chart-area"), new AssetDownload(_("Surface Model"),"dsm.tif","fa fa-chart-area", tiffExportFormats, elevationExportParams),
new AssetDownload(_("Surface Model (Tiles)"),"dsm_tiles.zip","fa fa-table"), new AssetDownload(_("Surface Model (Tiles)"),"dsm_tiles.zip","fa fa-table"),
new AssetDownload(_("Point Cloud (LAS)"),"georeferenced_model.las","fa fa-cube"), new AssetDownload(_("Point Cloud"),"georeferenced_model.laz","fa fa-cube", ["laz", "las", "ply", "csv"]),
new AssetDownload(_("Point Cloud (LAZ)"),"georeferenced_model.laz","fa fa-cube"),
new AssetDownload(_("Point Cloud (PLY)"),"georeferenced_model.ply","fa fa-cube"),
new AssetDownload(_("Point Cloud (CSV)"),"georeferenced_model.csv","fa fa-cube"),
new AssetDownload(_("Textured Model"),"textured_model.zip","fab fa-connectdevelop"), new AssetDownload(_("Textured Model"),"textured_model.zip","fab fa-connectdevelop"),
new AssetDownload(_("Camera Parameters"),"cameras.json","fa fa-camera"), new AssetDownload(_("Camera Parameters"),"cameras.json","fa fa-camera"),
new AssetDownload(_("Camera Shots (GeoJSON)"),"shots.geojson","fa fa-camera"), new AssetDownload(_("Camera Shots"),"shots.geojson","fa fa-camera"),
new AssetDownload(_("Ground Control Points (GeoJSON)"),"ground_control_points.geojson","far fa-dot-circle"), new AssetDownload(_("Ground Control Points"),"ground_control_points.geojson","far fa-dot-circle"),
new AssetDownload(_("Quality Report"),"report.pdf","far fa-file-pdf"), new AssetDownload(_("Quality Report"),"report.pdf","far fa-file-pdf"),

Wyświetl plik

@ -43,6 +43,7 @@ class AssetDownloadButtons extends React.Component {
<ExportAssetDialog task={this.props.task} <ExportAssetDialog task={this.props.task}
asset={this.state.exportDialogProps.asset} asset={this.state.exportDialogProps.asset}
exportFormats={this.state.exportDialogProps.exportFormats} exportFormats={this.state.exportDialogProps.exportFormats}
exportParams={this.state.exportDialogProps.exportParams}
onHide={this.onHide} onHide={this.onHide}
assetLabel={this.state.exportDialogProps.assetLabel} assetLabel={this.state.exportDialogProps.assetLabel}
/> />
@ -67,6 +68,7 @@ class AssetDownloadButtons extends React.Component {
this.setState({exportDialogProps: { this.setState({exportDialogProps: {
asset: asset.exportId(), asset: asset.exportId(),
exportFormats: asset.exportFormats, exportFormats: asset.exportFormats,
exportParams: asset.exportParams,
assetLabel: asset.label assetLabel: asset.label
}}); }});
} }

Wyświetl plik

@ -14,6 +14,7 @@ class ExportAssetDialog extends React.Component {
asset: PropTypes.string.isRequired, asset: PropTypes.string.isRequired,
task: PropTypes.object.isRequired, task: PropTypes.object.isRequired,
exportFormats: PropTypes.arrayOf(PropTypes.string), exportFormats: PropTypes.arrayOf(PropTypes.string),
exportParams: PropTypes.object,
assetLabel: PropTypes.string assetLabel: PropTypes.string
}; };
@ -43,7 +44,8 @@ class ExportAssetDialog extends React.Component {
task={this.props.task} task={this.props.task}
ref={(domNode) => { this.exportAssetPanel = domNode; }} ref={(domNode) => { this.exportAssetPanel = domNode; }}
selectorOnly selectorOnly
exportFormats={this.props.exportFormats} /> exportFormats={this.props.exportFormats}
exportParams={this.props.exportParams} />
</FormDialog> </FormDialog>
</div> </div>
); );

Wyświetl plik

@ -33,24 +33,40 @@ export default class ExportAssetPanel extends React.Component {
this.efInfo = { this.efInfo = {
'gtiff-rgb': { 'gtiff-rgb': {
label: _("GeoTIFF (RGB)"), label: "GeoTIFF (RGB)",
icon: "fas fa-palette" icon: "fas fa-palette"
}, },
'gtiff': { 'gtiff': {
label: _("GeoTIFF (Raw)"), label: "GeoTIFF (Raw)",
icon: "far fa-image" icon: "far fa-image"
}, },
'jpg': { 'jpg': {
label: _("JPEG (RGB)"), label: "JPEG (RGB)",
icon: "fas fa-palette" icon: "fas fa-palette"
}, },
'png': { 'png': {
label: _("PNG (RGB)"), label: "PNG (RGB)",
icon: "fas fa-palette" icon: "fas fa-palette"
}, },
'kmz': { 'kmz': {
label: _("KMZ (RGB)"), label: "KMZ (RGB)",
icon: "fa fa-globe" icon: "fa fa-globe"
},
'laz': {
label: "LAZ",
icon: "fa fa-cube"
},
'las': {
label: "LAS",
icon: "fa fa-cube"
},
'ply': {
label: "PLY",
icon: "fa fa-cube"
},
'csv': {
label: "CSV",
icon: "fa fa-file-text"
} }
}; };
@ -92,6 +108,7 @@ export default class ExportAssetPanel extends React.Component {
params.format = format; params.format = format;
params.epsg = this.getEpsg(); params.epsg = this.getEpsg();
console.log(params);
return params; return params;
} }

Wyświetl plik

@ -1,6 +1,6 @@
{ {
"name": "WebODM", "name": "WebODM",
"version": "1.9.10", "version": "1.9.11",
"description": "User-friendly, extendable application and API for processing aerial imagery.", "description": "User-friendly, extendable application and API for processing aerial imagery.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

Wyświetl plik

@ -20,6 +20,7 @@ from webodm import settings
import worker import worker
from .celery import app from .celery import app
from app.raster_utils import export_raster as export_raster_sync, extension_for_export_format from app.raster_utils import export_raster as export_raster_sync, extension_for_export_format
from app.pointcloud_utils import export_pointcloud as export_pointcloud_sync
import redis import redis
logger = get_task_logger("app.logger") logger = get_task_logger("app.logger")
@ -169,3 +170,19 @@ def export_raster(self, input, **opts):
except Exception as e: except Exception as e:
logger.error(str(e)) logger.error(str(e))
return {'error': str(e)} return {'error': str(e)}
@app.task(bind=True)
def export_pointcloud(self, input, **opts):
try:
logger.info("Exporting point cloud {} with options: {}".format(input, json.dumps(opts)))
tmpfile = tempfile.mktemp('_pointcloud.{}'.format(opts.get('format', 'laz')), dir=settings.MEDIA_TMP)
export_pointcloud_sync(input, tmpfile, **opts)
result = {'file': tmpfile}
if settings.TESTING:
TestSafeAsyncResult.set(self.request.id, result)
return result
except Exception as e:
logger.error(str(e))
return {'error': str(e)}