From afa064ac204544cfec454f3d4be343b5e44468f7 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 24 Sep 2021 16:54:21 -0400 Subject: [PATCH] Display ground control points on map --- app/api/imageuploads.py | 60 ++++++++++++++++++++- app/api/tasks.py | 1 - app/models/task.py | 7 ++- app/static/app/js/classes/AssetDownloads.js | 2 + app/static/app/js/components/Map.jsx | 52 ++++++++++++++++++ app/static/app/js/css/ImagePopup.scss | 1 + package.json | 2 +- 7 files changed, 120 insertions(+), 5 deletions(-) diff --git a/app/api/imageuploads.py b/app/api/imageuploads.py index 57469bd1..a9b86315 100644 --- a/app/api/imageuploads.py +++ b/app/api/imageuploads.py @@ -1,12 +1,12 @@ import os - import io +import math from .tasks import TaskNestedView from rest_framework import exceptions from app.models import ImageUpload from app.models.task import assets_directory_path -from PIL import Image +from PIL import Image, ImageDraw from django.http import HttpResponse from .tasks import download_file_response import numpy as np @@ -26,6 +26,19 @@ def normalize(img): return Image.fromarray(arr) +def hex2rgb(hex_color): + """ + Adapted from https://stackoverflow.com/questions/29643352/converting-hex-to-rgb-value-in-python/29643643 + """ + hex_color = hex_color.lstrip('#') + print(hex_color) + if len(hex_color) != 6: + return tuple((255, 255, 255)) + try: + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + except ValueError: + return tuple((255, 255, 255)) + class Thumbnail(TaskNestedView): def get(self, request, pk=None, project_pk=None, image_filename=""): """ @@ -49,6 +62,31 @@ class Thumbnail(TaskNestedView): quality = int(self.request.query_params.get('quality', 75)) if quality < 0 or quality > 100: raise ValueError() + + center_x = float(self.request.query_params.get('center_x', '0.5')) + center_y = float(self.request.query_params.get('center_y', '0.5')) + if center_x < -0.5 or center_x > 1.5 or center_y < -0.5 or center_y > 1.5: + raise ValueError() + + draw_points = self.request.query_params.getlist('draw_point') + point_colors = self.request.query_params.getlist('point_color') + point_radiuses = self.request.query_params.getlist('point_radius') + + points = [] + i = 0 + for p in draw_points: + coords = list(map(float, p.split(","))) + if len(coords) != 2: + raise ValueError() + + points.append({ + 'x': coords[0], + 'y': coords[1], + 'color': hex2rgb(point_colors[i]) if i < len(point_colors) else (255, 255, 255), + 'radius': float(point_radiuses[i]) if i < len(point_radiuses) else 1, + }) + + i += 1 except ValueError: raise exceptions.ValidationError("Invalid query parameters") @@ -57,6 +95,24 @@ class Thumbnail(TaskNestedView): if img.mode != 'RGB': img = normalize(img) img = img.convert('RGB') + w, h = img.size + + # Draw points + for p in points: + d = ImageDraw.Draw(img) + r = p['radius'] + d.ellipse([(p['x'] * w - r, p['y'] * h - r), + (p['x'] * w + r, p['y'] * h + r)], outline=p['color'], width=max(1.0, math.floor(r / 3.0))) + + # Move image center + if center_x != 0.5 or center_y != 0.5: + img = img.crop(( + w * (center_x - 0.5), + h * (center_y - 0.5), + w * (center_x + 0.5), + h * (center_y + 0.5) + )) + img.thumbnail((thumb_size, thumb_size)) output = io.BytesIO() img.save(output, format='JPEG', quality=quality) diff --git a/app/api/tasks.py b/app/api/tasks.py index c70a97ca..3df35ec5 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -210,7 +210,6 @@ class TaskViewSet(viewsets.ViewSet): task.images_count = models.ImageUpload.objects.filter(task=task).count() # Update other parameters such as processing node, task name, etc. - print(f"Amount of images currently in task: {task.images_count}") serializer = TaskSerializer(task, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() diff --git a/app/models/task.py b/app/models/task.py index 3dbfd433..c0402365 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -196,6 +196,7 @@ class Task(models.Model): 'cameras.json': 'cameras.json', 'shots.geojson': os.path.join('odm_report', 'shots.geojson'), 'report.pdf': os.path.join('odm_report', 'report.pdf'), + 'ground_control_points.geojson': os.path.join('odm_georeferencing', 'ground_control_points.geojson'), } STATUS_CODES = ( @@ -860,6 +861,9 @@ class Task(models.Model): camera_shots = '' if 'shots.geojson' in self.available_assets: camera_shots = '/api/projects/{}/tasks/{}/download/shots.geojson'.format(self.project.id, self.id) + ground_control_points = '' + if 'ground_control_points.geojson' in self.available_assets: ground_control_points = '/api/projects/{}/tasks/{}/download/ground_control_points.geojson'.format(self.project.id, self.id) + return { 'tiles': [{'url': self.get_tile_base_url(t), 'type': t} for t in types], 'meta': { @@ -867,7 +871,8 @@ class Task(models.Model): 'id': str(self.id), 'project': self.project.id, 'public': self.public, - 'camera_shots': camera_shots + 'camera_shots': camera_shots, + 'ground_control_points': ground_control_points } } } diff --git a/app/static/app/js/classes/AssetDownloads.js b/app/static/app/js/classes/AssetDownloads.js index a8bd87aa..0052eb63 100644 --- a/app/static/app/js/classes/AssetDownloads.js +++ b/app/static/app/js/classes/AssetDownloads.js @@ -49,9 +49,11 @@ const api = { new AssetDownload(_("Textured Model"),"textured_model.zip","fab fa-connectdevelop"), new AssetDownload(_("Camera Parameters"),"cameras.json","fa fa-camera"), new AssetDownload(_("Camera Shots (GeoJSON)"),"shots.geojson","fa fa-camera"), + new AssetDownload(_("Ground Control Points (GeoJSON)"),"ground_control_points.geojson","far fa-dot-circle"), new AssetDownload(_("Quality Report"),"report.pdf","far fa-file-pdf"), + new AssetDownloadSeparator(), new AssetDownload(_("All Assets"),"all.zip","far fa-file-archive") ]; diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index 3fd78877..22edee56 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -13,6 +13,7 @@ import Dropzone from '../vendor/dropzone'; import $ from 'jquery'; import ErrorMessage from './ErrorMessage'; import ImagePopup from './ImagePopup'; +import GCPPopup from './GCPPopup'; import SwitchModeButton from './SwitchModeButton'; import ShareButton from './ShareButton'; import AssetDownloads from '../classes/AssetDownloads'; @@ -266,6 +267,57 @@ class Map extends React.Component { this.addedCameraShots = true; } + // Add ground control points layer if available + if (meta.task && meta.task.ground_control_points && !this.addedGroundControlPoints){ + const gcpMarker = L.AwesomeMarkers.icon({ + icon: 'dot-circle', + markerColor: 'blue', + prefix: 'fa' + }); + + const gcpLayer = new L.GeoJSON.AJAX(meta.task.ground_control_points, { + style: function (feature) { + return { + opacity: 1, + fillOpacity: 0.7, + color: "#000000" + } + }, + pointToLayer: function (feature, latlng) { + return new L.marker(latlng, { + icon: gcpMarker + }); + }, + onEachFeature: function (feature, layer) { + if (feature.properties && feature.properties.observations) { + // TODO! + let root = null; + const lazyrender = () => { + if (!root) root = document.createElement("div"); + ReactDOM.render(, root); + return root; + } + + layer.bindPopup(L.popup( + { + lazyrender, + maxHeight: 450, + minWidth: 320 + })); + } + } + }); + gcpLayer[Symbol.for("meta")] = {name: name + " " + _("(GCPs)"), icon: "far fa-dot-circle fa-fw"}; + + this.setState(update(this.state, { + overlays: {$push: [gcpLayer]} + })); + + gcpLayer.addTo(this.map); // TODO REMOVE + + this.addedGroundControlPoints = true; + } + done(); }) .fail((_, __, err) => done(err)) diff --git a/app/static/app/js/css/ImagePopup.scss b/app/static/app/js/css/ImagePopup.scss index d5d3fdd0..1c0b182a 100644 --- a/app/static/app/js/css/ImagePopup.scss +++ b/app/static/app/js/css/ImagePopup.scss @@ -19,6 +19,7 @@ &.fullscreen{ display: flex; align-items: center; + justify-content: center; color: white; i{ font-size: 200%; diff --git a/package.json b/package.json index 950b0298..76d2b214 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "1.9.6", + "version": "1.9.7", "description": "User-friendly, extendable application and API for processing aerial imagery.", "main": "index.js", "scripts": {