PoC object detection working

pull/1589/head
Piero Toffanin 2025-01-24 11:43:19 -05:00
rodzic 7f44e62ac4
commit 27074d7107
5 zmienionych plików z 73 dodań i 145 usunięć

Wyświetl plik

@ -3,6 +3,7 @@ import os
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

Wyświetl plik

@ -1,122 +1,48 @@
import os
import json
from rest_framework import status
from rest_framework.response import Response
from app.plugins.views import TaskView, CheckTask, GetTaskResult
from app.plugins.views import TaskView, CheckTask, GetTaskResult, TaskResultOutputError
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):
def detect(orthophoto, model):
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
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"}
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):
try:
return {'output': gdetect(orthophoto, model, output_type='geojson')}
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)
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.')})
if task.orthophoto_extent is None:
return Response({'error': _('No orthophoto 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))
orthophoto = os.path.abspath(task.get_asset_download_path("orthophoto.tif"))
model = request.data.get('model', 'cars')
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))
if not model in ['cars', 'trees']:
return Response({'error': 'Invalid model'}, status=status.HTTP_200_OK)
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)
celery_task_id = run_function_async(detect, orthophoto, model).task_id
return Response({'celery_task_id': celery_task_id}, 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 TaskObjCheck(CheckTask):
pass
class TaskObjDownload(GetTaskResult):
def handle_output(self, output, result, **kwargs):
try:
return json.loads(output)
except:
raise TaskResultOutputError("Invalid GeoJSON")

Wyświetl plik

@ -1,8 +1,8 @@
from app.plugins import PluginBase
from app.plugins import MountPoint
# from .api import TaskContoursGenerate
# from .api import TaskContoursCheck
# from .api import TaskContoursDownload
from .api import TaskObjDetect
from .api import TaskObjCheck
from .api import TaskObjDownload
class Plugin(PluginBase):
@ -14,7 +14,7 @@ class Plugin(PluginBase):
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()),
MountPoint('task/(?P<pk>[^/.]+)/detect', TaskObjDetect.as_view()),
MountPoint('task/[^/.]+/check/(?P<celery_task_id>.+)', TaskObjCheck.as_view()),
MountPoint('task/[^/.]+/download/(?P<celery_task_id>.+)', TaskObjDownload.as_view()),
]

Wyświetl plik

@ -78,37 +78,33 @@ export default class ObjDetectPanel extends React.Component {
};
}
addGeoJSONFromURL = (url, cb) => {
addGeoJSON = (geojson, cb) => {
const { map } = this.props;
$.getJSON(url)
.done((geojson) => {
try{
this.handleRemoveObjLayer();
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);
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: "red"};
}
})});
this.state.objLayer.addTo(map);
cb();
}catch(e){
cb(e.message);
}
})
.fail(cb);
cb();
}catch(e){
cb(e.message);
}
}
handleRemoveObjLayer = () => {
@ -139,11 +135,20 @@ export default class ObjDetectPanel extends React.Component {
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}`;
Workers.getOutput(result.celery_task_id, (error, geojson) => {
try{
geojson = JSON.parse(geojson);
}catch(e){
error = "Invalid GeoJSON";
}
this.addGeoJSONFromURL(fileUrl, e => {
if (e) this.setState({error: JSON.stringify(e)});
this.setState({detecting: false});
if (error) this.setState({detecting: false, error});
else{
this.addGeoJSON(geojson, e => {
if (e) this.setState({error: JSON.stringify(e)});
this.setState({detecting: false});
});
}
});
}
}, `/api/plugins/objdetect/task/${taskId}/check/`);

Wyświetl plik

@ -62,10 +62,6 @@
}
}
.btn-detect{
margin-right: 8px;
}
.action-buttons{
margin-top: 12px;
}