Skip to content

Commit

Permalink
Merge pull request #1589 from pierotofy/objdec
Browse files Browse the repository at this point in the history
AI Object Detection
  • Loading branch information
pierotofy authored Jan 24, 2025
2 parents 95ce02f + 320bbaf commit ea4cb43
Show file tree
Hide file tree
Showing 23 changed files with 579 additions and 25 deletions.
12 changes: 11 additions & 1 deletion app/api/workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ def get(self, request, celery_task_id=None, **kwargs):
res = TestSafeAsyncResult(celery_task_id)

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:
result = res.get()

Expand All @@ -29,6 +36,9 @@ def get(self, request, celery_task_id=None, **kwargs):
msg = self.on_error(result)
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})

def on_error(self, result):
Expand Down
1 change: 1 addition & 0 deletions app/plugins/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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
Expand Down
11 changes: 9 additions & 2 deletions app/plugins/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ def run_function_async(func, *args, **kwargs):
return eval_async.delay(source, func.__name__, *args, **kwargs)


@app.task
def eval_async(source, funcname, *args, **kwargs):
@app.task(bind=True)
def eval_async(self, source, funcname, *args, **kwargs):
"""
Run Python code asynchronously using Celery.
It's recommended to use run_function_async instead.
"""
ns = {}
code = compile(source, 'file', 'exec')
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)
6 changes: 5 additions & 1 deletion app/static/app/js/classes/Workers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import $ from 'jquery';

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 url = checkUrl + celery_task_id;

Expand All @@ -15,6 +16,9 @@ export default {
}else if (result.ready){
cb();
}else{
if (typeof progress_cb === "function" && result.progress !== undefined && result.status !== undefined){
progress_cb(result.status, result.progress);
}
// Retry
setTimeout(() => check(), 2000);
}
Expand Down
8 changes: 0 additions & 8 deletions coreplugins/contours/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,6 @@ def post(self, request, pk=None):
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
2 changes: 0 additions & 2 deletions coreplugins/contours/plugin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from app.plugins import PluginBase
from app.plugins import MountPoint
from .api import TaskContoursGenerate
from .api import TaskContoursCheck
from .api import TaskContoursDownload


Expand All @@ -15,6 +14,5 @@ def build_jsx_components(self):
def api_mount_points(self):
return [
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()),
]
2 changes: 1 addition & 1 deletion coreplugins/contours/public/ContoursPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export default class ContoursPanel extends React.Component {
this.setState({[loadingProp]: false});
}
}
}, `/api/plugins/contours/task/${taskId}/contours/check/`);
});
}else if (result.error){
this.setState({[loadingProp]: false, error: result.error});
}else{
Expand Down
5 changes: 1 addition & 4 deletions coreplugins/measure/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from rest_framework import serializers
from rest_framework import status
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.plugins.views import TaskView
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -34,9 +34,6 @@ def post(self, request, pk=None):
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_200_OK)

class TaskVolumeCheck(CheckTask):
pass

class TaskVolumeResult(GetTaskResult):
def get(self, request, pk=None, celery_task_id=None):
task = Task.objects.only('dsm_extent').get(pk=pk)
Expand Down
3 changes: 1 addition & 2 deletions coreplugins/measure/plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from app.plugins import MountPoint
from app.plugins import PluginBase
from .api import TaskVolume, TaskVolumeCheck, TaskVolumeResult
from .api import TaskVolume, TaskVolumeResult

class Plugin(PluginBase):
def include_js_files(self):
Expand All @@ -12,6 +12,5 @@ def build_jsx_components(self):
def api_mount_points(self):
return [
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()),
]
6 changes: 3 additions & 3 deletions coreplugins/measure/public/MeasurePopup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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"
Expand All @@ -139,7 +139,7 @@ export default class MeasurePopup extends React.Component {
else this.setState({volume: parseFloat(volume)});
}, `/api/plugins/measure/task/${task.id}/volume/get/`);
}
}, `/api/plugins/measure/task/${task.id}/volume/check/`);
});
}else if (result.error){
this.setState({error: result.error});
}else{
Expand Down
1 change: 1 addition & 0 deletions coreplugins/objdetect/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .plugin import *
46 changes: 46 additions & 0 deletions coreplugins/objdetect/api.py
Original file line number Diff line number Diff line change
@@ -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")
13 changes: 13 additions & 0 deletions coreplugins/objdetect/manifest.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"repository": "https://github.com/OpenDroneMap/WebODM",
"tags": ["object", "detect", "ai"],
"homepage": "https://github.com/OpenDroneMap/WebODM",
"experimental": false,
"deprecated": false
}
18 changes: 18 additions & 0 deletions coreplugins/objdetect/plugin.py
Original file line number Diff line number Diff line change
@@ -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()),
]
55 changes: 55 additions & 0 deletions coreplugins/objdetect/public/ObjDetect.jsx
Original file line number Diff line number Diff line change
@@ -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;
}
});

24 changes: 24 additions & 0 deletions coreplugins/objdetect/public/ObjDetect.scss
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit ea4cb43

Please sign in to comment.