kopia lustrzana https://github.com/OpenDroneMap/WebODM
PoC thumbnails
rodzic
5c26ff0742
commit
8570bf8e42
|
@ -4,6 +4,10 @@ import shutil
|
|||
from wsgiref.util import FileWrapper
|
||||
|
||||
import mimetypes
|
||||
import rasterio
|
||||
from rasterio.enums import ColorInterp
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
from shutil import copyfileobj, move
|
||||
from django.core.exceptions import ObjectDoesNotExist, SuspiciousFileOperation, ValidationError
|
||||
|
@ -235,7 +239,7 @@ class TaskViewSet(viewsets.ViewSet):
|
|||
return Response({'success': True, 'task': TaskSerializer(new_task).data}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({'error': _("Cannot duplicate task")}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def create(self, request, project_pk=None):
|
||||
project = get_and_check_project(request, project_pk, ('change_project', ))
|
||||
|
||||
|
@ -403,6 +407,65 @@ class TaskDownloads(TaskNestedView):
|
|||
else:
|
||||
return download_file_response(request, asset_fs, 'attachment', download_filename=download_filename)
|
||||
|
||||
|
||||
class TaskThumbnail(TaskNestedView):
|
||||
def get(self, request, pk=None, project_pk=None):
|
||||
"""
|
||||
Generate a thumbnail on the fly for a particular task
|
||||
"""
|
||||
task = self.get_and_check_task(request, pk)
|
||||
orthophoto_path = task.get_check_file_asset_path("orthophoto.tif")
|
||||
if orthophoto_path is None:
|
||||
raise exceptions.NotFound()
|
||||
|
||||
try:
|
||||
thumb_size = int(self.request.query_params.get('size', 512))
|
||||
if thumb_size < 1 or thumb_size > 2048:
|
||||
raise ValueError()
|
||||
|
||||
quality = int(self.request.query_params.get('quality', 75))
|
||||
if quality < 0 or quality > 100:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
raise exceptions.ValidationError("Invalid query parameters")
|
||||
|
||||
with rasterio.open(orthophoto_path, "r") as raster:
|
||||
ci = raster.colorinterp
|
||||
indexes = (1, 2, 3,)
|
||||
|
||||
# More than 4 bands?
|
||||
if len(ci) > 4:
|
||||
# Try to find RGBA band order
|
||||
if ColorInterp.red in ci and \
|
||||
ColorInterp.green in ci and \
|
||||
ColorInterp.blue in ci:
|
||||
indexes = (ci.index(ColorInterp.red) + 1,
|
||||
ci.index(ColorInterp.green) + 1,
|
||||
ci.index(ColorInterp.blue) + 1,)
|
||||
elif len(ci) < 3:
|
||||
raise exceptions.NotFound()
|
||||
|
||||
if ColorInterp.alpha in ci:
|
||||
indexes += (ci.index(ColorInterp.alpha) + 1, )
|
||||
|
||||
img = raster.read(indexes=indexes, boundless=True, fill_value=255, out_shape=(
|
||||
len(indexes),
|
||||
thumb_size,
|
||||
thumb_size,
|
||||
), resampling=rasterio.enums.Resampling.nearest).transpose((1, 2, 0))
|
||||
|
||||
img = Image.fromarray(img)
|
||||
output = io.BytesIO()
|
||||
img.save(output, format='PNG', quality=quality)
|
||||
|
||||
res = HttpResponse(content_type="image/png")
|
||||
res['Content-Disposition'] = 'inline'
|
||||
res.write(output.getvalue())
|
||||
output.close()
|
||||
|
||||
return res
|
||||
|
||||
|
||||
"""
|
||||
Raw access to the task's asset folder resources
|
||||
Useful when accessing a textured 3d model, or the Potree point cloud data
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.conf.urls import url, include
|
|||
from app.api.presets import PresetViewSet
|
||||
from app.plugins.views import api_view_handler
|
||||
from .projects import ProjectViewSet
|
||||
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskBackup, TaskAssetsImport
|
||||
from .tasks import TaskViewSet, TaskDownloads, TaskThumbnail, TaskAssets, TaskBackup, TaskAssetsImport
|
||||
from .imageuploads import Thumbnail, ImageDownload
|
||||
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
|
||||
from .admin import AdminUserViewSet, AdminGroupViewSet, AdminProfileViewSet
|
||||
|
@ -46,6 +46,7 @@ urlpatterns = [
|
|||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/import$', TaskAssetsImport.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/thumbnail$', TaskThumbnail.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/backup$', TaskBackup.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/images/thumbnail/(?P<image_filename>.+)$', Thumbnail.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/images/download/(?P<image_filename>.+)$', ImageDownload.as_view()),
|
||||
|
|
|
@ -1260,6 +1260,11 @@ class Task(models.Model):
|
|||
shutil.copy(alignment_file, dst_file)
|
||||
else:
|
||||
logger.warn("Cannot set alignment file for {}, {} does not exist".format(self, alignment_file))
|
||||
|
||||
def get_check_file_asset_path(self, asset):
|
||||
file = self.assets_path(self.ASSETS_MAP[asset])
|
||||
if isinstance(file, str) and os.path.isfile(file):
|
||||
return file
|
||||
|
||||
def handle_images_upload(self, files):
|
||||
uploaded = {}
|
||||
|
|
|
@ -171,6 +171,10 @@ class TaskListItem extends React.Component {
|
|||
return `/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/output/?line=${line}`;
|
||||
}
|
||||
|
||||
thumbnailUrl = () => {
|
||||
return `/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/thumbnail?size=192`;
|
||||
}
|
||||
|
||||
hoursMinutesSecs(t){
|
||||
if (t === 0 || t === -1) return "-- : -- : --";
|
||||
|
||||
|
@ -556,54 +560,62 @@ class TaskListItem extends React.Component {
|
|||
<div className="expanded-panel">
|
||||
<div className="row">
|
||||
<div className="col-md-12 no-padding">
|
||||
<table className="table table-condensed info-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{_("Created on:")}</strong></td>
|
||||
<td>{(new Date(task.created_at)).toLocaleString()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{_("Processing Node:")}</strong></td>
|
||||
<td>{task.processing_node_name || "-"} ({task.auto_processing_node ? _("auto") : _("manual")})</td>
|
||||
</tr>
|
||||
{Array.isArray(task.options) &&
|
||||
<tr>
|
||||
<td><strong>{_("Options:")}</strong></td>
|
||||
<td>{this.optionsToList(task.options)}</td>
|
||||
</tr>}
|
||||
{stats && stats.gsd &&
|
||||
<tr>
|
||||
<td><strong>{_("Average GSD:")}</strong></td>
|
||||
<td>{parseFloat(stats.gsd.toFixed(2)).toLocaleString()} cm</td>
|
||||
</tr>}
|
||||
{stats && stats.area &&
|
||||
<tr>
|
||||
<td><strong>{_("Area:")}</strong></td>
|
||||
<td>{parseFloat(stats.area.toFixed(2)).toLocaleString()} m²</td>
|
||||
</tr>}
|
||||
{stats && stats.pointcloud && stats.pointcloud.points &&
|
||||
<tr>
|
||||
<td><strong>{_("Reconstructed Points:")}</strong></td>
|
||||
<td>{stats.pointcloud.points.toLocaleString()}</td>
|
||||
</tr>}
|
||||
{task.size > 0 &&
|
||||
<tr>
|
||||
<td><strong>{_("Disk Usage:")}</strong></td>
|
||||
<td>{Utils.bytesToSize(task.size * 1024 * 1024)}</td>
|
||||
</tr>}
|
||||
<tr>
|
||||
<td><strong>{_("Task ID:")}</strong></td>
|
||||
<td>{task.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{_("Task Output:")}</strong></td>
|
||||
<td><div className="btn-group btn-toggle">
|
||||
<button onClick={this.setView("console")} className={"btn btn-xs " + (this.state.view === "basic" ? "btn-default" : "btn-primary")}>{_("On")}</button>
|
||||
<button onClick={this.setView("basic")} className={"btn btn-xs " + (this.state.view === "console" ? "btn-default" : "btn-primary")}>{_("Off")}</button>
|
||||
</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="col-md-9 col-sm-10 no-padding">
|
||||
<table className="table table-condensed info-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{_("Created on:")}</strong></td>
|
||||
<td>{(new Date(task.created_at)).toLocaleString()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{_("Processing Node:")}</strong></td>
|
||||
<td>{task.processing_node_name || "-"} ({task.auto_processing_node ? _("auto") : _("manual")})</td>
|
||||
</tr>
|
||||
{Array.isArray(task.options) &&
|
||||
<tr>
|
||||
<td><strong>{_("Options:")}</strong></td>
|
||||
<td>{this.optionsToList(task.options)}</td>
|
||||
</tr>}
|
||||
{stats && stats.gsd &&
|
||||
<tr>
|
||||
<td><strong>{_("Average GSD:")}</strong></td>
|
||||
<td>{parseFloat(stats.gsd.toFixed(2)).toLocaleString()} cm</td>
|
||||
</tr>}
|
||||
{stats && stats.area &&
|
||||
<tr>
|
||||
<td><strong>{_("Area:")}</strong></td>
|
||||
<td>{parseFloat(stats.area.toFixed(2)).toLocaleString()} m²</td>
|
||||
</tr>}
|
||||
{stats && stats.pointcloud && stats.pointcloud.points &&
|
||||
<tr>
|
||||
<td><strong>{_("Reconstructed Points:")}</strong></td>
|
||||
<td>{stats.pointcloud.points.toLocaleString()}</td>
|
||||
</tr>}
|
||||
{task.size > 0 &&
|
||||
<tr>
|
||||
<td><strong>{_("Disk Usage:")}</strong></td>
|
||||
<td>{Utils.bytesToSize(task.size * 1024 * 1024)}</td>
|
||||
</tr>}
|
||||
<tr>
|
||||
<td><strong>{_("Task ID:")}</strong></td>
|
||||
<td>{task.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{_("Task Output:")}</strong></td>
|
||||
<td><div className="btn-group btn-toggle">
|
||||
<button onClick={this.setView("console")} className={"btn btn-xs " + (this.state.view === "basic" ? "btn-default" : "btn-primary")}>{_("On")}</button>
|
||||
<button onClick={this.setView("basic")} className={"btn btn-xs " + (this.state.view === "console" ? "btn-default" : "btn-primary")}>{_("Off")}</button>
|
||||
</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{task.status === statusCodes.COMPLETED ?
|
||||
<div className="col-md-3 col-sm-2 text-center">
|
||||
<a href={`/map/project/${task.project}/task/${task.id}/`}>
|
||||
<img className="task-thumbnail" src={this.thumbnailUrl()} alt={_("Thumbnail")}/>
|
||||
</a>
|
||||
</div> : ""}
|
||||
|
||||
{this.state.view === 'console' ?
|
||||
<Console
|
||||
|
|
|
@ -156,4 +156,8 @@
|
|||
.name-link{
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.task-thumbnail{
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue