Merge pull request #1589 from pierotofy/objdec

AI Object Detection
pull/1593/head
Piero Toffanin 2025-01-24 16:42:37 -05:00 zatwierdzone przez GitHub
commit ea4cb436e6
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
23 zmienionych plików z 579 dodań i 25 usunięć

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

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

@ -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,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")

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

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

@ -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": {

Wyświetl plik

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

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