kopia lustrzana https://github.com/OpenDroneMap/WebODM
Added support for server resize, in-browser resize, or no resize
rodzic
3415220565
commit
2735942409
|
@ -145,7 +145,8 @@ class TaskViewSet(viewsets.ViewSet):
|
||||||
raise exceptions.ValidationError(detail="Cannot create task, you need at least 2 images")
|
raise exceptions.ValidationError(detail="Cannot create task, you need at least 2 images")
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
task = models.Task.objects.create(project=project)
|
task = models.Task.objects.create(project=project,
|
||||||
|
pending_action=pending_actions.RESIZE if 'resize_to' in request.data else None)
|
||||||
|
|
||||||
for image in files:
|
for image in files:
|
||||||
models.ImageUpload.objects.create(task=task, image=image)
|
models.ImageUpload.objects.create(task=task, image=image)
|
||||||
|
@ -155,7 +156,9 @@ class TaskViewSet(viewsets.ViewSet):
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
worker_tasks.process_task.delay(task.id)
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
def update(self, request, pk=None, project_pk=None, partial=False):
|
def update(self, request, pk=None, project_pk=None, partial=False):
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.7 on 2018-02-19 19:46
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('app', '0016_public_task_uuids'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='task',
|
||||||
|
name='resize_to',
|
||||||
|
field=models.IntegerField(default=-1, help_text='When set to a value different than -1, indicates that the images for this task have been / will be resized to the size specified here before processing.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='task',
|
||||||
|
name='pending_action',
|
||||||
|
field=models.IntegerField(blank=True, choices=[(1, 'CANCEL'), (2, 'REMOVE'), (3, 'RESTART'), (4, 'RESIZE')], db_index=True, help_text='A requested action to be performed on the task. The selected action will be performed by the worker at the next iteration.', null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -4,6 +4,9 @@ import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
import uuid as uuid_module
|
import uuid as uuid_module
|
||||||
|
|
||||||
|
import piexif
|
||||||
|
import re
|
||||||
|
from PIL import Image
|
||||||
from django.contrib.gis.gdal import GDALRaster
|
from django.contrib.gis.gdal import GDALRaster
|
||||||
from django.contrib.gis.gdal import OGRGeometry
|
from django.contrib.gis.gdal import OGRGeometry
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
|
@ -22,6 +25,10 @@ from nodeodm.models import ProcessingNode
|
||||||
from webodm import settings
|
from webodm import settings
|
||||||
from .project import Project
|
from .project import Project
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
from multiprocessing import cpu_count
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
logger = logging.getLogger('app.logger')
|
logger = logging.getLogger('app.logger')
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,6 +64,47 @@ def validate_task_options(value):
|
||||||
raise ValidationError("Invalid options")
|
raise ValidationError("Invalid options")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def resize_image(image_path, resize_to):
|
||||||
|
try:
|
||||||
|
im = Image.open(image_path)
|
||||||
|
path, ext = os.path.splitext(image_path)
|
||||||
|
resized_image_path = os.path.join(path + '.resized' + ext)
|
||||||
|
|
||||||
|
width, height = im.size
|
||||||
|
max_side = max(width, height)
|
||||||
|
if max_side < resize_to:
|
||||||
|
logger.warning('You asked to make {} bigger ({} --> {}), but we are not going to do that.'.format(image_path, max_side, resize_to))
|
||||||
|
im.close()
|
||||||
|
return {'path': image_path, 'resize_ratio': 1}
|
||||||
|
|
||||||
|
ratio = float(resize_to) / float(max_side)
|
||||||
|
resized_width = int(width * ratio)
|
||||||
|
resized_height = int(height * ratio)
|
||||||
|
|
||||||
|
im.thumbnail((resized_width, resized_height), Image.LANCZOS)
|
||||||
|
|
||||||
|
if 'exif' in im.info:
|
||||||
|
exif_dict = piexif.load(im.info['exif'])
|
||||||
|
exif_dict['Exif'][piexif.ExifIFD.PixelXDimension] = resized_width
|
||||||
|
exif_dict['Exif'][piexif.ExifIFD.PixelYDimension] = resized_height
|
||||||
|
im.save(resized_image_path, "JPEG", exif=piexif.dump(exif_dict), quality=100)
|
||||||
|
else:
|
||||||
|
im.save(resized_image_path, "JPEG", quality=100)
|
||||||
|
|
||||||
|
im.close()
|
||||||
|
|
||||||
|
# Delete original image, rename resized image to original
|
||||||
|
os.remove(image_path)
|
||||||
|
os.rename(resized_image_path, image_path)
|
||||||
|
|
||||||
|
logger.info("Resized {} to {}x{}".format(image_path, resized_width, resized_height))
|
||||||
|
except IOError as e:
|
||||||
|
logger.warning("Cannot resize {}: {}.".format(image_path, str(e)))
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {'path': image_path, 'resize_ratio': ratio}
|
||||||
|
|
||||||
class Task(models.Model):
|
class Task(models.Model):
|
||||||
ASSETS_MAP = {
|
ASSETS_MAP = {
|
||||||
'all.zip': 'all.zip',
|
'all.zip': 'all.zip',
|
||||||
|
@ -85,6 +133,7 @@ class Task(models.Model):
|
||||||
(pending_actions.CANCEL, 'CANCEL'),
|
(pending_actions.CANCEL, 'CANCEL'),
|
||||||
(pending_actions.REMOVE, 'REMOVE'),
|
(pending_actions.REMOVE, 'REMOVE'),
|
||||||
(pending_actions.RESTART, 'RESTART'),
|
(pending_actions.RESTART, 'RESTART'),
|
||||||
|
(pending_actions.RESIZE, 'RESIZE'),
|
||||||
)
|
)
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid_module.uuid4, unique=True, serialize=False, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid_module.uuid4, unique=True, serialize=False, editable=False)
|
||||||
|
@ -112,6 +161,7 @@ class Task(models.Model):
|
||||||
pending_action = models.IntegerField(choices=PENDING_ACTIONS, db_index=True, null=True, blank=True, help_text="A requested action to be performed on the task. The selected action will be performed by the worker at the next iteration.")
|
pending_action = models.IntegerField(choices=PENDING_ACTIONS, db_index=True, null=True, blank=True, help_text="A requested action to be performed on the task. The selected action will be performed by the worker at the next iteration.")
|
||||||
|
|
||||||
public = models.BooleanField(default=False, help_text="A flag indicating whether this task is available to the public")
|
public = models.BooleanField(default=False, help_text="A flag indicating whether this task is available to the public")
|
||||||
|
resize_to = models.IntegerField(default=-1, help_text="When set to a value different than -1, indicates that the images for this task have been / will be resized to the size specified here before processing.")
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -227,6 +277,11 @@ class Task(models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if self.pending_action == pending_actions.RESIZE:
|
||||||
|
self.resize_images()
|
||||||
|
self.pending_action = None
|
||||||
|
self.save()
|
||||||
|
|
||||||
if self.auto_processing_node and not self.status in [status_codes.FAILED, status_codes.CANCELED]:
|
if self.auto_processing_node and not self.status in [status_codes.FAILED, status_codes.CANCELED]:
|
||||||
# No processing node assigned and need to auto assign
|
# No processing node assigned and need to auto assign
|
||||||
if self.processing_node is None:
|
if self.processing_node is None:
|
||||||
|
@ -515,8 +570,31 @@ class Task(models.Model):
|
||||||
self.pending_action = None
|
self.pending_action = None
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
def resize_images(self):
|
||||||
|
"""
|
||||||
|
Destructively resize this assets JPG images while retaining EXIF tags.
|
||||||
|
Resulting images are always converted to JPG.
|
||||||
|
TODO: add support for tiff files
|
||||||
|
:return list containing paths of resized images and resize ratios
|
||||||
|
"""
|
||||||
|
if self.resize_to < 0:
|
||||||
|
logger.warning("We were asked to resize images to {}, this might be an error.".format(self.resize_to))
|
||||||
|
return []
|
||||||
|
|
||||||
|
directory = full_task_directory_path(self.id, self.project.id)
|
||||||
|
images_path = [os.path.join(directory, f) for f in os.listdir(directory) if
|
||||||
|
re.match(r'.*\.jpe?g$', f, re.IGNORECASE)]
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
|
||||||
|
resized_images = list(filter(lambda i: i is not None, executor.map(
|
||||||
|
partial(resize_image, resize_to=self.resize_to),
|
||||||
|
images_path)))
|
||||||
|
|
||||||
|
return resized_images
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
('view_task', 'Can view task'),
|
('view_task', 'Can view task'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
CANCEL = 1
|
CANCEL = 1
|
||||||
REMOVE = 2
|
REMOVE = 2
|
||||||
RESTART = 3
|
RESTART = 3
|
||||||
|
RESIZE = 4
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const CANCEL = 1,
|
const CANCEL = 1,
|
||||||
REMOVE = 2,
|
REMOVE = 2,
|
||||||
RESTART = 3;
|
RESTART = 3,
|
||||||
|
RESIZE = 4;
|
||||||
|
|
||||||
let pendingActions = {
|
let pendingActions = {
|
||||||
[CANCEL]: {
|
[CANCEL]: {
|
||||||
|
@ -11,6 +12,9 @@ let pendingActions = {
|
||||||
},
|
},
|
||||||
[RESTART]: {
|
[RESTART]: {
|
||||||
descr: "Restarting..."
|
descr: "Restarting..."
|
||||||
|
},
|
||||||
|
[RESIZE]: {
|
||||||
|
descr: "Resizing images..."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +22,7 @@ export default {
|
||||||
CANCEL: CANCEL,
|
CANCEL: CANCEL,
|
||||||
REMOVE: REMOVE,
|
REMOVE: REMOVE,
|
||||||
RESTART: RESTART,
|
RESTART: RESTART,
|
||||||
|
RESIZE: RESIZE,
|
||||||
|
|
||||||
description: function(pendingAction) {
|
description: function(pendingAction) {
|
||||||
if (pendingActions[pendingAction]) return pendingActions[pendingAction].descr;
|
if (pendingActions[pendingAction]) return pendingActions[pendingAction].descr;
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
const dict = [
|
||||||
|
{k: 'NO', v: 0, human: "No"}, // Don't resize
|
||||||
|
{k: 'YES', v: 1, human: "Yes"}, // Resize on server
|
||||||
|
{k: 'YESINBROWSER', v: 2, human: "Yes (In browser)"} // Resize on browser
|
||||||
|
];
|
||||||
|
|
||||||
|
const exp = {
|
||||||
|
all: () => dict.map(d => d.v),
|
||||||
|
fromString: (s) => {
|
||||||
|
let v = parseInt(s);
|
||||||
|
if (!isNaN(v) && v >= 0 && v <= 2) return v;
|
||||||
|
else return 0;
|
||||||
|
},
|
||||||
|
toHuman: (v) => {
|
||||||
|
for (let i in dict){
|
||||||
|
if (dict[i].v === v) return dict[i].human;
|
||||||
|
}
|
||||||
|
throw new Error("Invalid value: " + v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dict.forEach(en => {
|
||||||
|
exp[en.k] = en.v;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default exp;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import EditTaskForm from './EditTaskForm';
|
import EditTaskForm from './EditTaskForm';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Storage from '../classes/Storage';
|
import Storage from '../classes/Storage';
|
||||||
|
import ResizeModes from '../classes/ResizeModes';
|
||||||
|
|
||||||
class NewTaskPanel extends React.Component {
|
class NewTaskPanel extends React.Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -25,14 +26,14 @@ class NewTaskPanel extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
name: props.name,
|
name: props.name,
|
||||||
editTaskFormLoaded: false,
|
editTaskFormLoaded: false,
|
||||||
resize: Storage.getItem('do_resize') !== null ? Storage.getItem('do_resize') == "1" : true,
|
resizeMode: Storage.getItem('resize_mode') === null ? ResizeModes.YES : ResizeModes.fromString(Storage.getItem('resize_mode')),
|
||||||
resizeSize: parseInt(Storage.getItem('resize_size')) || 2048
|
resizeSize: parseInt(Storage.getItem('resize_size')) || 2048
|
||||||
};
|
};
|
||||||
|
|
||||||
this.save = this.save.bind(this);
|
this.save = this.save.bind(this);
|
||||||
this.handleFormTaskLoaded = this.handleFormTaskLoaded.bind(this);
|
this.handleFormTaskLoaded = this.handleFormTaskLoaded.bind(this);
|
||||||
this.getTaskInfo = this.getTaskInfo.bind(this);
|
this.getTaskInfo = this.getTaskInfo.bind(this);
|
||||||
this.setResize = this.setResize.bind(this);
|
this.setResizeMode = this.setResizeMode.bind(this);
|
||||||
this.handleResizeSizeChange = this.handleResizeSizeChange.bind(this);
|
this.handleResizeSizeChange = this.handleResizeSizeChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ class NewTaskPanel extends React.Component {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.taskForm.saveLastPresetToStorage();
|
this.taskForm.saveLastPresetToStorage();
|
||||||
Storage.setItem('resize_size', this.state.resizeSize);
|
Storage.setItem('resize_size', this.state.resizeSize);
|
||||||
Storage.setItem('do_resize', this.state.resize ? "1" : "0");
|
Storage.setItem('resize_mode', this.state.resizeMode);
|
||||||
if (this.props.onSave) this.props.onSave(this.getTaskInfo());
|
if (this.props.onSave) this.props.onSave(this.getTaskInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,13 +55,14 @@ class NewTaskPanel extends React.Component {
|
||||||
|
|
||||||
getTaskInfo(){
|
getTaskInfo(){
|
||||||
return Object.assign(this.taskForm.getTaskInfo(), {
|
return Object.assign(this.taskForm.getTaskInfo(), {
|
||||||
resizeTo: (this.state.resize && this.state.resizeSize > 0) ? this.state.resizeSize : null
|
resizeSize: this.state.resizeSize,
|
||||||
|
resizeMode: this.state.resizeMode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setResize(flag){
|
setResizeMode(v){
|
||||||
return e => {
|
return e => {
|
||||||
this.setState({resize: flag});
|
this.setState({resizeMode: v});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,23 +93,19 @@ class NewTaskPanel extends React.Component {
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
<div className="btn-group">
|
<div className="btn-group">
|
||||||
<button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
<button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||||
{this.state.resize ?
|
{ResizeModes.toHuman(this.state.resizeMode)} <span className="caret"></span>
|
||||||
"Yes" : "No"} <span className="caret"></span>
|
|
||||||
</button>
|
</button>
|
||||||
<ul className="dropdown-menu">
|
<ul className="dropdown-menu">
|
||||||
<li>
|
{ResizeModes.all().map(mode =>
|
||||||
<a href="javascript:void(0);"
|
<li key={mode}>
|
||||||
onClick={this.setResize(true)}>
|
<a href="javascript:void(0);"
|
||||||
<i style={{opacity: this.state.resize ? 1 : 0}} className="fa fa-check"></i> Yes</a>
|
onClick={this.setResizeMode(mode)}>
|
||||||
</li>
|
<i style={{opacity: this.state.resizeMode === mode ? 1 : 0}} className="fa fa-check"></i> {ResizeModes.toHuman(mode)}</a>
|
||||||
<li>
|
</li>
|
||||||
<a href="javascript:void(0);"
|
)}
|
||||||
onClick={this.setResize(false)}>
|
|
||||||
<i style={{opacity: !this.state.resize ? 1 : 0}} className="fa fa-check"></i> Skip</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className={"resize-control " + (!this.state.resize ? "hide" : "")}>
|
<div className={"resize-control " + (this.state.resizeMode === ResizeModes.NO ? "hide" : "")}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="100"
|
step="100"
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Dropzone from '../vendor/dropzone';
|
||||||
import csrf from '../django/csrf';
|
import csrf from '../django/csrf';
|
||||||
import HistoryNav from '../classes/HistoryNav';
|
import HistoryNav from '../classes/HistoryNav';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import ResizeModes from '../classes/ResizeModes';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
class ProjectListItem extends React.Component {
|
class ProjectListItem extends React.Component {
|
||||||
|
@ -180,6 +181,10 @@ class ProjectListItem extends React.Component {
|
||||||
if (!formData.has || !formData.has("options")) formData.append("options", JSON.stringify(taskInfo.options));
|
if (!formData.has || !formData.has("options")) formData.append("options", JSON.stringify(taskInfo.options));
|
||||||
if (!formData.has || !formData.has("processing_node")) formData.append("processing_node", taskInfo.selectedNode.id);
|
if (!formData.has || !formData.has("processing_node")) formData.append("processing_node", taskInfo.selectedNode.id);
|
||||||
if (!formData.has || !formData.has("auto_processing_node")) formData.append("auto_processing_node", taskInfo.selectedNode.key == "auto");
|
if (!formData.has || !formData.has("auto_processing_node")) formData.append("auto_processing_node", taskInfo.selectedNode.key == "auto");
|
||||||
|
|
||||||
|
if (taskInfo.resizeMode === ResizeModes.YES){
|
||||||
|
if (!formData.has || !formData.has("resize_to")) formData.append("resize_to", taskInfo.resizeSize);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -225,8 +230,8 @@ class ProjectListItem extends React.Component {
|
||||||
this.dz._taskInfo = taskInfo; // Allow us to access the task info from dz
|
this.dz._taskInfo = taskInfo; // Allow us to access the task info from dz
|
||||||
|
|
||||||
// Update dropzone settings
|
// Update dropzone settings
|
||||||
if (taskInfo.resizeTo !== null){
|
if (taskInfo.resizeMode === ResizeModes.YESINBROWSER){
|
||||||
this.dz.options.resizeWidth = taskInfo.resizeTo;
|
this.dz.options.resizeWidth = taskInfo.resizeSize;
|
||||||
this.dz.options.resizeQuality = 1.0;
|
this.dz.options.resizeQuality = 1.0;
|
||||||
|
|
||||||
this.setUploadState({resizing: true, editing: false});
|
this.setUploadState({resizing: true, editing: false});
|
||||||
|
|
|
@ -351,7 +351,7 @@ class TaskListItem extends React.Component {
|
||||||
let status = statusCodes.description(task.status);
|
let status = statusCodes.description(task.status);
|
||||||
if (status === "") status = "Uploading images";
|
if (status === "") status = "Uploading images";
|
||||||
|
|
||||||
if (!task.processing_node) status = "";
|
if (!task.processing_node) status = "Waiting for a node...";
|
||||||
if (task.pending_action !== null) status = pendingActions.description(task.pending_action);
|
if (task.pending_action !== null) status = pendingActions.description(task.pending_action);
|
||||||
|
|
||||||
let expanded = "";
|
let expanded = "";
|
||||||
|
|
2
start.sh
2
start.sh
|
@ -69,6 +69,8 @@ if [ "$WO_SSL" = "YES" ]; then
|
||||||
proto="https"
|
proto="https"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
./worker.sh scheduler start
|
||||||
|
|
||||||
congrats(){
|
congrats(){
|
||||||
(sleep 5; echo
|
(sleep 5; echo
|
||||||
echo -e "\033[92m"
|
echo -e "\033[92m"
|
||||||
|
|
|
@ -322,6 +322,8 @@ CELERY_TASK_SERIALIZER = 'json'
|
||||||
CELERY_RESULT_SERIALIZER = 'json'
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
CELERY_ACCEPT_CONTENT = ['json']
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
CELERY_INCLUDE=['worker.tasks']
|
CELERY_INCLUDE=['worker.tasks']
|
||||||
|
CELERY_WORKER_REDIRECT_STDOUTS = False
|
||||||
|
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
||||||
if TESTING:
|
if TESTING:
|
||||||
CELERY_TASK_ALWAYS_EAGER = True
|
CELERY_TASK_ALWAYS_EAGER = True
|
||||||
|
|
||||||
|
|
35
worker.sh
35
worker.sh
|
@ -9,7 +9,9 @@ usage(){
|
||||||
echo "This program manages the background worker processes. WebODM requires at least one background process worker to be running at all times."
|
echo "This program manages the background worker processes. WebODM requires at least one background process worker to be running at all times."
|
||||||
echo
|
echo
|
||||||
echo "Command list:"
|
echo "Command list:"
|
||||||
echo " start Start background worker"
|
echo " start Start background worker"
|
||||||
|
echo " scheduler start Start background worker scheduler"
|
||||||
|
echo " scheduler stop Stop background worker scheduler"
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,12 +53,41 @@ start(){
|
||||||
action=$1
|
action=$1
|
||||||
|
|
||||||
echo "Starting worker using broker at $WO_BROKER"
|
echo "Starting worker using broker at $WO_BROKER"
|
||||||
celery -A worker worker --loglevel=info
|
celery -A worker worker --loglevel=warn > /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
start_scheduler(){
|
||||||
|
stop_scheduler
|
||||||
|
if [[ ! -f ./celerybeat.pid ]]; then
|
||||||
|
celery -A worker beat &
|
||||||
|
else
|
||||||
|
echo "Scheduler already running (celerybeat.pid exists)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_scheduler(){
|
||||||
|
if [[ -f ./celerybeat.pid ]]; then
|
||||||
|
kill -9 $(cat ./celerybeat.pid) 2>/dev/null
|
||||||
|
rm ./celerybeat.pid 2>/dev/null
|
||||||
|
echo "Scheduler has shutdown."
|
||||||
|
else
|
||||||
|
echo "Scheduler is not running."
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ $1 = "start" ]]; then
|
if [[ $1 = "start" ]]; then
|
||||||
environment_check
|
environment_check
|
||||||
start
|
start
|
||||||
|
elif [[ $1 = "scheduler" ]]; then
|
||||||
|
if [[ $2 = "start" ]]; then
|
||||||
|
environment_check
|
||||||
|
start_scheduler
|
||||||
|
elif [[ $2 = "stop" ]]; then
|
||||||
|
environment_check
|
||||||
|
stop_scheduler
|
||||||
|
else
|
||||||
|
usage
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
import os
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import re
|
from celery.utils.log import get_task_logger
|
||||||
|
|
||||||
import piexif
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from app.models import Project
|
from app.models import Project
|
||||||
from app.models import Task
|
from app.models import Task
|
||||||
from webodm import settings
|
|
||||||
from nodeodm import status_codes
|
from nodeodm import status_codes
|
||||||
from nodeodm.models import ProcessingNode
|
from nodeodm.models import ProcessingNode
|
||||||
|
from webodm import settings
|
||||||
from .celery import app
|
from .celery import app
|
||||||
from celery.utils.log import get_task_logger
|
|
||||||
from django.db import transaction
|
|
||||||
from PIL import Image
|
|
||||||
from functools import partial
|
|
||||||
from multiprocessing import Pool, cpu_count
|
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
|
@ -84,57 +77,3 @@ def process_pending_tasks():
|
||||||
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
process_task.delay(task.id)
|
process_task.delay(task.id)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def resize_image(image_path, resize_to):
|
|
||||||
try:
|
|
||||||
exif_dict = piexif.load(image_path)
|
|
||||||
im = Image.open(image_path)
|
|
||||||
path, ext = os.path.splitext(image_path)
|
|
||||||
resized_image_path = os.path.join(path + '.resized' + ext)
|
|
||||||
|
|
||||||
width, height = im.size
|
|
||||||
max_side = max(width, height)
|
|
||||||
if max_side < resize_to:
|
|
||||||
logger.warning('We are making {} bigger ({} --> {})'.format(image_path, max_side, resize_to))
|
|
||||||
|
|
||||||
ratio = float(resize_to) / float(max_side)
|
|
||||||
resized_width = int(width * ratio)
|
|
||||||
resized_height = int(height * ratio)
|
|
||||||
|
|
||||||
im.thumbnail((resized_width, resized_height), Image.LANCZOS)
|
|
||||||
|
|
||||||
if len(exif_dict['Exif']) > 0:
|
|
||||||
exif_dict['Exif'][piexif.ExifIFD.PixelXDimension] = resized_width
|
|
||||||
exif_dict['Exif'][piexif.ExifIFD.PixelYDimension] = resized_height
|
|
||||||
im.save(resized_image_path, "JPEG", exif=piexif.dump(exif_dict), quality=100)
|
|
||||||
else:
|
|
||||||
im.save(resized_image_path, "JPEG", quality=100)
|
|
||||||
|
|
||||||
im.close()
|
|
||||||
|
|
||||||
# Delete original image, rename resized image to original
|
|
||||||
os.remove(image_path)
|
|
||||||
os.rename(resized_image_path, image_path)
|
|
||||||
|
|
||||||
logger.info("Resized {}".format(os.path.basename(resized_image_path)))
|
|
||||||
except IOError as e:
|
|
||||||
logger.warning("Cannot resize {}: {}.".format(image_path, str(e)))
|
|
||||||
return None
|
|
||||||
|
|
||||||
return image_path
|
|
||||||
|
|
||||||
@app.task
|
|
||||||
def resize_images(directory, resize_to):
|
|
||||||
"""
|
|
||||||
Destructively resize a directory of JPG images while retaining EXIF tags.
|
|
||||||
Resulting images are always converted to JPG.
|
|
||||||
TODO: add support for tiff files
|
|
||||||
:return list containing paths of resized images
|
|
||||||
"""
|
|
||||||
images_path = [os.path.join(directory, f) for f in os.listdir(directory) if re.match(r'.*\.jpe?g$', f, re.IGNORECASE)]
|
|
||||||
resized_images = list(filter(lambda i: i is not None, Pool(cpu_count()).map(
|
|
||||||
partial(resize_image, resize_to=resize_to),
|
|
||||||
images_path)))
|
|
||||||
return resized_images
|
|
Ładowanie…
Reference in New Issue