From 90f775ce65023587931b4918c85e22572ecec746 Mon Sep 17 00:00:00 2001 From: Hauke Stieler Date: Sun, 19 Jan 2025 21:52:00 +0100 Subject: [PATCH 01/16] Update gunicorn to 19.8.0 fixing #1586 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10900789..945db922 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ djangorestframework-guardian==0.3.0 drf-nested-routers==0.11.1 funcsigs==1.0.2 futures==3.1.1 -gunicorn==19.7.1 +gunicorn==19.8.0 itypes==1.1.0 kombu==4.6.7 Markdown==3.3.4 From 7f44e62ac425dade1cd2743df841946b98a00733 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 23 Jan 2025 15:09:58 -0500 Subject: [PATCH 02/16] Object detection plugin mock --- coreplugins/objdetect/__init__.py | 1 + coreplugins/objdetect/api.py | 122 +++++++++++ coreplugins/objdetect/manifest.json | 13 ++ coreplugins/objdetect/plugin.py | 20 ++ coreplugins/objdetect/public/ObjDetect.jsx | 55 +++++ coreplugins/objdetect/public/ObjDetect.scss | 24 ++ .../objdetect/public/ObjDetectPanel.jsx | 205 ++++++++++++++++++ .../objdetect/public/ObjDetectPanel.scss | 72 ++++++ coreplugins/objdetect/public/icon.svg | 60 +++++ coreplugins/objdetect/public/main.js | 14 ++ package.json | 2 +- requirements.txt | 1 + 12 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 coreplugins/objdetect/__init__.py create mode 100644 coreplugins/objdetect/api.py create mode 100644 coreplugins/objdetect/manifest.json create mode 100644 coreplugins/objdetect/plugin.py create mode 100644 coreplugins/objdetect/public/ObjDetect.jsx create mode 100644 coreplugins/objdetect/public/ObjDetect.scss create mode 100644 coreplugins/objdetect/public/ObjDetectPanel.jsx create mode 100644 coreplugins/objdetect/public/ObjDetectPanel.scss create mode 100644 coreplugins/objdetect/public/icon.svg create mode 100644 coreplugins/objdetect/public/main.js diff --git a/coreplugins/objdetect/__init__.py b/coreplugins/objdetect/__init__.py new file mode 100644 index 00000000..48aad58e --- /dev/null +++ b/coreplugins/objdetect/__init__.py @@ -0,0 +1 @@ +from .plugin import * diff --git a/coreplugins/objdetect/api.py b/coreplugins/objdetect/api.py new file mode 100644 index 00000000..026eab61 --- /dev/null +++ b/coreplugins/objdetect/api.py @@ -0,0 +1,122 @@ +import os + +from rest_framework import status +from rest_framework.response import Response +from app.plugins.views import TaskView, CheckTask, GetTaskResult +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): + 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 + + 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): + 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.')}) + + 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)) + + 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)) + + 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) + +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): + pass diff --git a/coreplugins/objdetect/manifest.json b/coreplugins/objdetect/manifest.json new file mode 100644 index 00000000..92839d5f --- /dev/null +++ b/coreplugins/objdetect/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "Object Detect", + "webodmMinVersion": "2.5.8", + "description": "Detect objects using AI in orthophotos", + "version": "1.0.0", + "author": "Piero Toffanin", + "email": "pt@uav4geo.com", + "repository": "https://github.com/OpenDroneMap/WebODM", + "tags": ["object", "detect", "ai"], + "homepage": "https://github.com/OpenDroneMap/WebODM", + "experimental": false, + "deprecated": false +} \ No newline at end of file diff --git a/coreplugins/objdetect/plugin.py b/coreplugins/objdetect/plugin.py new file mode 100644 index 00000000..1dbf7e2e --- /dev/null +++ b/coreplugins/objdetect/plugin.py @@ -0,0 +1,20 @@ +from app.plugins import PluginBase +from app.plugins import MountPoint +# from .api import TaskContoursGenerate +# from .api import TaskContoursCheck +# from .api import TaskContoursDownload + + +class Plugin(PluginBase): + def include_js_files(self): + return ['main.js'] + + def build_jsx_components(self): + return ['ObjDetect.jsx'] + + 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()), + ] \ No newline at end of file diff --git a/coreplugins/objdetect/public/ObjDetect.jsx b/coreplugins/objdetect/public/ObjDetect.jsx new file mode 100644 index 00000000..e494479e --- /dev/null +++ b/coreplugins/objdetect/public/ObjDetect.jsx @@ -0,0 +1,55 @@ +import L from 'leaflet'; +import ReactDOM from 'ReactDOM'; +import React from 'React'; +import PropTypes from 'prop-types'; +import './ObjDetect.scss'; +import ObjDetectPanel from './ObjDetectPanel'; + +class ObjDetectButton extends React.Component { + static propTypes = { + tasks: PropTypes.object.isRequired, + map: PropTypes.object.isRequired + } + + constructor(props){ + super(props); + + this.state = { + showPanel: false + }; + } + + handleOpen = () => { + this.setState({showPanel: true}); + } + + handleClose = () => { + this.setState({showPanel: false}); + } + + render(){ + const { showPanel } = this.state; + + return (
+ + +
); + } +} + +export default L.Control.extend({ + options: { + position: 'topright' + }, + + onAdd: function (map) { + var container = L.DomUtil.create('div', 'leaflet-control-objdetect leaflet-bar leaflet-control'); + L.DomEvent.disableClickPropagation(container); + ReactDOM.render(, container); + + return container; + } +}); + diff --git a/coreplugins/objdetect/public/ObjDetect.scss b/coreplugins/objdetect/public/ObjDetect.scss new file mode 100644 index 00000000..c23b5be5 --- /dev/null +++ b/coreplugins/objdetect/public/ObjDetect.scss @@ -0,0 +1,24 @@ +.leaflet-control-objdetect{ + z-index: 999 !important; + + a.leaflet-control-objdetect-button{ + background: url(icon.svg) no-repeat 0 0; + background-size: 26px 26px; + border-radius: 2px; + } + + div.objdetect-panel{ display: none; } + + .open{ + a.leaflet-control-objdetect-button{ + display: none; + } + + div.objdetect-panel{ + display: block; + } + } +} +.leaflet-touch .leaflet-control-objdetect a { + background-position: 2px 2px; +} diff --git a/coreplugins/objdetect/public/ObjDetectPanel.jsx b/coreplugins/objdetect/public/ObjDetectPanel.jsx new file mode 100644 index 00000000..946b44f3 --- /dev/null +++ b/coreplugins/objdetect/public/ObjDetectPanel.jsx @@ -0,0 +1,205 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Storage from 'webodm/classes/Storage'; +import L from 'leaflet'; +import './ObjDetectPanel.scss'; +import ErrorMessage from 'webodm/components/ErrorMessage'; +import Workers from 'webodm/classes/Workers'; +import { _ } from 'webodm/classes/gettext'; + +export default class ObjDetectPanel extends React.Component { + static defaultProps = { + }; + static propTypes = { + onClose: PropTypes.func.isRequired, + tasks: PropTypes.object.isRequired, + isShowed: PropTypes.bool.isRequired, + map: PropTypes.object.isRequired + } + + constructor(props){ + super(props); + + this.state = { + error: "", + permanentError: "", + model: Storage.getItem("last_objdetect_model") || "cars", + loading: true, + task: props.tasks[0] || null, + detecting: false, + objLayer: null, + }; + } + + componentDidMount(){ + } + + componentDidUpdate(){ + if (this.props.isShowed && this.state.loading){ + const {id, project} = this.state.task; + + this.loadingReq = $.getJSON(`/api/projects/${project}/tasks/${id}/`) + .done(res => { + const { available_assets } = res; + if (available_assets.indexOf("orthophoto.tif") === -1){ + this.setState({permanentError: _("No orthophoto is available. To use object detection you need an orthophoto.")}); + } + }) + .fail(() => { + this.setState({permanentError: _("Cannot retrieve information for task. Are you are connected to the internet?")}) + }) + .always(() => { + this.setState({loading: false}); + this.loadingReq = null; + }); + } + } + + componentWillUnmount(){ + if (this.loadingReq){ + this.loadingReq.abort(); + this.loadingReq = null; + } + if (this.detectReq){ + this.detectReq.abort(); + this.detectReq = null; + } + } + + handleSelectModel = e => { + this.setState({model: e.target.value}); + } + + getFormValues = () => { + const { model } = this.state; + + return { + model + }; + } + + addGeoJSONFromURL = (url, cb) => { + const { map } = this.props; + + $.getJSON(url) + .done((geojson) => { + 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); + + cb(); + }catch(e){ + cb(e.message); + } + }) + .fail(cb); + } + + handleRemoveObjLayer = () => { + const { map } = this.props; + + if (this.state.objLayer){ + map.removeLayer(this.state.objLayer); + this.setState({objLayer: null}); + } + } + + saveInputValues = () => { + // Save settings + Storage.setItem("last_objdetect_model", this.state.model); + } + + handleDetect = () => { + this.setState({detecting: true, error: ""}); + const taskId = this.state.task.id; + this.saveInputValues(); + + this.detectReq = $.ajax({ + type: 'POST', + url: `/api/plugins/objdetect/task/${taskId}/detect`, + data: this.getFormValues() + }).done(result => { + if (result.celery_task_id){ + 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}`; + + this.addGeoJSONFromURL(fileUrl, e => { + if (e) this.setState({error: JSON.stringify(e)}); + this.setState({detecting: false}); + }); + } + }, `/api/plugins/objdetect/task/${taskId}/check/`); + }else if (result.error){ + this.setState({detecting: false, error: result.error}); + }else{ + this.setState({detecting: false, error: "Invalid response: " + result}); + } + }).fail(error => { + this.setState({detecting: false, error: JSON.stringify(error)}); + }); + } + + render(){ + const { loading, permanentError, objLayer, detecting, model } = this.state; + const models = [ + {label: _('Cars'), value: 'cars'}, + {label: _('Trees'), value: 'trees'}, + ] + + let content = ""; + if (loading) content = ( {_("Loading…")}); + else if (permanentError) content = (
{permanentError}
); + else{ + content = (
+ +
+ +
+ +
+
+ +
+
+ {objLayer ? + + : ""} +
+
+ +
+
+
); + } + + return (
+ +
{_("Object Detection")}
+
+ {content} +
); + } +} diff --git a/coreplugins/objdetect/public/ObjDetectPanel.scss b/coreplugins/objdetect/public/ObjDetectPanel.scss new file mode 100644 index 00000000..47f8929d --- /dev/null +++ b/coreplugins/objdetect/public/ObjDetectPanel.scss @@ -0,0 +1,72 @@ +.leaflet-control-objdetect .objdetect-panel{ + padding: 6px 10px 6px 6px; + background: #fff; + min-width: 250px; + max-width: 300px; + + .close-button{ + display: inline-block; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAkUlEQVR4AZWRxQGDUBAFJ9pMflNIP/iVSkIb2wgccXd7g7O+3JXCQUgqBAfFSl8CMooJGQHfuUlEwZpoahZQ7ODTSXWJQkxyioock7BL2tXmdF4moJNX6IDZfbUBQNrX7qfeXfPuqwBAQjEz60w64htGJ+luFH48gt+NYe6v5b/cnr9asM+HlRQ2Qlwh2CjuqQQ9vKsKTwhQ1wAAAABJRU5ErkJggg==); + height: 18px; + width: 18px; + margin-right: 0; + float: right; + vertical-align: middle; + text-align: right; + margin-top: 0px; + margin-left: 16px; + position: relative; + left: 2px; + + &:hover{ + opacity: 0.7; + cursor: pointer; + } + } + + .title{ + font-size: 120%; + margin-right: 60px; + } + + hr{ + clear: both; + margin: 6px 0px; + border-color: #ddd; + } + + label{ + padding-top: 5px; + } + + select, input{ + height: auto; + padding: 4px; + } + + *{ + font-size: 12px; + } + + .row.form-group.form-inline{ + margin-bottom: 8px; + } + + .dropdown-menu{ + a{ + width: 100%; + text-align: left; + display: block; + padding-top: 0; + padding-bottom: 0; + } + } + + .btn-detect{ + margin-right: 8px; + } + + .action-buttons{ + margin-top: 12px; + } +} diff --git a/coreplugins/objdetect/public/icon.svg b/coreplugins/objdetect/public/icon.svg new file mode 100644 index 00000000..67497f59 --- /dev/null +++ b/coreplugins/objdetect/public/icon.svg @@ -0,0 +1,60 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/coreplugins/objdetect/public/main.js b/coreplugins/objdetect/public/main.js new file mode 100644 index 00000000..250fd49e --- /dev/null +++ b/coreplugins/objdetect/public/main.js @@ -0,0 +1,14 @@ +PluginsAPI.Map.willAddControls([ + 'objdetect/build/ObjDetect.js', + 'objdetect/build/ObjDetect.css' + ], function(args, ObjDetect){ + var tasks = []; + for (var i = 0; i < args.tiles.length; i++){ + tasks.push(args.tiles[i].meta.task); + } + + // TODO: add support for map view where multiple tasks are available? + if (tasks.length === 1){ + args.map.addControl(new ObjDetect({map: args.map, tasks: tasks})); + } +}); diff --git a/package.json b/package.json index 6edfe846..f53ba4c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "2.5.7", + "version": "2.5.8", "description": "User-friendly, extendable application and API for processing aerial imagery.", "main": "index.js", "scripts": { diff --git a/requirements.txt b/requirements.txt index 945db922..5dbf8bc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ drf-nested-routers==0.11.1 funcsigs==1.0.2 futures==3.1.1 gunicorn==19.8.0 +geodeep==0.9.4 itypes==1.1.0 kombu==4.6.7 Markdown==3.3.4 From 27074d71075343a746a9d47740c39c9e3fec0002 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Jan 2025 11:43:19 -0500 Subject: [PATCH 03/16] PoC object detection working --- app/plugins/views.py | 1 + coreplugins/objdetect/api.py | 134 ++++-------------- coreplugins/objdetect/plugin.py | 12 +- .../objdetect/public/ObjDetectPanel.jsx | 67 +++++---- .../objdetect/public/ObjDetectPanel.scss | 4 - 5 files changed, 73 insertions(+), 145 deletions(-) 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; } From 30c9d274a16c075e51b1508f6964a9c2cfe3df9b Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Jan 2025 12:20:53 -0500 Subject: [PATCH 04/16] UI improvements --- coreplugins/measure/public/MeasurePopup.jsx | 4 +- .../objdetect/public/ObjDetectPanel.jsx | 41 +++++++++++-------- .../objdetect/public/ObjDetectPanel.scss | 22 ++++++++++ 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/coreplugins/measure/public/MeasurePopup.jsx b/coreplugins/measure/public/MeasurePopup.jsx index 235b5f93..93679644 100644 --- a/coreplugins/measure/public/MeasurePopup.jsx +++ b/coreplugins/measure/public/MeasurePopup.jsx @@ -68,7 +68,7 @@ export default class MeasurePopup extends React.Component { } getGeoJSON(){ - const geoJSON = this.props.resultFeature.toGeoJSON(); + const geoJSON = this.props.resultFeature.toGeoJSON(14); geoJSON.properties = this.getProperties(); return geoJSON; } @@ -125,7 +125,7 @@ export default class MeasurePopup extends React.Component { type: 'POST', url: `/api/plugins/measure/task/${task.id}/volume`, data: JSON.stringify({ - area: this.props.resultFeature.toGeoJSON(), + area: this.props.resultFeature.toGeoJSON(14), method: baseMethod }), contentType: "application/json" diff --git a/coreplugins/objdetect/public/ObjDetectPanel.jsx b/coreplugins/objdetect/public/ObjDetectPanel.jsx index 154e9b13..fcd9743c 100644 --- a/coreplugins/objdetect/public/ObjDetectPanel.jsx +++ b/coreplugins/objdetect/public/ObjDetectPanel.jsx @@ -5,6 +5,7 @@ import L from 'leaflet'; import './ObjDetectPanel.scss'; import ErrorMessage from 'webodm/components/ErrorMessage'; import Workers from 'webodm/classes/Workers'; +import Utils from 'webodm/classes/Utils'; import { _ } from 'webodm/classes/gettext'; export default class ObjDetectPanel extends React.Component { @@ -86,10 +87,10 @@ export default class ObjDetectPanel extends React.Component { this.setState({objLayer: L.geoJSON(geojson, { onEachFeature: (feature, layer) => { - if (feature.properties && feature.properties.level !== undefined) { + if (feature.properties && feature.properties['class'] !== undefined) { layer.bindPopup(`
- ${_("Class:")} ${feature.properties.class}
- ${_("Score:")} ${feature.properties.score}
+ ${_("Label:")} ${feature.properties['class']}
+ ${_("Confidence:")} ${feature.properties.score.toFixed(3)}
`); } @@ -100,6 +101,7 @@ export default class ObjDetectPanel extends React.Component { } })}); this.state.objLayer.addTo(map); + this.state.objLayer.label = this.state.model; cb(); }catch(e){ @@ -162,6 +164,10 @@ export default class ObjDetectPanel extends React.Component { }); } + handleDownload = () => { + Utils.saveAs(JSON.stringify(this.state.objLayer.toGeoJSON(14), null, 4), `${this.state.objLayer.label || "objects"}.geojson`); + } + render(){ const { loading, permanentError, objLayer, detecting, model } = this.state; const models = [ @@ -175,28 +181,29 @@ export default class ObjDetectPanel extends React.Component { else{ content = (
-
- -
+
-
-
- -
-
- {objLayer ? - - : ""} -
-
-
+ + {objLayer ?
+ {_("Count:")} {objLayer.getLayers().length} +
+ + +
+
: ""}
); } diff --git a/coreplugins/objdetect/public/ObjDetectPanel.scss b/coreplugins/objdetect/public/ObjDetectPanel.scss index 1cfe68b7..8a008a95 100644 --- a/coreplugins/objdetect/public/ObjDetectPanel.scss +++ b/coreplugins/objdetect/public/ObjDetectPanel.scss @@ -37,6 +37,9 @@ label{ padding-top: 5px; + &.no-pad{ + padding-top: 0; + } } select, input{ @@ -52,6 +55,25 @@ margin-bottom: 8px; } + .model-selector{ + display: flex; + padding-left: 15px; + padding-right: 15px; + select{ + margin-right: 8px; + } + } + + .detect-action-buttons{ + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 8px; + .btn-download{ + margin-right: 6px; + } + } + .dropdown-menu{ a{ width: 100%; From 85c9df12650b14bb121016ef9d919e28c161db30 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Jan 2025 12:26:06 -0500 Subject: [PATCH 05/16] Fix count 0 cases --- coreplugins/objdetect/public/ObjDetectPanel.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/coreplugins/objdetect/public/ObjDetectPanel.jsx b/coreplugins/objdetect/public/ObjDetectPanel.jsx index fcd9743c..07d1f004 100644 --- a/coreplugins/objdetect/public/ObjDetectPanel.jsx +++ b/coreplugins/objdetect/public/ObjDetectPanel.jsx @@ -174,11 +174,13 @@ export default class ObjDetectPanel extends React.Component { {label: _('Cars'), value: 'cars'}, {label: _('Trees'), value: 'trees'}, ] - + let content = ""; if (loading) content = ( {_("Loading…")}); else if (permanentError) content = (
{permanentError}
); else{ + const featCount = objLayer ? objLayer.getLayers().length : 0; + content = (
@@ -192,12 +194,12 @@ export default class ObjDetectPanel extends React.Component {
{objLayer ?
- {_("Count:")} {objLayer.getLayers().length} + {_("Count:")} {featCount}
- + : ""}
From 766d516e1952bfaf9a7db6a15729f8f181d34133 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Jan 2025 14:52:12 -0500 Subject: [PATCH 07/16] Bump version --- coreplugins/objdetect/manifest.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coreplugins/objdetect/manifest.json b/coreplugins/objdetect/manifest.json index 92839d5f..81e1932c 100644 --- a/coreplugins/objdetect/manifest.json +++ b/coreplugins/objdetect/manifest.json @@ -1,6 +1,6 @@ { "name": "Object Detect", - "webodmMinVersion": "2.5.8", + "webodmMinVersion": "2.6.0", "description": "Detect objects using AI in orthophotos", "version": "1.0.0", "author": "Piero Toffanin", diff --git a/package.json b/package.json index f53ba4c9..83713b6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "2.5.8", + "version": "2.6.0", "description": "User-friendly, extendable application and API for processing aerial imagery.", "main": "index.js", "scripts": { From c3c2dfb784633734c87217e4e2f1d180bba3f99f Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Jan 2025 15:41:05 -0500 Subject: [PATCH 08/16] Fix test --- worker/celery.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worker/celery.py b/worker/celery.py index 7903f2b5..a604a58c 100644 --- a/worker/celery.py +++ b/worker/celery.py @@ -66,6 +66,8 @@ app.conf.beat_schedule = { class MockAsyncResult: def __init__(self, celery_task_id, result = None): self.celery_task_id = celery_task_id + self.state = "PENDING" + if result is None: if celery_task_id == 'bogus': self.result = None From 320bbafb0beeaa0b4c1324766ba72185dfcc22fa Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Jan 2025 16:05:57 -0500 Subject: [PATCH 09/16] Add none check --- app/api/workers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/workers.py b/app/api/workers.py index 60094475..308c30d0 100644 --- a/app/api/workers.py +++ b/app/api/workers.py @@ -20,7 +20,7 @@ class CheckTask(APIView): out = {'ready': False} # Copy progress meta - if res.state == "PROGRESS": + if res.state == "PROGRESS" and res.info is not None: for k in res.info: out[k] = res.info[k] From 46bdb258245352f0ba4b6ea7bcccc88d2eead9e3 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Jan 2025 16:42:47 -0500 Subject: [PATCH 10/16] 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 From e5b146693d0eae738f4ec4ca43c1d8dd2fe3f6f9 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Jan 2025 16:45:48 -0500 Subject: [PATCH 11/16] Dont parse float --- app/static/app/js/components/ExportAssetPanel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/app/js/components/ExportAssetPanel.jsx b/app/static/app/js/components/ExportAssetPanel.jsx index 483b5f57..0d66d3e5 100644 --- a/app/static/app/js/components/ExportAssetPanel.jsx +++ b/app/static/app/js/components/ExportAssetPanel.jsx @@ -99,7 +99,7 @@ export default class ExportAssetPanel extends React.Component { } handleChangeResample = e => { - this.setState({resample: parseFloat(e.target.value) || 0}); + this.setState({resample: e.target.value}); } getExportParams = (format) => { From 85c56df2e71b215a2904adcf614d88c4d37607ba Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 27 Jan 2025 13:44:39 -0500 Subject: [PATCH 12/16] Add worker max threads --- coreplugins/objdetect/api.py | 2 +- requirements.txt | 2 +- webodm/settings.py | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/coreplugins/objdetect/api.py b/coreplugins/objdetect/api.py index 5bc6f1c5..b3161948 100644 --- a/coreplugins/objdetect/api.py +++ b/coreplugins/objdetect/api.py @@ -17,7 +17,7 @@ def detect(orthophoto, model, progress_callback=None): return {'error': "GeoDeep library is missing"} try: - return {'output': gdetect(orthophoto, model, output_type='geojson', progress_callback=progress_callback)} + return {'output': gdetect(orthophoto, model, output_type='geojson', max_threads=settings.WORKERS_MAX_THREADS, progress_callback=progress_callback)} except Exception as e: return {'error': str(e)} diff --git a/requirements.txt b/requirements.txt index 5dbf8bc3..d3db17e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ drf-nested-routers==0.11.1 funcsigs==1.0.2 futures==3.1.1 gunicorn==19.8.0 -geodeep==0.9.4 +geodeep==0.9.7 itypes==1.1.0 kombu==4.6.7 Markdown==3.3.4 diff --git a/webodm/settings.py b/webodm/settings.py index 112b9d52..50cfcea4 100644 --- a/webodm/settings.py +++ b/webodm/settings.py @@ -384,6 +384,9 @@ UI_MAX_PROCESSING_NODES = None # are removed (or None to disable) CLEANUP_PARTIAL_TASKS = 72 +# Maximum number of threads that a worker should use for processing +WORKERS_MAX_THREADS = 1 + # Link to GCP docs GCP_DOCS_LINK = "https://docs.opendronemap.org/gcp/#gcp-file-format" From 18e34830dfea28f0661b77a7d19a0c98dd0c5846 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 29 Jan 2025 22:40:51 -0500 Subject: [PATCH 13/16] Update GeoDeep, more object detection classes --- coreplugins/objdetect/api.py | 18 ++++++++++++++---- .../objdetect/public/ObjDetectPanel.jsx | 5 ++++- requirements.txt | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/coreplugins/objdetect/api.py b/coreplugins/objdetect/api.py index b3161948..970b78d7 100644 --- a/coreplugins/objdetect/api.py +++ b/coreplugins/objdetect/api.py @@ -6,7 +6,7 @@ from app.plugins.views import TaskView, GetTaskResult, TaskResultOutputError from app.plugins.worker import run_function_async from django.utils.translation import gettext_lazy as _ -def detect(orthophoto, model, progress_callback=None): +def detect(orthophoto, model, classes=None, progress_callback=None): import os from webodm import settings @@ -17,7 +17,7 @@ def detect(orthophoto, model, progress_callback=None): return {'error': "GeoDeep library is missing"} try: - return {'output': gdetect(orthophoto, model, output_type='geojson', max_threads=settings.WORKERS_MAX_THREADS, progress_callback=progress_callback)} + return {'output': gdetect(orthophoto, model, output_type='geojson', classes=classes, max_threads=settings.WORKERS_MAX_THREADS, progress_callback=progress_callback)} except Exception as e: return {'error': str(e)} @@ -31,10 +31,20 @@ class TaskObjDetect(TaskView): orthophoto = os.path.abspath(task.get_asset_download_path("orthophoto.tif")) model = request.data.get('model', 'cars') - if not model in ['cars', 'trees']: + # model --> (modelID, classes) + model_map = { + 'cars': ('cars', None), + 'trees': ('trees', None), + 'athletic': ('aerovision', ['tennis-court', 'track-field', 'soccer-field', 'baseball-field', 'swimming-pool', 'basketball-court']), + 'boats': ('aerovision', ['boat']), + 'planes': ('aerovision', ['plane']), + } + + if not model in model_map: return Response({'error': 'Invalid model'}, status=status.HTTP_200_OK) - celery_task_id = run_function_async(detect, orthophoto, model, with_progress=True).task_id + model_id, classes = model_map[model] + celery_task_id = run_function_async(detect, orthophoto, model_id, classes, with_progress=True).task_id return Response({'celery_task_id': celery_task_id}, status=status.HTTP_200_OK) diff --git a/coreplugins/objdetect/public/ObjDetectPanel.jsx b/coreplugins/objdetect/public/ObjDetectPanel.jsx index 695b9f6b..02c003e6 100644 --- a/coreplugins/objdetect/public/ObjDetectPanel.jsx +++ b/coreplugins/objdetect/public/ObjDetectPanel.jsx @@ -175,7 +175,10 @@ export default class ObjDetectPanel extends React.Component { const { loading, permanentError, objLayer, detecting, model, progress } = this.state; const models = [ {label: _('Cars'), value: 'cars'}, - {label: _('Trees'), value: 'trees'}, + {label: _('Trees'), value: 'trees'}, + {label: _('Athletic Facilities'), value: 'athletic'}, + {label: _('Boats'), value: 'boats'}, + {label: _('Planes'), value: 'planes'} ] let content = ""; diff --git a/requirements.txt b/requirements.txt index d3db17e5..61abce0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ drf-nested-routers==0.11.1 funcsigs==1.0.2 futures==3.1.1 gunicorn==19.8.0 -geodeep==0.9.7 +geodeep==0.9.8 itypes==1.1.0 kombu==4.6.7 Markdown==3.3.4 From 4dd1b07c2ee70e24413f1b6199fd8b3ef2432376 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 30 Jan 2025 00:33:56 -0500 Subject: [PATCH 14/16] Remove obj layer on new detect --- coreplugins/objdetect/public/ObjDetectPanel.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/coreplugins/objdetect/public/ObjDetectPanel.jsx b/coreplugins/objdetect/public/ObjDetectPanel.jsx index 02c003e6..9f577f67 100644 --- a/coreplugins/objdetect/public/ObjDetectPanel.jsx +++ b/coreplugins/objdetect/public/ObjDetectPanel.jsx @@ -125,6 +125,7 @@ export default class ObjDetectPanel extends React.Component { } handleDetect = () => { + this.handleRemoveObjLayer(); this.setState({detecting: true, error: "", progress: null}); const taskId = this.state.task.id; this.saveInputValues(); From 25bbadfc98b756c10a3d59b1539323f18d91f905 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 31 Jan 2025 02:34:36 -0500 Subject: [PATCH 15/16] Remove trees from detection list --- coreplugins/objdetect/public/ObjDetectPanel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coreplugins/objdetect/public/ObjDetectPanel.jsx b/coreplugins/objdetect/public/ObjDetectPanel.jsx index 9f577f67..3c7f899b 100644 --- a/coreplugins/objdetect/public/ObjDetectPanel.jsx +++ b/coreplugins/objdetect/public/ObjDetectPanel.jsx @@ -176,7 +176,7 @@ export default class ObjDetectPanel extends React.Component { const { loading, permanentError, objLayer, detecting, model, progress } = this.state; const models = [ {label: _('Cars'), value: 'cars'}, - {label: _('Trees'), value: 'trees'}, + // {label: _('Trees'), value: 'trees'}, {label: _('Athletic Facilities'), value: 'athletic'}, {label: _('Boats'), value: 'boats'}, {label: _('Planes'), value: 'planes'} From 1bc4ab00f1918d17fc23faa4d77a2bac841cad66 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Sun, 2 Feb 2025 23:46:46 -0500 Subject: [PATCH 16/16] Remove legacy requirements from list --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 4e148f90..4a03f749 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,6 @@ To install WebODM manually on your machine with docker: ### Requirements - [Git](https://git-scm.com/downloads) - [Docker](https://www.docker.com/) - - [Docker-compose](https://docs.docker.com/compose/install/) - - Python - - Pip * Windows users should install [Docker Desktop](https://hub.docker.com/editions/community/docker-ce-desktop-windows) and : 1. make sure Linux containers are enabled (Switch to Linux Containers...)