kopia lustrzana https://github.com/OpenDroneMap/WebODM
commit
ea4cb436e6
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()),
|
||||||
]
|
]
|
|
@ -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{
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .plugin import *
|
|
@ -0,0 +1,46 @@
|
||||||
|
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, 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', 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')
|
||||||
|
|
||||||
|
if not model in ['cars', 'trees']:
|
||||||
|
return Response({'error': 'Invalid model'}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
celery_task_id = run_function_async(detect, orthophoto, model, 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")
|
|
@ -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
|
||||||
|
}
|
|
@ -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()),
|
||||||
|
]
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
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.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'},
|
||||||
|
]
|
||||||
|
|
||||||
|
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>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 |
|
@ -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}));
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "WebODM",
|
"name": "WebODM",
|
||||||
"version": "2.5.7",
|
"version": "2.6.0",
|
||||||
"description": "User-friendly, extendable application and API for processing aerial imagery.",
|
"description": "User-friendly, extendable application and API for processing aerial imagery.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -21,6 +21,7 @@ drf-nested-routers==0.11.1
|
||||||
funcsigs==1.0.2
|
funcsigs==1.0.2
|
||||||
futures==3.1.1
|
futures==3.1.1
|
||||||
gunicorn==19.8.0
|
gunicorn==19.8.0
|
||||||
|
geodeep==0.9.4
|
||||||
itypes==1.1.0
|
itypes==1.1.0
|
||||||
kombu==4.6.7
|
kombu==4.6.7
|
||||||
Markdown==3.3.4
|
Markdown==3.3.4
|
||||||
|
|
|
@ -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
|
||||||
|
|
Ładowanie…
Reference in New Issue