diff --git a/app/plugins/views.py b/app/plugins/views.py index 8944a0a9..c3d16a7e 100644 --- a/app/plugins/views.py +++ b/app/plugins/views.py @@ -3,6 +3,7 @@ import os from app.api.tasks import TaskNestedView as TaskView from app.api.workers import CheckTask as CheckTask from app.api.workers import GetTaskResult as GetTaskResult +from app.api.workers import TaskResultOutputError from django.http import HttpResponse, Http404 from .functions import get_plugin_by_name, get_active_plugins diff --git a/coreplugins/objdetect/api.py b/coreplugins/objdetect/api.py index 026eab61..4131930a 100644 --- a/coreplugins/objdetect/api.py +++ b/coreplugins/objdetect/api.py @@ -1,122 +1,48 @@ import os - +import json from rest_framework import status from rest_framework.response import Response -from app.plugins.views import TaskView, CheckTask, GetTaskResult +from app.plugins.views import TaskView, CheckTask, GetTaskResult, TaskResultOutputError from app.plugins.worker import run_function_async from django.utils.translation import gettext_lazy as _ -class ContoursException(Exception): - pass - -def calc_contours(dem, epsg, interval, output_format, simplify, zfactor = 1): +def detect(orthophoto, model): import os - import subprocess - import tempfile - import shutil - import glob from webodm import settings - ext = "" - if output_format == "GeoJSON": - ext = "json" - elif output_format == "GPKG": - ext = "gpkg" - elif output_format == "DXF": - ext = "dxf" - elif output_format == "ESRI Shapefile": - ext = "shp" - MIN_CONTOUR_LENGTH = 10 + try: + from geodeep import detect as gdetect, models + models.cache_dir = os.path.join(settings.MEDIA_ROOT, "CACHE", "detection_models") + except ImportError: + return {'error': "GeoDeep library is missing"} - tmpdir = os.path.join(settings.MEDIA_TMP, os.path.basename(tempfile.mkdtemp('_contours', dir=settings.MEDIA_TMP))) - gdal_contour_bin = shutil.which("gdal_contour") - ogr2ogr_bin = shutil.which("ogr2ogr") - - if gdal_contour_bin is None: - return {'error': 'Cannot find gdal_contour'} - if ogr2ogr_bin is None: - return {'error': 'Cannot find ogr2ogr'} - - contours_file = f"contours.gpkg" - p = subprocess.Popen([gdal_contour_bin, "-q", "-a", "level", "-3d", "-f", "GPKG", "-i", str(interval), dem, contours_file], cwd=tmpdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - - out = out.decode('utf-8').strip() - err = err.decode('utf-8').strip() - success = p.returncode == 0 - - if not success: - return {'error', f'Error calling gdal_contour: {str(err)}'} - - outfile = os.path.join(tmpdir, f"output.{ext}") - p = subprocess.Popen([ogr2ogr_bin, outfile, contours_file, "-simplify", str(simplify), "-f", output_format, "-t_srs", f"EPSG:{epsg}", "-nln", "contours", - "-dialect", "sqlite", "-sql", f"SELECT ID, ROUND(level * {zfactor}, 5) AS level, GeomFromGML(AsGML(ATM_Transform(GEOM, ATM_Scale(ATM_Create(), 1, 1, {zfactor})), 10)) as GEOM FROM contour WHERE ST_Length(GEOM) >= {MIN_CONTOUR_LENGTH}"], cwd=tmpdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - - out = out.decode('utf-8').strip() - err = err.decode('utf-8').strip() - success = p.returncode == 0 - - if not success: - return {'error', f'Error calling ogr2ogr: {str(err)}'} - - if not os.path.isfile(outfile): - return {'error': f'Cannot find output file: {outfile}'} - - if output_format == "ESRI Shapefile": - ext="zip" - shp_dir = os.path.join(tmpdir, "contours") - os.makedirs(shp_dir) - contour_files = glob.glob(os.path.join(tmpdir, "output.*")) - for cf in contour_files: - shutil.move(cf, shp_dir) - - shutil.make_archive(os.path.join(tmpdir, 'output'), 'zip', shp_dir) - outfile = os.path.join(tmpdir, f"output.{ext}") - - return {'file': outfile} - - -class TaskContoursGenerate(TaskView): + try: + return {'output': gdetect(orthophoto, model, output_type='geojson')} + except Exception as e: + return {'error': str(e)} + +class TaskObjDetect(TaskView): def post(self, request, pk=None): task = self.get_and_check_task(request, pk) - layer = request.data.get('layer', None) - if layer == 'DSM' and task.dsm_extent is None: - return Response({'error': _('No DSM layer is available.')}) - elif layer == 'DTM' and task.dtm_extent is None: - return Response({'error': _('No DTM layer is available.')}) + if task.orthophoto_extent is None: + return Response({'error': _('No orthophoto is available.')}) - try: - if layer == 'DSM': - dem = os.path.abspath(task.get_asset_download_path("dsm.tif")) - elif layer == 'DTM': - dem = os.path.abspath(task.get_asset_download_path("dtm.tif")) - else: - raise ContoursException('{} is not a valid layer.'.format(layer)) + orthophoto = os.path.abspath(task.get_asset_download_path("orthophoto.tif")) + model = request.data.get('model', 'cars') - epsg = int(request.data.get('epsg', '3857')) - interval = float(request.data.get('interval', 1)) - format = request.data.get('format', 'GPKG') - supported_formats = ['GPKG', 'ESRI Shapefile', 'DXF', 'GeoJSON'] - if not format in supported_formats: - raise ContoursException("Invalid format {} (must be one of: {})".format(format, ",".join(supported_formats))) - simplify = float(request.data.get('simplify', 0.01)) - zfactor = float(request.data.get('zfactor', 1)) + if not model in ['cars', 'trees']: + return Response({'error': 'Invalid model'}, status=status.HTTP_200_OK) - celery_task_id = run_function_async(calc_contours, dem, epsg, interval, format, simplify, zfactor).task_id - return Response({'celery_task_id': celery_task_id}, status=status.HTTP_200_OK) - except ContoursException as e: - return Response({'error': str(e)}, status=status.HTTP_200_OK) + celery_task_id = run_function_async(detect, orthophoto, model).task_id + return Response({'celery_task_id': celery_task_id}, status=status.HTTP_200_OK) -class TaskContoursCheck(CheckTask): - def on_error(self, result): - pass - - def error_check(self, result): - contours_file = result.get('file') - if not contours_file or not os.path.exists(contours_file): - return _('Could not generate contour file. This might be a bug.') - -class TaskContoursDownload(GetTaskResult): +class TaskObjCheck(CheckTask): pass + +class TaskObjDownload(GetTaskResult): + def handle_output(self, output, result, **kwargs): + try: + return json.loads(output) + except: + raise TaskResultOutputError("Invalid GeoJSON") diff --git a/coreplugins/objdetect/plugin.py b/coreplugins/objdetect/plugin.py index 1dbf7e2e..adc097df 100644 --- a/coreplugins/objdetect/plugin.py +++ b/coreplugins/objdetect/plugin.py @@ -1,8 +1,8 @@ from app.plugins import PluginBase from app.plugins import MountPoint -# from .api import TaskContoursGenerate -# from .api import TaskContoursCheck -# from .api import TaskContoursDownload +from .api import TaskObjDetect +from .api import TaskObjCheck +from .api import TaskObjDownload class Plugin(PluginBase): @@ -14,7 +14,7 @@ class Plugin(PluginBase): def api_mount_points(self): return [ - # MountPoint('task/(?P[^/.]+)/contours/generate', TaskContoursGenerate.as_view()), - # MountPoint('task/[^/.]+/contours/check/(?P.+)', TaskContoursCheck.as_view()), - # MountPoint('task/[^/.]+/contours/download/(?P.+)', TaskContoursDownload.as_view()), + MountPoint('task/(?P[^/.]+)/detect', TaskObjDetect.as_view()), + MountPoint('task/[^/.]+/check/(?P.+)', TaskObjCheck.as_view()), + MountPoint('task/[^/.]+/download/(?P.+)', TaskObjDownload.as_view()), ] \ No newline at end of file diff --git a/coreplugins/objdetect/public/ObjDetectPanel.jsx b/coreplugins/objdetect/public/ObjDetectPanel.jsx index 946b44f3..154e9b13 100644 --- a/coreplugins/objdetect/public/ObjDetectPanel.jsx +++ b/coreplugins/objdetect/public/ObjDetectPanel.jsx @@ -78,37 +78,33 @@ export default class ObjDetectPanel extends React.Component { }; } - addGeoJSONFromURL = (url, cb) => { + addGeoJSON = (geojson, cb) => { const { map } = this.props; - $.getJSON(url) - .done((geojson) => { - try{ - this.handleRemoveObjLayer(); + try{ + this.handleRemoveObjLayer(); - this.setState({objLayer: L.geoJSON(geojson, { - onEachFeature: (feature, layer) => { - if (feature.properties && feature.properties.level !== undefined) { - layer.bindPopup(`
- ${_("Class:")} ${feature.properties.class}
- ${_("Score:")} ${feature.properties.score}
-
- `); - } - }, - style: feature => { - // TODO: different colors for different elevations? - return {color: "yellow"}; - } - })}); - this.state.objLayer.addTo(map); + this.setState({objLayer: L.geoJSON(geojson, { + onEachFeature: (feature, layer) => { + if (feature.properties && feature.properties.level !== undefined) { + layer.bindPopup(`
+ ${_("Class:")} ${feature.properties.class}
+ ${_("Score:")} ${feature.properties.score}
+
+ `); + } + }, + style: feature => { + // TODO: different colors for different elevations? + return {color: "red"}; + } + })}); + this.state.objLayer.addTo(map); - cb(); - }catch(e){ - cb(e.message); - } - }) - .fail(cb); + cb(); + }catch(e){ + cb(e.message); + } } handleRemoveObjLayer = () => { @@ -139,11 +135,20 @@ export default class ObjDetectPanel extends React.Component { Workers.waitForCompletion(result.celery_task_id, error => { if (error) this.setState({detecting: false, error}); else{ - const fileUrl = `/api/plugins/objdetect/task/${taskId}/download/${result.celery_task_id}`; + Workers.getOutput(result.celery_task_id, (error, geojson) => { + try{ + geojson = JSON.parse(geojson); + }catch(e){ + error = "Invalid GeoJSON"; + } - this.addGeoJSONFromURL(fileUrl, e => { - if (e) this.setState({error: JSON.stringify(e)}); - this.setState({detecting: false}); + if (error) this.setState({detecting: false, error}); + else{ + this.addGeoJSON(geojson, e => { + if (e) this.setState({error: JSON.stringify(e)}); + this.setState({detecting: false}); + }); + } }); } }, `/api/plugins/objdetect/task/${taskId}/check/`); diff --git a/coreplugins/objdetect/public/ObjDetectPanel.scss b/coreplugins/objdetect/public/ObjDetectPanel.scss index 47f8929d..1cfe68b7 100644 --- a/coreplugins/objdetect/public/ObjDetectPanel.scss +++ b/coreplugins/objdetect/public/ObjDetectPanel.scss @@ -62,10 +62,6 @@ } } - .btn-detect{ - margin-right: 8px; - } - .action-buttons{ margin-top: 12px; }