Object detection plugin mock

pull/1589/head
Piero Toffanin 2025-01-23 15:09:58 -05:00
rodzic 95ce02fc2b
commit 7f44e62ac4
12 zmienionych plików z 588 dodań i 1 usunięć

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,122 @@
import os
from rest_framework import status
from rest_framework.response import Response
from app.plugins.views import TaskView, CheckTask, GetTaskResult
from app.plugins.worker import run_function_async
from django.utils.translation import gettext_lazy as _
class ContoursException(Exception):
pass
def calc_contours(dem, epsg, interval, output_format, simplify, zfactor = 1):
import os
import subprocess
import tempfile
import shutil
import glob
from webodm import settings
ext = ""
if output_format == "GeoJSON":
ext = "json"
elif output_format == "GPKG":
ext = "gpkg"
elif output_format == "DXF":
ext = "dxf"
elif output_format == "ESRI Shapefile":
ext = "shp"
MIN_CONTOUR_LENGTH = 10
tmpdir = os.path.join(settings.MEDIA_TMP, os.path.basename(tempfile.mkdtemp('_contours', dir=settings.MEDIA_TMP)))
gdal_contour_bin = shutil.which("gdal_contour")
ogr2ogr_bin = shutil.which("ogr2ogr")
if gdal_contour_bin is None:
return {'error': 'Cannot find gdal_contour'}
if ogr2ogr_bin is None:
return {'error': 'Cannot find ogr2ogr'}
contours_file = f"contours.gpkg"
p = subprocess.Popen([gdal_contour_bin, "-q", "-a", "level", "-3d", "-f", "GPKG", "-i", str(interval), dem, contours_file], cwd=tmpdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
out = out.decode('utf-8').strip()
err = err.decode('utf-8').strip()
success = p.returncode == 0
if not success:
return {'error', f'Error calling gdal_contour: {str(err)}'}
outfile = os.path.join(tmpdir, f"output.{ext}")
p = subprocess.Popen([ogr2ogr_bin, outfile, contours_file, "-simplify", str(simplify), "-f", output_format, "-t_srs", f"EPSG:{epsg}", "-nln", "contours",
"-dialect", "sqlite", "-sql", f"SELECT ID, ROUND(level * {zfactor}, 5) AS level, GeomFromGML(AsGML(ATM_Transform(GEOM, ATM_Scale(ATM_Create(), 1, 1, {zfactor})), 10)) as GEOM FROM contour WHERE ST_Length(GEOM) >= {MIN_CONTOUR_LENGTH}"], cwd=tmpdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
out = out.decode('utf-8').strip()
err = err.decode('utf-8').strip()
success = p.returncode == 0
if not success:
return {'error', f'Error calling ogr2ogr: {str(err)}'}
if not os.path.isfile(outfile):
return {'error': f'Cannot find output file: {outfile}'}
if output_format == "ESRI Shapefile":
ext="zip"
shp_dir = os.path.join(tmpdir, "contours")
os.makedirs(shp_dir)
contour_files = glob.glob(os.path.join(tmpdir, "output.*"))
for cf in contour_files:
shutil.move(cf, shp_dir)
shutil.make_archive(os.path.join(tmpdir, 'output'), 'zip', shp_dir)
outfile = os.path.join(tmpdir, f"output.{ext}")
return {'file': outfile}
class TaskContoursGenerate(TaskView):
def post(self, request, pk=None):
task = self.get_and_check_task(request, pk)
layer = request.data.get('layer', None)
if layer == 'DSM' and task.dsm_extent is None:
return Response({'error': _('No DSM layer is available.')})
elif layer == 'DTM' and task.dtm_extent is None:
return Response({'error': _('No DTM layer is available.')})
try:
if layer == 'DSM':
dem = os.path.abspath(task.get_asset_download_path("dsm.tif"))
elif layer == 'DTM':
dem = os.path.abspath(task.get_asset_download_path("dtm.tif"))
else:
raise ContoursException('{} is not a valid layer.'.format(layer))
epsg = int(request.data.get('epsg', '3857'))
interval = float(request.data.get('interval', 1))
format = request.data.get('format', 'GPKG')
supported_formats = ['GPKG', 'ESRI Shapefile', 'DXF', 'GeoJSON']
if not format in supported_formats:
raise ContoursException("Invalid format {} (must be one of: {})".format(format, ",".join(supported_formats)))
simplify = float(request.data.get('simplify', 0.01))
zfactor = float(request.data.get('zfactor', 1))
celery_task_id = run_function_async(calc_contours, dem, epsg, interval, format, simplify, zfactor).task_id
return Response({'celery_task_id': celery_task_id}, status=status.HTTP_200_OK)
except ContoursException as e:
return Response({'error': str(e)}, status=status.HTTP_200_OK)
class TaskContoursCheck(CheckTask):
def on_error(self, result):
pass
def error_check(self, result):
contours_file = result.get('file')
if not contours_file or not os.path.exists(contours_file):
return _('Could not generate contour file. This might be a bug.')
class TaskContoursDownload(GetTaskResult):
pass

Wyświetl plik

@ -0,0 +1,13 @@
{
"name": "Object Detect",
"webodmMinVersion": "2.5.8",
"description": "Detect objects using AI in orthophotos",
"version": "1.0.0",
"author": "Piero Toffanin",
"email": "pt@uav4geo.com",
"repository": "https://github.com/OpenDroneMap/WebODM",
"tags": ["object", "detect", "ai"],
"homepage": "https://github.com/OpenDroneMap/WebODM",
"experimental": false,
"deprecated": false
}

Wyświetl plik

@ -0,0 +1,20 @@
from app.plugins import PluginBase
from app.plugins import MountPoint
# from .api import TaskContoursGenerate
# from .api import TaskContoursCheck
# from .api import TaskContoursDownload
class Plugin(PluginBase):
def include_js_files(self):
return ['main.js']
def build_jsx_components(self):
return ['ObjDetect.jsx']
def api_mount_points(self):
return [
# MountPoint('task/(?P<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()),
]

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,205 @@
import React from 'react';
import PropTypes from 'prop-types';
import Storage from 'webodm/classes/Storage';
import L from 'leaflet';
import './ObjDetectPanel.scss';
import ErrorMessage from 'webodm/components/ErrorMessage';
import Workers from 'webodm/classes/Workers';
import { _ } from 'webodm/classes/gettext';
export default class ObjDetectPanel extends React.Component {
static defaultProps = {
};
static propTypes = {
onClose: PropTypes.func.isRequired,
tasks: PropTypes.object.isRequired,
isShowed: PropTypes.bool.isRequired,
map: PropTypes.object.isRequired
}
constructor(props){
super(props);
this.state = {
error: "",
permanentError: "",
model: Storage.getItem("last_objdetect_model") || "cars",
loading: true,
task: props.tasks[0] || null,
detecting: false,
objLayer: null,
};
}
componentDidMount(){
}
componentDidUpdate(){
if (this.props.isShowed && this.state.loading){
const {id, project} = this.state.task;
this.loadingReq = $.getJSON(`/api/projects/${project}/tasks/${id}/`)
.done(res => {
const { available_assets } = res;
if (available_assets.indexOf("orthophoto.tif") === -1){
this.setState({permanentError: _("No orthophoto is available. To use object detection you need an orthophoto.")});
}
})
.fail(() => {
this.setState({permanentError: _("Cannot retrieve information for task. Are you are connected to the internet?")})
})
.always(() => {
this.setState({loading: false});
this.loadingReq = null;
});
}
}
componentWillUnmount(){
if (this.loadingReq){
this.loadingReq.abort();
this.loadingReq = null;
}
if (this.detectReq){
this.detectReq.abort();
this.detectReq = null;
}
}
handleSelectModel = e => {
this.setState({model: e.target.value});
}
getFormValues = () => {
const { model } = this.state;
return {
model
};
}
addGeoJSONFromURL = (url, cb) => {
const { map } = this.props;
$.getJSON(url)
.done((geojson) => {
try{
this.handleRemoveObjLayer();
this.setState({objLayer: L.geoJSON(geojson, {
onEachFeature: (feature, layer) => {
if (feature.properties && feature.properties.level !== undefined) {
layer.bindPopup(`<div style="margin-right: 32px;">
<b>${_("Class:")}</b> ${feature.properties.class}<br/>
<b>${_("Score:")}</b> ${feature.properties.score}<br/>
</div>
`);
}
},
style: feature => {
// TODO: different colors for different elevations?
return {color: "yellow"};
}
})});
this.state.objLayer.addTo(map);
cb();
}catch(e){
cb(e.message);
}
})
.fail(cb);
}
handleRemoveObjLayer = () => {
const { map } = this.props;
if (this.state.objLayer){
map.removeLayer(this.state.objLayer);
this.setState({objLayer: null});
}
}
saveInputValues = () => {
// Save settings
Storage.setItem("last_objdetect_model", this.state.model);
}
handleDetect = () => {
this.setState({detecting: true, error: ""});
const taskId = this.state.task.id;
this.saveInputValues();
this.detectReq = $.ajax({
type: 'POST',
url: `/api/plugins/objdetect/task/${taskId}/detect`,
data: this.getFormValues()
}).done(result => {
if (result.celery_task_id){
Workers.waitForCompletion(result.celery_task_id, error => {
if (error) this.setState({detecting: false, error});
else{
const fileUrl = `/api/plugins/objdetect/task/${taskId}/download/${result.celery_task_id}`;
this.addGeoJSONFromURL(fileUrl, e => {
if (e) this.setState({error: JSON.stringify(e)});
this.setState({detecting: false});
});
}
}, `/api/plugins/objdetect/task/${taskId}/check/`);
}else if (result.error){
this.setState({detecting: false, error: result.error});
}else{
this.setState({detecting: false, error: "Invalid response: " + result});
}
}).fail(error => {
this.setState({detecting: false, error: JSON.stringify(error)});
});
}
render(){
const { loading, permanentError, objLayer, detecting, model } = this.state;
const models = [
{label: _('Cars'), value: 'cars'},
{label: _('Trees'), value: 'trees'},
]
let content = "";
if (loading) content = (<span><i className="fa fa-circle-notch fa-spin"></i> {_("Loading…")}</span>);
else if (permanentError) content = (<div className="alert alert-warning">{permanentError}</div>);
else{
content = (<div>
<ErrorMessage bind={[this, "error"]} />
<div className="row form-group form-inline">
<label className="col-sm-2 control-label">{_("Model:")}</label>
<div className="col-sm-10 ">
<select className="form-control" value={model} onChange={this.handleSelectModel}>
{models.map(m => <option value={m.value}>{m.label}</option>)}
</select>
</div>
</div>
<div className="row action-buttons">
<div className="col-sm-3">
{objLayer ? <a title="Delete Layer" href="javascript:void(0);" onClick={this.handleRemoveObjLayer}>
<i className="fa fa-trash"></i>
</a> : ""}
</div>
<div className="col-sm-9 text-right">
<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")}
</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,72 @@
.leaflet-control-objdetect .objdetect-panel{
padding: 6px 10px 6px 6px;
background: #fff;
min-width: 250px;
max-width: 300px;
.close-button{
display: inline-block;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAkUlEQVR4AZWRxQGDUBAFJ9pMflNIP/iVSkIb2wgccXd7g7O+3JXCQUgqBAfFSl8CMooJGQHfuUlEwZpoahZQ7ODTSXWJQkxyioock7BL2tXmdF4moJNX6IDZfbUBQNrX7qfeXfPuqwBAQjEz60w64htGJ+luFH48gt+NYe6v5b/cnr9asM+HlRQ2Qlwh2CjuqQQ9vKsKTwhQ1wAAAABJRU5ErkJggg==);
height: 18px;
width: 18px;
margin-right: 0;
float: right;
vertical-align: middle;
text-align: right;
margin-top: 0px;
margin-left: 16px;
position: relative;
left: 2px;
&:hover{
opacity: 0.7;
cursor: pointer;
}
}
.title{
font-size: 120%;
margin-right: 60px;
}
hr{
clear: both;
margin: 6px 0px;
border-color: #ddd;
}
label{
padding-top: 5px;
}
select, input{
height: auto;
padding: 4px;
}
*{
font-size: 12px;
}
.row.form-group.form-inline{
margin-bottom: 8px;
}
.dropdown-menu{
a{
width: 100%;
text-align: left;
display: block;
padding-top: 0;
padding-bottom: 0;
}
}
.btn-detect{
margin-right: 8px;
}
.action-buttons{
margin-top: 12px;
}
}

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",
"version": "2.5.7",
"version": "2.5.8",
"description": "User-friendly, extendable application and API for processing aerial imagery.",
"main": "index.js",
"scripts": {

Wyświetl plik

@ -21,6 +21,7 @@ drf-nested-routers==0.11.1
funcsigs==1.0.2
futures==3.1.1
gunicorn==19.8.0
geodeep==0.9.4
itypes==1.1.0
kombu==4.6.7
Markdown==3.3.4