pull/1597/head
Piero Toffanin 2025-02-06 11:24:04 +01:00
commit f029a7d83c
28 zmienionych plików z 637 dodań i 36 usunięć

Wyświetl plik

@ -67,9 +67,6 @@ To install WebODM manually on your machine with docker:
### Requirements ### Requirements
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
- [Docker](https://www.docker.com/) - [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 : * 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...) 1. make sure Linux containers are enabled (Switch to Linux Containers...)

Wyświetl plik

@ -524,6 +524,7 @@ class Export(TaskNestedView):
epsg = request.data.get('epsg') epsg = request.data.get('epsg')
color_map = request.data.get('color_map') color_map = request.data.get('color_map')
hillshade = request.data.get('hillshade') hillshade = request.data.get('hillshade')
resample = request.data.get('resample')
if formula == '': formula = None if formula == '': formula = None
if bands == '': bands = None if bands == '': bands = None
@ -531,6 +532,7 @@ class Export(TaskNestedView):
if epsg == '': epsg = None if epsg == '': epsg = None
if color_map == '': color_map = None if color_map == '': color_map = None
if hillshade == '': hillshade = None if hillshade == '': hillshade = None
if resample == '': resample = None
expr = None expr = None
@ -552,6 +554,12 @@ class Export(TaskNestedView):
except InvalidColorMapName: except InvalidColorMapName:
raise exceptions.ValidationError(_("Not a valid color_map value")) 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: if epsg is not None:
try: try:
epsg = int(epsg) epsg = int(epsg)
@ -627,9 +635,10 @@ class Export(TaskNestedView):
return Response({'celery_task_id': celery_task_id, 'filename': filename}) return Response({'celery_task_id': celery_task_id, 'filename': filename})
elif asset_type == 'georeferenced_model': elif asset_type == 'georeferenced_model':
# Shortcut the process if no processing is required # 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}) return Response({'url': '/api/projects/{}/tasks/{}/download/{}.laz'.format(task.project.id, task.id, asset_type), 'filename': filename})
else: else:
celery_task_id = export_pointcloud.delay(url, epsg=epsg, 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}) return Response({'celery_task_id': celery_task_id, 'filename': filename})

Wyświetl plik

@ -17,7 +17,14 @@ class CheckTask(APIView):
res = TestSafeAsyncResult(celery_task_id) res = TestSafeAsyncResult(celery_task_id)
if not res.ready(): if not res.ready():
return Response({'ready': False}, status=status.HTTP_200_OK) out = {'ready': False}
# Copy progress meta
if res.state == "PROGRESS" and res.info is not None:
for k in res.info:
out[k] = res.info[k]
return Response(out, status=status.HTTP_200_OK)
else: else:
result = res.get() result = res.get()
@ -29,6 +36,9 @@ class CheckTask(APIView):
msg = self.on_error(result) msg = self.on_error(result)
return Response({'ready': True, 'error': msg}) return Response({'ready': True, 'error': msg})
if isinstance(result.get('file'), str) and not os.path.isfile(result.get('file')):
return Response({'ready': True, 'error': "Cannot generate file"})
return Response({'ready': True}) return Response({'ready': True})
def on_error(self, result): def on_error(self, result):

Wyświetl plik

@ -3,6 +3,7 @@ import os
from app.api.tasks import TaskNestedView as TaskView from app.api.tasks import TaskNestedView as TaskView
from app.api.workers import CheckTask as CheckTask from app.api.workers import CheckTask as CheckTask
from app.api.workers import GetTaskResult as GetTaskResult from app.api.workers import GetTaskResult as GetTaskResult
from app.api.workers import TaskResultOutputError
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from .functions import get_plugin_by_name, get_active_plugins from .functions import get_plugin_by_name, get_active_plugins

Wyświetl plik

@ -15,8 +15,8 @@ def run_function_async(func, *args, **kwargs):
return eval_async.delay(source, func.__name__, *args, **kwargs) return eval_async.delay(source, func.__name__, *args, **kwargs)
@app.task @app.task(bind=True)
def eval_async(source, funcname, *args, **kwargs): def eval_async(self, source, funcname, *args, **kwargs):
""" """
Run Python code asynchronously using Celery. Run Python code asynchronously using Celery.
It's recommended to use run_function_async instead. It's recommended to use run_function_async instead.
@ -24,4 +24,11 @@ def eval_async(source, funcname, *args, **kwargs):
ns = {} ns = {}
code = compile(source, 'file', 'exec') code = compile(source, 'file', 'exec')
eval(code, ns, ns) eval(code, ns, ns)
if kwargs.get("with_progress"):
def progress_callback(status, perc):
self.update_state(state="PROGRESS", meta={"status": status, "progress": perc})
kwargs['progress_callback'] = progress_callback
del kwargs['with_progress']
return ns[funcname](*args, **kwargs) return ns[funcname](*args, **kwargs)

Wyświetl plik

@ -10,19 +10,24 @@ logger = logging.getLogger('app.logger')
def export_pointcloud(input, output, **opts): def export_pointcloud(input, output, **opts):
epsg = opts.get('epsg') epsg = opts.get('epsg')
export_format = opts.get('format') export_format = opts.get('format')
resample = float(opts.get('resample', 0))
resample_args = []
reprojection_args = [] reprojection_args = []
extra_args = [] extra_args = []
if epsg: if epsg:
reprojection_args = ["reprojection", reprojection_args = ["reprojection",
"--filters.reprojection.out_srs=%s" % double_quote("EPSG:" + str(epsg))] "--filters.reprojection.out_srs=%s" % double_quote("EPSG:" + str(epsg))]
if export_format == "ply": if export_format == "ply":
extra_args = ['--writers.ply.sized_types', 'false', extra_args = ['--writers.ply.sized_types', 'false',
'--writers.ply.storage_mode', 'little endian'] '--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): def is_pointcloud_georeferenced(laz_path):

Wyświetl plik

@ -1,7 +1,8 @@
import $ from 'jquery'; import $ from 'jquery';
export default { export default {
waitForCompletion: (celery_task_id, cb, checkUrl = "/api/workers/check/") => { waitForCompletion: (celery_task_id, cb, progress_cb) => {
const checkUrl = "/api/workers/check/";
let errorCount = 0; let errorCount = 0;
let url = checkUrl + celery_task_id; let url = checkUrl + celery_task_id;
@ -15,6 +16,9 @@ export default {
}else if (result.ready){ }else if (result.ready){
cb(); cb();
}else{ }else{
if (typeof progress_cb === "function" && result.progress !== undefined && result.status !== undefined){
progress_cb(result.status, result.progress);
}
// Retry // Retry
setTimeout(() => check(), 2000); setTimeout(() => check(), 2000);
} }

Wyświetl plik

@ -75,6 +75,7 @@ export default class ExportAssetPanel extends React.Component {
format: props.exportFormats[0], format: props.exportFormats[0],
epsg: this.props.task.epsg || null, epsg: this.props.task.epsg || null,
customEpsg: Storage.getItem("last_export_custom_epsg") || "4326", customEpsg: Storage.getItem("last_export_custom_epsg") || "4326",
resample: 0,
exporting: false exporting: false
} }
} }
@ -97,6 +98,10 @@ export default class ExportAssetPanel extends React.Component {
this.setState({customEpsg: e.target.value}); this.setState({customEpsg: e.target.value});
} }
handleChangeResample = e => {
this.setState({resample: e.target.value});
}
getExportParams = (format) => { getExportParams = (format) => {
let params = {}; let params = {};
@ -111,9 +116,15 @@ export default class ExportAssetPanel extends React.Component {
const epsg = this.getEpsg(); const epsg = this.getEpsg();
if (epsg) params.epsg = this.getEpsg(); if (epsg) params.epsg = this.getEpsg();
if (this.state.resample > 0) params.resample = this.state.resample;
return params; return params;
} }
isPointCloud = () => {
return this.props.asset == "georeferenced_model";
}
handleExport = (format) => { handleExport = (format) => {
if (!format) format = this.state.format; if (!format) format = this.state.format;
@ -171,7 +182,7 @@ export default class ExportAssetPanel extends React.Component {
} }
render(){ render(){
const {epsg, customEpsg, exporting, format } = this.state; const {epsg, customEpsg, exporting, format, resample } = this.state;
const { exportFormats } = this.props; const { exportFormats } = this.props;
const utmEPSG = this.props.task.epsg; const utmEPSG = this.props.task.epsg;
@ -200,14 +211,21 @@ export default class ExportAssetPanel extends React.Component {
let exportSelector = null; let exportSelector = null;
if (this.props.selectorOnly){ if (this.props.selectorOnly){
exportSelector = (<div className="row form-group form-inline"> exportSelector = [<div key={1} className="row form-group form-inline">
<label className="col-sm-3 control-label">{_("Format:")}</label> <label className="col-sm-3 control-label">{_("Format:")}</label>
<div className="col-sm-9 "> <div className="col-sm-9 ">
<select className="form-control" value={format} onChange={this.handleSelectFormat}> <select className="form-control" value={format} onChange={this.handleSelectFormat}>
{exportFormats.map(ef => <option key={ef} value={ef}>{this.efInfo[ef].label}</option>)} {exportFormats.map(ef => <option key={ef} value={ef}>{this.efInfo[ef].label}</option>)}
</select> </select>
</div> </div>
</div>); </div>,
this.isPointCloud() ? <div key={2} className="row form-group form-inline">
<label className="col-sm-3 control-label">{_("Resample (meters):")}</label>
<div className="col-sm-9 ">
<input type="number" min="0" className="form-control custom-interval" value={resample} onChange={this.handleChangeResample} />
</div>
</div>
: ""];
}else{ }else{
exportSelector = (<div className="row form-group form-inline"> exportSelector = (<div className="row form-group form-inline">
<label className="col-sm-3 control-label">{_("Export:")}</label> <label className="col-sm-3 control-label">{_("Export:")}</label>

Wyświetl plik

@ -82,7 +82,8 @@ class TestApiTask(BootTransactionTestCase):
('orthophoto', {'formula': 'NDVI', 'bands': 'RGN'}, status.HTTP_200_OK), ('orthophoto', {'formula': 'NDVI', 'bands': 'RGN'}, status.HTTP_200_OK),
('dsm', {'epsg': 4326}, status.HTTP_200_OK), ('dsm', {'epsg': 4326}, status.HTTP_200_OK),
('dtm', {'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 # Cannot export stuff

Wyświetl plik

@ -109,14 +109,6 @@ class TaskContoursGenerate(TaskView):
except ContoursException as e: except ContoursException as e:
return Response({'error': str(e)}, status=status.HTTP_200_OK) 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): class TaskContoursDownload(GetTaskResult):
pass pass

Wyświetl plik

@ -1,7 +1,6 @@
from app.plugins import PluginBase from app.plugins import PluginBase
from app.plugins import MountPoint from app.plugins import MountPoint
from .api import TaskContoursGenerate from .api import TaskContoursGenerate
from .api import TaskContoursCheck
from .api import TaskContoursDownload from .api import TaskContoursDownload
@ -15,6 +14,5 @@ class Plugin(PluginBase):
def api_mount_points(self): def api_mount_points(self):
return [ return [
MountPoint('task/(?P<pk>[^/.]+)/contours/generate', TaskContoursGenerate.as_view()), MountPoint('task/(?P<pk>[^/.]+)/contours/generate', TaskContoursGenerate.as_view()),
MountPoint('task/[^/.]+/contours/check/(?P<celery_task_id>.+)', TaskContoursCheck.as_view()),
MountPoint('task/[^/.]+/contours/download/(?P<celery_task_id>.+)', TaskContoursDownload.as_view()), MountPoint('task/[^/.]+/contours/download/(?P<celery_task_id>.+)', TaskContoursDownload.as_view()),
] ]

Wyświetl plik

@ -241,7 +241,7 @@ export default class ContoursPanel extends React.Component {
this.setState({[loadingProp]: false}); this.setState({[loadingProp]: false});
} }
} }
}, `/api/plugins/contours/task/${taskId}/contours/check/`); });
}else if (result.error){ }else if (result.error){
this.setState({[loadingProp]: false, error: result.error}); this.setState({[loadingProp]: false, error: result.error});
}else{ }else{

Wyświetl plik

@ -2,7 +2,7 @@ import os
from rest_framework import serializers from rest_framework import serializers
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from app.api.workers import GetTaskResult, TaskResultOutputError, CheckTask from app.api.workers import GetTaskResult, TaskResultOutputError
from app.models import Task from app.models import Task
from app.plugins.views import TaskView from app.plugins.views import TaskView
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -34,9 +34,6 @@ class TaskVolume(TaskView):
except Exception as e: except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_200_OK) return Response({'error': str(e)}, status=status.HTTP_200_OK)
class TaskVolumeCheck(CheckTask):
pass
class TaskVolumeResult(GetTaskResult): class TaskVolumeResult(GetTaskResult):
def get(self, request, pk=None, celery_task_id=None): def get(self, request, pk=None, celery_task_id=None):
task = Task.objects.only('dsm_extent').get(pk=pk) task = Task.objects.only('dsm_extent').get(pk=pk)

Wyświetl plik

@ -1,6 +1,6 @@
from app.plugins import MountPoint from app.plugins import MountPoint
from app.plugins import PluginBase from app.plugins import PluginBase
from .api import TaskVolume, TaskVolumeCheck, TaskVolumeResult from .api import TaskVolume, TaskVolumeResult
class Plugin(PluginBase): class Plugin(PluginBase):
def include_js_files(self): def include_js_files(self):
@ -12,6 +12,5 @@ class Plugin(PluginBase):
def api_mount_points(self): def api_mount_points(self):
return [ return [
MountPoint('task/(?P<pk>[^/.]+)/volume$', TaskVolume.as_view()), MountPoint('task/(?P<pk>[^/.]+)/volume$', TaskVolume.as_view()),
MountPoint('task/[^/.]+/volume/check/(?P<celery_task_id>.+)$', TaskVolumeCheck.as_view()),
MountPoint('task/(?P<pk>[^/.]+)/volume/get/(?P<celery_task_id>.+)$', TaskVolumeResult.as_view()), MountPoint('task/(?P<pk>[^/.]+)/volume/get/(?P<celery_task_id>.+)$', TaskVolumeResult.as_view()),
] ]

Wyświetl plik

@ -68,7 +68,7 @@ export default class MeasurePopup extends React.Component {
} }
getGeoJSON(){ getGeoJSON(){
const geoJSON = this.props.resultFeature.toGeoJSON(); const geoJSON = this.props.resultFeature.toGeoJSON(14);
geoJSON.properties = this.getProperties(); geoJSON.properties = this.getProperties();
return geoJSON; return geoJSON;
} }
@ -125,7 +125,7 @@ export default class MeasurePopup extends React.Component {
type: 'POST', type: 'POST',
url: `/api/plugins/measure/task/${task.id}/volume`, url: `/api/plugins/measure/task/${task.id}/volume`,
data: JSON.stringify({ data: JSON.stringify({
area: this.props.resultFeature.toGeoJSON(), area: this.props.resultFeature.toGeoJSON(14),
method: baseMethod method: baseMethod
}), }),
contentType: "application/json" contentType: "application/json"
@ -139,7 +139,7 @@ export default class MeasurePopup extends React.Component {
else this.setState({volume: parseFloat(volume)}); else this.setState({volume: parseFloat(volume)});
}, `/api/plugins/measure/task/${task.id}/volume/get/`); }, `/api/plugins/measure/task/${task.id}/volume/get/`);
} }
}, `/api/plugins/measure/task/${task.id}/volume/check/`); });
}else if (result.error){ }else if (result.error){
this.setState({error: result.error}); this.setState({error: result.error});
}else{ }else{

Wyświetl plik

@ -0,0 +1 @@
from .plugin import *

Wyświetl plik

@ -0,0 +1,56 @@
import os
import json
from rest_framework import status
from rest_framework.response import Response
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, classes=None, progress_callback=None):
import os
from webodm import settings
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"}
try:
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)}
class TaskObjDetect(TaskView):
def post(self, request, pk=None):
task = self.get_and_check_task(request, pk)
if task.orthophoto_extent is None:
return Response({'error': _('No orthophoto is available.')})
orthophoto = os.path.abspath(task.get_asset_download_path("orthophoto.tif"))
model = request.data.get('model', 'cars')
# 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)
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)
class TaskObjDownload(GetTaskResult):
def handle_output(self, output, result, **kwargs):
try:
return json.loads(output)
except:
raise TaskResultOutputError("Invalid GeoJSON")

Wyświetl plik

@ -0,0 +1,13 @@
{
"name": "Object Detect",
"webodmMinVersion": "2.6.0",
"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
}

Wyświetl plik

@ -0,0 +1,18 @@
from app.plugins import PluginBase
from app.plugins import MountPoint
from .api import TaskObjDetect
from .api import TaskObjDownload
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<pk>[^/.]+)/detect', TaskObjDetect.as_view()),
MountPoint('task/[^/.]+/download/(?P<celery_task_id>.+)', TaskObjDownload.as_view()),
]

Wyświetl plik

@ -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 (<div className={showPanel ? "open" : ""}>
<a href="javascript:void(0);"
onClick={this.handleOpen}
className="leaflet-control-objdetect-button leaflet-bar-part theme-secondary"></a>
<ObjDetectPanel map={this.props.map} isShowed={showPanel} tasks={this.props.tasks} onClose={this.handleClose} />
</div>);
}
}
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(<ObjDetectButton map={this.options.map} tasks={this.options.tasks} />, container);
return container;
}
});

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -0,0 +1,226 @@
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 Utils from 'webodm/classes/Utils';
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,
progress: null,
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
};
}
addGeoJSON = (geojson, cb) => {
const { map } = this.props;
try{
this.handleRemoveObjLayer();
this.setState({objLayer: L.geoJSON(geojson, {
onEachFeature: (feature, layer) => {
if (feature.properties && feature.properties['class'] !== undefined) {
layer.bindPopup(`<div style="margin-right: 32px;">
<b>${_("Label:")}</b> ${feature.properties['class']}<br/>
<b>${_("Confidence:")}</b> ${feature.properties.score.toFixed(3)}<br/>
</div>
`);
}
},
style: feature => {
// TODO: different colors for different elevations?
return {color: "red"};
}
})});
this.state.objLayer.addTo(map);
this.state.objLayer.label = this.state.model;
cb();
}catch(e){
cb(e.message);
}
}
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.handleRemoveObjLayer();
this.setState({detecting: true, error: "", progress: null});
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{
Workers.getOutput(result.celery_task_id, (error, geojson) => {
try{
geojson = JSON.parse(geojson);
}catch(e){
error = "Invalid GeoJSON";
}
if (error) this.setState({detecting: false, error});
else{
this.addGeoJSON(geojson, e => {
if (e) this.setState({error: JSON.stringify(e)});
this.setState({detecting: false});
});
}
});
}
}, (_, progress) => {
this.setState({progress});
});
}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)});
});
}
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, progress } = this.state;
const models = [
{label: _('Cars'), value: 'cars'},
// {label: _('Trees'), value: 'trees'},
{label: _('Athletic Facilities'), value: 'athletic'},
{label: _('Boats'), value: 'boats'},
{label: _('Planes'), value: 'planes'}
]
let content = "";
if (loading) content = (<span><i className="fa fa-circle-notch fa-spin"></i> {_("Loading…")}</span>);
else if (permanentError) content = (<div className="alert alert-warning">{permanentError}</div>);
else{
const featCount = objLayer ? objLayer.getLayers().length : 0;
content = (<div>
<ErrorMessage bind={[this, "error"]} />
<div className="row model-selector">
<select className="form-control" value={model} onChange={this.handleSelectModel}>
{models.map(m => <option value={m.value}>{m.label}</option>)}
</select>
<button onClick={this.handleDetect}
disabled={detecting} type="button" className="btn btn-sm btn-primary btn-detect">
{detecting ? <i className="fa fa-spin fa-circle-notch"/> : <i className="fa fa-search fa-fw"/>} {_("Detect")} {detecting && progress !== null ? ` (${progress.toFixed(0)}%)` : ""}
</button>
</div>
{objLayer ? <div className="detect-action-buttons">
<span><strong>{_("Count:")}</strong> {featCount}</span>
<div>
{featCount > 0 ? <button onClick={this.handleDownload}
type="button" className="btn btn-sm btn-primary btn-download">
<i className="fa fa-download fa-fw"/> {_("Download")}
</button> : ""}
<button onClick={this.handleRemoveObjLayer}
type="button" className="btn btn-sm btn-default">
<i className="fa fa-trash fa-fw"/>
</button>
</div>
</div> : ""}
</div>);
}
return (<div className="objdetect-panel">
<span className="close-button" onClick={this.props.onClose}/>
<div className="title">{_("Object Detection")}</div>
<hr/>
{content}
</div>);
}
}

Wyświetl plik

@ -0,0 +1,90 @@
.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;
&.no-pad{
padding-top: 0;
}
}
select, input{
height: auto;
padding: 4px;
}
*{
font-size: 12px;
}
.row.form-group.form-inline{
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%;
text-align: left;
display: block;
padding-top: 0;
padding-bottom: 0;
}
}
.action-buttons{
margin-top: 12px;
}
}

Wyświetl plik

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="52"
height="52"
viewBox="0 0 13.758333 13.758333"
version="1.1"
id="svg8976"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs8970" />
<metadata
id="metadata8973">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(0,-283.24167)">
<g
id="g9543"
transform="translate(0.76834737,-0.40970963)"
style="stroke:#000000;stroke-opacity:1;stroke-width:0.52916667;stroke-miterlimit:4;stroke-dasharray:none;stroke-linejoin:round;stroke-linecap:round">
<g
id="g9551"
transform="matrix(0.83317796,0,0,0.83296021,1.0098488,48.512648)"
style="stroke-width:0.63520145">
<g
id="g9575"
transform="matrix(1.1918583,0,0,1.1960416,-1.0686423,-56.918136)"
style="stroke-width:0.53201765">
<g
id="g831"
transform="translate(0.45113764,-0.11484945)">
<path
id="rect1"
style="fill:none;stroke:#000000;stroke-width:0.798026;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:3.61774;stroke-opacity:1;paint-order:markers fill stroke"
d="m 10.999825,293.33128 v 2.70001 H 8.2910396 m -5.4175705,0 H 0.16468392 v -2.70001 m 0,-5.40004 v -2.70001 H 2.8734691 m 5.4175705,0 h 2.7087854 v 2.70001" />
<path
fill-rule="nonzero"
d="m 2.3768541,288.69119 c -2.424e-4,-0.0594 0.036216,-0.11541 0.095329,-0.13736 l 3.0647714,-1.14407 a 0.145889,0.14543457 0 0 1 0.1031982,3.8e-4 l 3.0538864,1.16746 c 0.063373,0.0303 0.094163,0.085 0.094163,0.14812 l -0.00331,3.92377 c 0,0.0638 -0.041033,0.11797 -0.098218,0.13784 l -3.0262749,1.06095 c -0.059785,0.0211 -0.1004864,0.0187 -0.1514519,-8.1e-4 l -3.0367925,-1.13465 c -0.058911,-0.0219 -0.095329,-0.0776 -0.095329,-0.13679 H 2.376311 v -3.87215 z m 6.1183923,0.23619 -2.7602334,1.06342 v 3.52169 l 2.7572169,-0.96652 z m -2.9083779,-1.22456 -2.6684903,0.99613 2.680465,1.03123 2.6348849,-1.01512 z m -0.1457624,5.80716 v -3.52748 l -2.7708647,-1.06599 v 3.55817 z"
id="path1"
style="stroke-width:0.39901321;stroke-dasharray:none;fill:#000000;fill-opacity:1" />
</g>
</g>
</g>
</g>
</g>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.7 KiB

Wyświetl plik

@ -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}));
}
});

Wyświetl plik

@ -20,7 +20,8 @@ djangorestframework-guardian==0.3.0
drf-nested-routers==0.11.1 drf-nested-routers==0.11.1
funcsigs==1.0.2 funcsigs==1.0.2
futures==3.1.1 futures==3.1.1
gunicorn==19.7.1 gunicorn==19.8.0
geodeep==0.9.8
itypes==1.1.0 itypes==1.1.0
kombu==4.6.7 kombu==4.6.7
Markdown==3.3.4 Markdown==3.3.4

Wyświetl plik

@ -384,6 +384,9 @@ UI_MAX_PROCESSING_NODES = None
# are removed (or None to disable) # are removed (or None to disable)
CLEANUP_PARTIAL_TASKS = 72 CLEANUP_PARTIAL_TASKS = 72
# Maximum number of threads that a worker should use for processing
WORKERS_MAX_THREADS = 1
# Link to GCP docs # Link to GCP docs
GCP_DOCS_LINK = "https://docs.opendronemap.org/gcp/#gcp-file-format" GCP_DOCS_LINK = "https://docs.opendronemap.org/gcp/#gcp-file-format"

Wyświetl plik

@ -66,6 +66,8 @@ app.conf.beat_schedule = {
class MockAsyncResult: class MockAsyncResult:
def __init__(self, celery_task_id, result = None): def __init__(self, celery_task_id, result = None):
self.celery_task_id = celery_task_id self.celery_task_id = celery_task_id
self.state = "PENDING"
if result is None: if result is None:
if celery_task_id == 'bogus': if celery_task_id == 'bogus':
self.result = None self.result = None