decouple work from api requests and move files to object storage

This commit is contained in:
MarcZierle 2022-06-21 15:48:07 +02:00
parent d160bf3e7b
commit 8ae78c5b91
20 changed files with 1201 additions and 177 deletions

@ -1 +0,0 @@
Subproject commit 70828ba4e14e67a1db819de5c6371713145f868c

View File

@ -1,5 +1,11 @@
from rest_framework import serializers
from photo_log.models import PhotoGroup, Photo, PhotoLog, PhotoTag
from photo_log.models import (
PhotoGroup,
Photo,
PhotoLog,
PhotoTag,
PhotoLogTemplate,
)
class PhotoTagSerializer(serializers.ModelSerializer):
@ -8,6 +14,12 @@ class PhotoTagSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'color')
class PhotoLogTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = PhotoLogTemplate
fields = ('id', 'title', 'date', 'render_date', 'start_slide_image', 'slides',)
class PhotoGroupSerializer(serializers.ModelSerializer):
class Meta:
model = PhotoGroup
@ -28,6 +40,12 @@ class PhotoSerializer(serializers.ModelSerializer):
fields = ('id', 'legacy_id', 'group', 'bbox_coords', 'rotate', 'intersections', 'original_image', 'cropped_image', 'ocr_text', 'tag')
class AddPhotoSerializer(serializers.ModelSerializer):
class Meta:
model = Photo
fields = ('id', 'legacy_id', 'group', 'bbox_coords', 'rotate', 'intersections', 'original_image', 'cropped_image', 'ocr_text', 'tag')
class PhotoUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Photo

View File

@ -17,6 +17,9 @@ from .views import (
PhotoTagsAPIView,
RetrieveUpdateDestroyPhotoTagAPIView,
CreatePhotoTagAPIView,
PhotoLogTemplatesAPIView,
CreatePhotoLogTemplateAPIView,
RetrieveUpdateDestroyPhotoLogTemplateAPIView,
)
@ -36,7 +39,12 @@ urlpatterns = [
path('updatephotolog/<int:pk>/', UpdatePhotoLogAPIView.as_view()),
path('updatephoto/<int:pk>/', UpdatePhotoAPIView.as_view()),
path('generatephotolog/<int:pk>/', GeneratePhotoLogAPIView.as_view()),
path('phototag/', CreatePhotoTagAPIView.as_view()),
path('phototags/', PhotoTagsAPIView.as_view()),
path('phototag/<int:pk>/', RetrieveUpdateDestroyPhotoTagAPIView.as_view()),
path('photolog/template/', CreatePhotoLogTemplateAPIView.as_view()),
path('photolog/templates/', PhotoLogTemplatesAPIView.as_view()),
path('photolog/template/<int:pk>/', RetrieveUpdateDestroyPhotoLogTemplateAPIView.as_view()),
]

View File

@ -1,20 +1,30 @@
from rest_framework import generics, views, status
from rest_framework.response import Response
from django.shortcuts import get_list_or_404
from photo_log.models import PhotoGroup, Photo, PhotoLog, PhotoTag
from photo_log.models import (
PhotoGroup,
Photo,
PhotoLog,
PhotoTag,
PhotoLogTemplate,
)
from .serializers import (
PhotoGroupSerializer,
PhotosSerializer,
PhotoSerializer,
AddPhotoSerializer,
PhotoUpdateSerializer,
PhotoLogSerializer,
PhotoLogsSerializer,
PhotoTagSerializer,
)
PhotoLogTemplateSerializer,
)
from .photolog_generator import generate_photolog_from_latex
from .autocrop.autocrop import autocrop
from photo_log import tasks
from django.db.models import FileField
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.files.base import ContentFile
@ -38,6 +48,21 @@ class RetrieveUpdateDestroyPhotoTagAPIView(generics.RetrieveUpdateDestroyAPIView
serializer_class = PhotoTagSerializer
class PhotoLogTemplatesAPIView(generics.ListAPIView):
queryset = PhotoLogTemplate.objects.all()
serializer_class = PhotoLogTemplateSerializer
class CreatePhotoLogTemplateAPIView(generics.CreateAPIView):
queryset = PhotoLogTemplate.objects.all()
serializer_class = PhotoLogTemplateSerializer
class RetrieveUpdateDestroyPhotoLogTemplateAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = PhotoLogTemplate.objects.all()
serializer_class = PhotoLogTemplateSerializer
class PhotoGroupAPIView(generics.ListAPIView):
queryset = PhotoGroup.objects.all()
serializer_class = PhotoGroupSerializer
@ -66,7 +91,7 @@ class PhotoAPIView(generics.RetrieveAPIView):
class AddPhotoAPIView(generics.CreateAPIView):
queryset = Photo.objects.all()
serializer_class = PhotoSerializer
serializer_class = AddPhotoSerializer
class AddPhotoGroupAPIView(generics.CreateAPIView):
@ -141,88 +166,15 @@ class GeneratePhotoLogAPIView(generics.RetrieveAPIView):
photolog_data = self.get_serializer(photolog).data
if photolog_data:
title = photolog_data['title']
date = photolog_data['date']
render_date = photolog_data['render_date']
start_slide_image = photolog_data['start_slide_image']
slides = photolog_data['slides']
log_bytes = generate_photolog_from_latex(request, title, date, render_date, start_slide_image, slides)
tasks.generate_photo_log_chain(
photolog_data['id'],
)
if log_bytes:
photolog.pdf.save('log.pdf', ContentFile(log_bytes))
return Response({'pdf': 'https://zierle-training.riezel.com%s' % str(photolog.pdf.url)})
return Response({'status': 'task created'})
return Response({"error": "Not Found"}, status=status.HTTP_404_NOT_FOUND)
def crop_photo_and_save(id, save=False):
return "code run with id " + str(id)
def save_cropped_pillow_image(photo, pil_img):
cropped_img_field = photo.cropped_image
buffer = BytesIO()
pil_img.save(fp=buffer, format="PNG")
pil_img = ContentFile(buffer.getvalue())
img_name = "cropped_image.png"
cropped_img_field.save(img_name, InMemoryUploadedFile(
pil_img,
None, # field_name
img_name, # image name
'image/png',
pil_img.tell, # image size
None
))
def simple_crop_to_bbox(photo):
img = Image.open(photo.original_image)
img = rotateByExif(img)
bbox = photo.bbox_coords
rotate_angle = photo.rotate
if not img:
return None
#if rotate_angle:
# img = img.rotate(rotate_angle, expand=1)
if bbox:
img = img.crop((
bbox[0][0],
bbox[0][1],
bbox[2][0],
bbox[2][1]
))
return img
def rotateByExif(img):
try:
for orientation in ExifTags.TAGS.keys():
if ExifTags.TAGS[orientation]=='Orientation':
break
exif = img._getexif()
if not exif:
return img
if exif[orientation] == 3:
img=img.rotate(180, expand=True)
elif exif[orientation] == 6:
img=img.rotate(270, expand=True)
elif exif[orientation] == 8:
img=img.rotate(90, expand=True)
except (AttributeError, KeyError, IndexError):
# cases: image don't have getexif
pass
return img
class AutoCropPhotoAPIView(generics.RetrieveAPIView):
queryset = Photo.objects.all()
@ -240,34 +192,30 @@ class AutoCropPhotoAPIView(generics.RetrieveAPIView):
if not 'mode' in request.query_params:
return Response({'error':'cropping mode not specified (auto, bbox or inters)'}, status=status.HTTP_400_BAD_REQUEST)
mode = request.query_params.get('mode')
photo = Photo.objects.get(id=photo_id)
cropped_img = photo.cropped_image
if cropped_img:
cropped_img = cropped_img.name
else:
cropped_img = None
if mode == 'bbox':
photo = Photo.objects.get(id=photo_id)
cropped_img = simple_crop_to_bbox(photo)
save_cropped_pillow_image(photo, cropped_img)
tasks.crop_image_bbox_chain(
photo.id,
photo.original_image.name,
photo.bbox_coords,
photo.rotate,
cropped_img
)
elif mode == 'auto' or mode == 'inters':
photo = Photo.objects.get(id=photo_id)
img = Image.open(photo.original_image)
img = rotateByExif(img)
try:
cropped_img, _, bbox, intersections = autocrop(img)
except Exception:
cropped_img = img
bbox = None
intersections = None
if mode == 'auto':
save_cropped_pillow_image(photo, cropped_img)
if bbox:
photo.bbox_coords = bbox
photo.save()
if intersections:
photo.intersections = intersections
photo.save()
# TODO: mode 'inters': just calculate intersections w/o saving the cropped image
tasks.crop_image_auto_chain(
photo.id,
photo.original_image.name,
cropped_img
)
else:
return Response({'error':'invalid cropping mode (auto, bbox or inters)'}, status=status.HTTP_400_BAD_REQUEST)

View File

@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celeryapp import app as celery_app
__all__ = ('celery_app',)

View File

@ -0,0 +1,24 @@
[program:celery_zierle_training_staging]
directory=/home/marc/www-staging/backend
user=www-data
numprocs=1
stdout_logfile=/var/log/celery/worker.log
stderr_logfile=/var/log/celery/worker.log
autostart=true
autorestart=true
startsecs=10
command=/home/marc/www-staging/backend/env/bin/python3 -m celery -A config worker --loglevel=INFO
; Need to wait for currently executing tasks to finish at shutdown.
; Increase this if you have very long running tasks.
stopwaitsecs = 60
; Causes supervisor to send the termination signal (SIGTERM) to the whole process group.
stopasgroup=true
; Set Celery priority higher than default (999)
; so, if rabbitmq is supervised, it will start first.
priority=1000

22
config/celeryapp.py Normal file
View File

@ -0,0 +1,22 @@
import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
app = Celery('config')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print(f'Request: {self.request!r}')

View File

@ -26,7 +26,7 @@ SECRET_KEY = 'django-insecure-z465dl_(vk55hxbm0bj*mp-ok3!*=ssw#!$5s2nrxa!9j+67z+
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['zierle-training.riezel.com', 'localhost', '127.0.0.1', '192.168.1.244']
ALLOWED_HOSTS = ['zierle-training-staging.riezel.com', 'localhost', '127.0.0.1', '192.168.1.244']
# Application definition
@ -44,8 +44,11 @@ INSTALLED_APPS = [
'rest_framework',
'corsheaders',
'drf_yasg',
'storages',
'django_extensions',
'django_tex',
'colorfield',
'django_celery_results',
# local
'accounts',
@ -146,7 +149,7 @@ AUTH_USER_MODEL = 'accounts.CustomUser'
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
TIME_ZONE = 'CET'
USE_I18N = True
@ -166,29 +169,60 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MINIO = False
MINIO = True
if MINIO:
# MinIO S3 Object-Storage
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'
#STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'
AWS_ACCESS_KEY_ID = 'zierle-trainig-minio'
AWS_SECRET_ACCESS_KEY = 'iGWJt7IFlKzufMtVTnl4W4kGmHz0TTBG'
AWS_ACCESS_KEY_ID = 'zierle-training'
AWS_SECRET_ACCESS_KEY = 'IMienQKx6B5foJRegqZnSTk9MsUjDvd4'
AWS_STORAGE_BUCKET_NAME = 'zierle-training'
AWS_S3_ENDPOINT_URL = 'https://minio.riezel.com'
AWS_DEFAULT_ACL = 'public'
MEDIA_URL = 'https://minio.riezel.com/zierle-training/'
STATIC_URL = 'https://minio.riezel.com/zierle-training/'
#STATIC_URL = 'https://minio.riezel.com/zierle-training/'
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'public, max-age=86400',
}
else:
#MEDIA_URL = 'media/'
#MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
#STATICFILES_DIRS = [os.path.join(BASE_DIR, "static/")]
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
#STATICFILES_DIRS = [os.path.join(BASE_DIR, "static/")]
# Celery
# See https://docs.celeryq.dev/en/stable/django/first-steps-with-django.html
CELERY_CACHE_BACKEND = 'default'
CELERY_WORK_DIR = '/home/marc/www-staging/celery/'
CELERY_BROKER_URL = 'redis://localhost:6378/1'
CELERY_RESULT_BACKEND= 'redis://localhost:6378/1'
CELERY_TIMEZONE = 'CET'
CELERY_BROKER_TRANSPORT_OPTIONS = {
'visibility_timeout': 300,
}
# Redis Cache
# See
CACHES = {
'default': {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6378/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
"KEY_PREFIX": "zierletraining",
}
}

View File

@ -1,9 +1,16 @@
from django.contrib import admin
from .models import PhotoGroup, Photo, PhotoLog, PhotoTag
from .models import (
PhotoGroup,
Photo,
PhotoLog,
PhotoTag,
PhotoLogTemplate,
)
admin.site.register(PhotoGroup)
admin.site.register(Photo)
admin.site.register(PhotoLog)
admin.site.register(PhotoTag)
admin.site.register(PhotoLogTemplate)

425
photo_log/autocrop/autocrop.py Executable file
View File

@ -0,0 +1,425 @@
import cv2
import imutils
import random
import numpy as np
import math
from PIL import Image, ImageDraw
import itertools
import pytesseract
def preprocess_image(image, blur_params=[20,20,25], canny_params=[25,250]):
"""
Turns the image into grayscale, applies a bilateral filter
to smooth out unimportant parts of the image, and returns
the Canny filtered result.
image: cv2.imread image
"""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.bilateralFilter(gray, blur_params[0], blur_params[1], blur_params[2],)
edged = cv2.Canny(gray, canny_params[0], canny_params[1])
return edged
def hough_transform(edged):
"""
Returns and image representing the hough space of the edge image.
edged: Canny filtered cv2 image
"""
max_d = np.sqrt(edged.shape[0]**2 + edged.shape[1]**2)
hough_space = [[0] * (2 * math.ceil(max_d)) for i in range(157*2)]
for y in range(edged.shape[0]):
for x in range(edged.shape[1]):
pixel = edged[y][x]
if not pixel > 0:
continue
for alpha in range(157*2):
d = x * math.cos(alpha/2.0/100.0) + y * math.sin(alpha/2.0/100.0)
hough_space[alpha][round(d)+math.ceil(max_d)] += 1
return hough_space
#TODO fix removal on "the other site" of the hough space => negative indexes in try / except
def get_max_params(hough_space, num_params, rm_radius=7):
"""
Iterates over the maxima of hough space image.
After each maximum found a circle of the radius rm_radius is beeing "cut out"
of the hough space image.
Returns an array of tuples containing the maximum parameters (alpha, d).
hough_space: hough space image
num_params: number of the maxima to be found
rm_radius: optional = 7; the radius to be cut out around the maximum found
"""
all_max_params = []
for i in range(num_params):
hough_array = np.array(hough_space)
max_params = np.unravel_index(hough_array.argmax(), hough_array.shape)
if -math.inf in max_params:
break
alpha = max_params[0]/2.0/100.0
d = max_params[1] - hough_array.shape[1]/2.0
all_max_params.append((alpha, d))
for yi in range(rm_radius*2+1):
for xi in range(rm_radius*2+1):
if math.sqrt((-rm_radius+yi)**2 + (-rm_radius+xi)**2) <= rm_radius:
try:
hough_space[abs(max_params[0]-rm_radius+yi)][abs(max_params[1]-rm_radius+xi)] = -math.inf
except Exception:
pass
return all_max_params, hough_space
def _draw_hough_space(hough_space):
max_val = np.amax(hough_space)
img = Image.new(mode="RGB", size=(len(hough_space[0]), len(hough_space)))
pixels = img.load()
for y in range(len(hough_space)):
for x in range(len(hough_space[0])):
if hough_space[y][x] == -math.inf:
hough_space[y][x] = 0
val = int(hough_space[y][x] / float(max_val) * 255)
pixels[x,y] = (val,val,val)
return img
def _draw_lines(image, all_max_params, resize_height):
img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
img = Image.fromarray(img)
new_height = resize_height
new_width = int(new_height * img.size[0] / img.size[1])
img = img.resize((new_width, new_height), Image.ANTIALIAS)
pixels = img.load()
for (alpha,d) in all_max_params:
dist = 0.5 # line thickness (distance image pixels to line coordinates)
for y in range(img.size[1]):
for x in range(img.size[0]):
val = (x * math.cos(alpha) + y * math.sin(alpha)) - d
if val <= dist and val >= -dist:
pixels[x,y] = (0,200,255)
return img
def get_intersect(a1, a2, b1, b2):
"""
Returns the point of intersection of the lines passing through a2,a1 and b2,b1.
a1: [x, y] a point on the first line
a2: [x, y] another point on the first line
b1: [x, y] a point on the second line
b2: [x, y] another point on the second line
"""
s = np.vstack([a1,a2,b1,b2]) # s for stacked
h = np.hstack((s, np.ones((4, 1)))) # h for homogeneous
l1 = np.cross(h[0], h[1]) # get first line
l2 = np.cross(h[2], h[3]) # get second line
x, y, z = np.cross(l1, l2) # point of intersection
if z == 0: # lines are parallel
return (float('inf'), float('inf'))
return (x/z, y/z)
def insert_intersection(intersections, params1, params2, inters_pt):
if not params1 in intersections:
intersections[params1] = []
intersections[params1].append((params2, inters_pt))
if not params2 in intersections:
intersections[params2] = []
intersections[params2].append((params1, inters_pt))
# TODO fix wrong random intersection points
def find_intersections(all_max_params, img_size, allowed_angle_diff=3):
"""
Takes hough space parameters of found lines and returns a list of tuples of
intersection points.
all_max_params: (alpha, d) tuple list of hough space parameters
img_size: the size of the in which the intersections are to be found in
"""
intersections_by_lines = {}
all_intersections = []
other_max_params = all_max_params.copy()
for (alpha1,d1) in all_max_params:
for (alpha2,d2) in other_max_params:
if alpha1 == alpha2 and d1 == d2:
continue
y = random.randint(0, img_size[0])
x = (d1 - (y * math.sin(alpha1))) / math.cos(alpha1)
a1 = [x,y]
y = random.randint(0, img_size[0])
x = (d1 - (y * math.sin(alpha1))) / math.cos(alpha1)
a2 = [x,y]
y = random.randint(0, img_size[0])
x = (d2 - (y * math.sin(alpha2))) / math.cos(alpha2)
b1 = [x,y]
y = random.randint(0, img_size[0])
x = (d2 - (y * math.sin(alpha2))) / math.cos(alpha2)
b2 = [x,y]
# get intersection point of two lines, where each line
# is given by two random points of each line
# a1, a1: two [x,y] points on line a
# b1, b1: two [x,y] points on line b
inters = get_intersect(a1,a2, b1,b2)
# are lines parallel or is intersection outside of the image?
if math.inf in inters:
continue
if inters[0] < 0 or inters[0] >= img_size[1]:
continue
if inters[1] < 0 or inters[1] >= img_size[0]:
continue
# calculate vectors of each line 1 and 2
p1 = [a1[0]-a2[0], a1[1]-a2[1]]
p2 = [b1[0]-b2[0], b1[1]-b2[1]]
inters = (round(inters[0]),round(inters[1]))
try:
dot_product = p1[0]*p2[0] + p1[1]*p2[1]
abs_p1 = math.sqrt(p1[0]**2 + p1[1]**2)
abs_p2 = math.sqrt(p2[0]**2 + p2[1]**2)
angle = math.degrees(math.acos(dot_product / (abs_p1 * abs_p2)))
angle_diff = abs(abs(angle) - 90)
except ValueError as e:
print(e)
continue
if not(angle_diff > allowed_angle_diff):
all_intersections.append(inters)
params1 = (alpha1, d1)
params2 = (alpha2, d2)
insert_intersection(intersections_by_lines, params1, params2, inters)
other_max_params.remove((alpha1, d1))
return intersections_by_lines, all_intersections
def _draw_intersections(image, intersections):
image = image.copy()
for inters in intersections:
color = (0,255,0)
pt_radius = 1
draw = ImageDraw.Draw(image)
draw.ellipse((inters[0]-pt_radius,inters[1]-pt_radius,inters[0]+pt_radius,inters[1]+pt_radius), fill=color, outline=color)
return image
def get_n_connections(start, intersections_by_lines, n):
if n <= 0:
return [[start]]
connections = []
for line in intersections_by_lines[start]:
neighbors_connections = get_n_connections(line[0], intersections_by_lines, n-1)
for con in neighbors_connections:
if not start in con:
if len(con) == 1:
connections.append([start, con[0]])
else:
connections.append([start] + con)
return connections
def get_cycles(intersections_by_lines, n=4):
start = list(intersections_by_lines.keys())[0]
connections = get_n_connections(start, intersections_by_lines, n=n-1)
cycles = []
for connection in connections:
last_vertex = connection[-1]
# can we get back to the beginning?
if start in [con[0] for con in intersections_by_lines[last_vertex]]:
cycles.append(connection + [start])
return cycles
def get_intersection_points_from_lines(rects_lines, intersections_by_lines):
rects_points = []
for rect in rects_lines:
points = []
for i in range(len(rect)-1):
line1 = rect[i]
line2 = rect[i+1]
points.append(list(filter(lambda con: con[0]==line2, intersections_by_lines[line1]))[0][1])
points.append(points[0])
rects_points.append(points)
return rects_points
def remove_small_rects(rects_points, image_shape, min_image_coverage=0.3):
image_area = image_shape[0] * image_shape[1]
possible_rects = []
for rect in rects_points:
pt1 = rect[0]
pt2 = rect[1]
pt3 = rect[2]
len_side1 = math.sqrt((pt1[0]-pt2[0])**2 + (pt1[1]-pt2[1])**2)
len_side2 = math.sqrt((pt2[0]-pt3[0])**2 + (pt2[1]-pt3[1])**2)
area = len_side1 * len_side2
if area >= min_image_coverage * image_area:
possible_rects.append(rect)
return possible_rects
def _draw_rects(rects_points, image):
image_lines = image.copy()
draw = ImageDraw.Draw(image_lines)
for rect in rects_points:
rect = [(pt[0],pt[1]) for pt in rect]
draw.line(rect, width=2, fill='yellow')
return image_lines
def crop_warp_image(image, rect):
#rect = np.asarray(rect[:4])
rect = np.array(rect,dtype = "float32").reshape(4,2)
ordered_points = np.zeros((4, 2), dtype = "float32")
s = rect.sum(axis = 1)
ordered_points[0] = rect[np.argmin(s)]
ordered_points[2] = rect[np.argmax(s)]
diff = np.diff(rect, axis = 1)
ordered_points[1] = rect[np.argmin(diff)]
ordered_points[3] = rect[np.argmax(diff)]
(tl, tr, br, bl) = ordered_points
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))
maxHeight = max(int(heightA), int(heightB))
dst_rect = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]], dtype = "float32")
M = cv2.getPerspectiveTransform(ordered_points, dst_rect)
warped_img = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
return warped_img
def get_rect_bounding_box(rect):
min_x = min([pt[0] for pt in rect])
max_x = max([pt[0] for pt in rect])
min_y = min([pt[1] for pt in rect])
max_y = max([pt[1] for pt in rect])
bbox = [[min_x, min_y], [max_x, min_y], [max_x, max_y], [min_x, max_y]]
return bbox
def _cv2ImageToPIL(cv2_image):
#cv2_image = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(cv2_image)
return pil_image
def _PILImageToCv2(pil_image):
cv2_image = np.asarray(pil_image)
return cv2_image
def autocrop(original_image, DEBUG=False):
"""
Automatically crops an image to size of the flip chart in the given image,
and returns a cropped PIL image.
image: the PIL image to crop
"""
resize_height = 300
blur_params = [35,15,40] # size, color, space
canny_params = [80,250]
num_hough_params = 50
hough_rm_radius = 20
line_angle_diff = 3
min_image_coverage = 0.3
original_image = _PILImageToCv2(original_image)
# shrink image to a smaller size to speed up autocropping
ratio = original_image.shape[0] / float(resize_height)
image_small = imutils.resize(original_image, height=resize_height)
edge_image = preprocess_image(image_small, blur_params=blur_params, canny_params=canny_params)
if DEBUG:
cv2.imwrite('debug_out/edges.png', edge_image)
hough_space = hough_transform(edge_image)
all_max_params, hough_space = get_max_params(hough_space, num_params=num_hough_params, rm_radius=hough_rm_radius)
if DEBUG:
hough_space_image = _draw_hough_space(hough_space)
hough_space_image.save("debug_out/hough.png")
image_lines = _draw_lines(original_image, all_max_params, resize_height)
intersections_by_lines, all_intersections = find_intersections(all_max_params, image_small.shape, allowed_angle_diff=line_angle_diff)
if DEBUG:
image_lines = _draw_intersections(image_lines, all_intersections)
rects_lines = get_cycles(intersections_by_lines)
rects_points = get_intersection_points_from_lines(rects_lines, intersections_by_lines)
rects_points = remove_small_rects(rects_points, image_small.shape, min_image_coverage=min_image_coverage)
if DEBUG:
image_lines = _draw_rects(rects_points, image_lines)
image_lines.save("debug_out/line.png")
# TODO: find best rectangle
try:
best_rect = rects_points[0]
best_rect = [(round(pt[0]*ratio), round(pt[1]*ratio)) for pt in best_rect]
best_rect = best_rect[:4]
cropped_image = crop_warp_image(original_image, best_rect)
cropped_image = _cv2ImageToPIL(cropped_image)
bbox = get_rect_bounding_box(best_rect)
except Exception as e:
cropped_image = _cv2ImageToPIL(original_image)
best_rect = None
bbox = None
intersections_as_list = [[round(i[0]*ratio), round(i[1]*ratio)] for i in all_intersections]
return cropped_image, best_rect, bbox, intersections_as_list
if __name__ == "__main__":
#img = Image.open('example_imgs/bill.png')
img = Image.open('example_imgs/image4.jpg')
cropped_image, rect, bbox, intersections = autocrop(img, DEBUG=True)
cropped_image.save('debug_out/cropped.png')
print(rect)
print(bbox)
print(intersections)

16
photo_log/autocrop/ocr.py Executable file
View File

@ -0,0 +1,16 @@
################################################################
##### OCR
################################################################
gray = cv2.cvtColor(cropped_image, cv2.COLOR_BGR2GRAY)
gray = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
gray = cv2.medianBlur(gray, 3)
cv2.imwrite("debug_out/bin.png", gray)
img = Image.fromarray(cropped_image)
tessdata_dir_config = r'--tessdata-dir "./ocr_langs"'
text = pytesseract.image_to_string(gray, lang='deu-best', config=tessdata_dir_config)
print(text)

Binary file not shown.

View File

@ -0,0 +1,13 @@
imageio==2.13.5
imutils==0.5.4
networkx==2.6.3
numpy==1.22.0
opencv-python==4.5.5.62
packaging==21.3
Pillow==9.0.0
pyparsing==3.0.6
pytesseract==0.3.8
PyWavelets==1.2.0
scikit-image==0.19.1
scipy==1.7.3
tifffile==2021.11.2

View File

@ -2,6 +2,10 @@ from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.dispatch import receiver
from model_utils import FieldTracker
from . import tasks
from colorfield.fields import ColorField
from datetime import date
@ -34,11 +38,11 @@ def get_default_photogroup():
def get_original_photo_path(instance, filename):
_, ext = os.path.splitext(filename)
return 'static/original_images/%s%s' % (str(uuid.uuid4()), ext)
return 'original_images/%s%s' % (str(uuid.uuid4()), ext)
def get_cropped_photo_path(instance, filename):
_, ext = os.path.splitext(filename)
return 'static/cropped_images/%s%s' % (str(uuid.uuid4()), ext)
return 'cropped_images/%s%s' % (str(uuid.uuid4()), ext)
class Photo(models.Model):
@ -81,62 +85,64 @@ class Photo(models.Model):
default=None,
)
tracker = FieldTracker()
def __str__(self):
return "Photo #" + str(self.id)
@receiver(models.signals.post_delete, sender=Photo)
def auto_delete_file_on_delete(sender, instance, **kwargs):
"""
Deletes file from filesystem
when corresponding `MediaFile` object is deleted.
"""
if instance.original_image:
if os.path.isfile(instance.original_image.path):
os.remove(instance.original_image.path)
instance.original_image.delete(save=False)
if instance.cropped_image:
if os.path.isfile(instance.cropped_image.path):
os.remove(instance.cropped_image.path)
instance.cropped_image.delete(save=False)
@receiver(models.signals.pre_save, sender=Photo)
def auto_delete_file_on_change(sender, instance, **kwargs):
"""
Deletes old file from filesystem
when corresponding `MediaFile` object is updated
with new file.
"""
if not instance.pk:
return False
try:
old_file = Photo.objects.get(pk=instance.pk).original_image
photo = Photo.objects.get(pk=instance.pk)
except Photo.DoesNotExist:
return False
# check original image for change
new_file = instance.original_image
old_file = photo.original_image
if not old_file == new_file:
if os.path.isfile(old_file.path):
os.remove(old_file.path)
photo.original_image.delete(save=False)
try:
old_file = Photo.objects.get(pk=instance.pk).cropped_image
except Photo.DoesNotExist:
return False
# check cropped image for change
new_file = instance.cropped_image
old_file = photo.cropped_image
if not old_file == new_file:
photo.cropped_image.delete(save=False)
if old_file.name:
new_file = instance.cropped_image
if not old_file == new_file:
if os.path.isfile(old_file.path):
os.remove(old_file.path)
@receiver(models.signals.post_save, sender=Photo)
def max_resize_images(sender, instance, **kwargs):
MAX_IMAGE_WIDTH = 800
tracker = instance.tracker
original_image_s3_path = instance.original_image.name
if original_image_s3_path and tracker.has_changed('original_image'):
tasks.max_resize_image_chain(original_image_s3_path, MAX_IMAGE_WIDTH)
cropped_image_s3_path = instance.cropped_image.name
if cropped_image_s3_path and tracker.has_changed('cropped_image'):
tasks.max_resize_image_chain(cropped_image_s3_path, MAX_IMAGE_WIDTH)
def get_empty_photolog_default():
return []
def get_photolog_pdf_path(instance, filename):
return "static/photolog_pdf/%s.pdf" % str(uuid.uuid4())
return "photolog_pdf/%s.pdf" % str(uuid.uuid4())
class PhotoLog(models.Model):
title = models.CharField(null=False, blank=False, max_length=5000)
@ -159,35 +165,23 @@ class PhotoLog(models.Model):
def __str__(self):
return self.title
class PhotoLogTemplate(models.Model):
title = models.CharField(null=False, blank=False, max_length=5000)
date = models.DateField(auto_now=False, default=date.today)
render_date = models.BooleanField(null=False, blank=True, default=True)
start_slide_image = models.IntegerField(null=True, blank=True, default=3)
slides = models.JSONField(
null=False,
blank=False,
default=list,
)
def __str__(self):
return self.title
@receiver(models.signals.post_delete, sender=PhotoLog)
def auto_delete_file_on_delete(sender, instance, **kwargs):
"""
Deletes file from filesystem
when corresponding `MediaFile` object is deleted.
"""
if instance.pdf:
if os.path.isfile(instance.pdf.path):
os.remove(instance.pdf.path)
@receiver(models.signals.pre_save, sender=PhotoLog)
def auto_delete_file_on_change(sender, instance, **kwargs):
"""
Deletes old file from filesystem
when corresponding `MediaFile` object is updated
with new file.
"""
if not instance.pk:
return False
try:
old_file = PhotoLog.objects.get(pk=instance.pk).pdf
except PhotoLog.DoesNotExist:
return False
if not old_file.name:
return False
new_file = instance.pdf
if not old_file == new_file:
if os.path.isfile(old_file.path):
os.remove(old_file.path)
instance.pdf.delete(save=False)

55
photo_log/photolog_layout.py Executable file
View File

@ -0,0 +1,55 @@
from datetime import datetime
from django.conf import settings
import os
def generate_tex(title, date, render_date, start_slide_image, slides, id_to_name, work_dir):
log_start = "% !TEX program = xelatex\n\\documentclass[aspectratio=169]{beamer}\n\\usepackage{graphicx}\n\\usepackage{tikz}\n\\usepackage[absolute,overlay]{textpos}\n\\definecolor{gray1}{rgb}{0.85,0.85,0.85}\n\\definecolor{gray2}{rgb}{0.95,0.95,0.95}\n\\setbeamertemplate{background canvas}{%\n\\begin{tikzpicture}[remember picture,overlay]\n\\begin{scope}[xshift=0.55\\textwidth, yshift=-4.5cm]\n\\fill[even odd rule,inner color=gray2,outer color=gray1] (0,0) circle (10);\n\\node[inner sep=0pt] at (0.26,-4.02) {\\includegraphics[width=1.16\\textwidth]{wood_floor}};\n\\end{scope}\n\\end{tikzpicture}%\n}\n\\setbeamertemplate{navigation symbols}{}\n\\vfuzz=1000pt\n\\hfuzz=1000pt\n\\usepackage{fontspec}\n\\newfontfamily{\\gill} [ Path = "+str(settings.BASE_DIR)+"/static/photolog_assets/,UprightFont = * Medium,BoldFont = * Bold] {Gill Sans MT}\n\\definecolor{title_line_red}{rgb}{0.8,0.2,0.2}\n\\makeatletter\n\\let\\old@rule\\@rule\n\\def\\@rule[#1]#2#3{\\textcolor{title_line_red}{\\old@rule[#1]{#2}{#3}}}\n\\makeatother\n\\title{Fotoprotokoll}\n\\author{Antje Zierle-Kohlmorgen}\n\\institute{Zierle-Training}\n\\date{2022}\n\\begin{document}\n\\gill"
log_end = "\\begin{frame}\n\\begin{columns}\n\\begin{column}{0.6\\textwidth}\n\\begin{center}\n\\includegraphics[width=0.8\\textwidth]{smile}\n\\end{center}\n\\end{column}\n\\begin{column}{0.4\\textwidth}\n\\Huge{Viel Erfolg!}\n\\end{column}\n\\end{columns}\n\\end{frame}\n\\end{document}"
title_slide_start = "\\begin{frame}\n\\begin{textblock*}{13.0cm}(3cm,1.75cm)\n\\huge{"
title_slide_end_w_photo = "}\\\\[-0.25cm]\n\\rule{14cm}{0.05cm}\n\\begin{tikzpicture}[remember picture, overlay]\n\\node[xshift=0.8\\textwidth,yshift=-0.5cm]{\\includegraphics[height=4.5cm]{%s}};\n\\end{tikzpicture}\n\\end{textblock*}\n\\end{frame}"
title_slide_end_wo_photo = "}\\\\[-0.25cm]\n\\rule{14cm}{0.05cm}\n\n\\end{textblock*}\n\\end{frame}"
slide_start = "\\begin{frame}\n\\begin{columns}"
slide_end = "\\end{columns}\n\\hfill\\\\[1.5cm]\n\\end{frame}"
photo_start = "\\begin{column}{%.3f\\textwidth}\n\\begin{center}\n\\includegraphics[height=6.5cm]{"
photo_end = "}\\end{center}\n\\end{column}"
assets_path = "{%s}" % (os.path.abspath(os.path.join(work_dir, '..', 'photolog_assets')))
photos_path = "{%s}" % (str(work_dir))
include_paths = "\\graphicspath{"+assets_path+","+photos_path+"}\n"
tex_file_content = log_start
tex_file_content += include_paths
tex_file_content += title_slide_start + title
if date and render_date:
date = datetime.strptime(date, '%Y-%m-%dT%H:%M:%S')
tex_file_content += "\\\\" + date.strftime("%d.%m.%Y")
if start_slide_image:
tex_file_content += title_slide_end_w_photo % (id_to_name[str(start_slide_image)])
else:
tex_file_content += title_slide_end_wo_photo
for slide in slides:
tex_file_content += slide_start
num_none_photos = sum([1 for p in slide if p==None])
for photo_id in slide:
if not photo_id:
continue
if num_none_photos == 2: # slides are saved as [2, None, None] for only 1 image
tex_file_content += photo_start % (3.0/len(slide))
else:
tex_file_content += photo_start % (1.0/len(slide))
tex_file_content += id_to_name[str(photo_id)]
tex_file_content += photo_end
tex_file_content += slide_end
tex_file_content += log_end
return tex_file_content

389
photo_log/tasks.py Normal file
View File

@ -0,0 +1,389 @@
from celery import shared_task, chain, group, chord
import boto3
import imghdr
from PIL import Image, ExifTags
import os, shutil
from uuid import uuid4
from django.conf import settings
from django.core.files import File
from django.apps import apps
from django_tex.shortcuts import compile_template_to_pdf
from .photolog_layout import generate_tex
from .autocrop.autocrop import autocrop
@shared_task
def chordfinisher(*args, **kwargs):
"""
Used at the end of chord( group( ... ), chordfinisher.s())
to chain multiple groups together.
"""
return 'OK'
@shared_task
def download_s3_file(folder_path, s3_file_path, bucket):
"""
Downloads a file stored in a S3 object storages.
:param folder_path Path on local disk where the file should be saved.
:param s3_file_path Full file path in the S3 storage bucket (without the buckets name).
:param bucket The name of the bucket where the file is stored in.
"""
# create local folder
if not os.path.exists(folder_path):
os.makedirs(folder_path, exist_ok=True) # mkdir -p
s3_resource = boto3.resource(
's3',
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
client = s3_resource.meta.client
# retrieve the file name if the file is stored in a sub dir on the S3
file_name = s3_file_path.split('/')[-1]
client.download_file(bucket, s3_file_path, os.path.join(folder_path, file_name))
@shared_task
def upload_s3_file(file_path, s3_file_path, bucket):
"""
Uploads a file to a S3 object storages.
:param file_path Path on local disk where the is saved.
:param s3_file_path Full file path in the S3 storage bucket (without the buckets name).
:param bucket The name of the bucket where the file will be stored in.
"""
s3_resource = boto3.resource(
's3',
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
client = s3_resource.meta.client
client.upload_file(file_path, bucket, s3_file_path)
@shared_task
def update_model_field(app_name, model_name, pk, field_name, new_value):
model = apps.get_model(app_name, model_name)
instance = model.objects.get(pk=pk)
setattr(instance, field_name, new_value)
instance.save()
def rotateByExif(img):
try:
for orientation in ExifTags.TAGS.keys():
if ExifTags.TAGS[orientation]=='Orientation':
break
exif = img._getexif()
if not exif:
return img
if exif[orientation] == 3:
img=img.rotate(180, expand=True)
elif exif[orientation] == 6:
img=img.rotate(270, expand=True)
elif exif[orientation] == 8:
img=img.rotate(90, expand=True)
except (AttributeError, KeyError, IndexError):
# cases: image don't have getexif
pass
return img
@shared_task
def max_resize_image(image_path, max_width):
if not imghdr.what(image_path):
return
img = Image.open(image_path)
img = rotateByExif(img)
if img.size[0] <= max_width:
return
wpercent = (max_width/float(img.size[0]))
hsize = int((float(img.size[1])*float(wpercent)))
img = img.resize((max_width,hsize), Image.ANTIALIAS)
img.save(image_path)
@shared_task
def crop_image_bbox(image_path, bbox, rotate_angle):
if not imghdr.what(image_path):
return
img = Image.open(image_path)
img = rotateByExif(img)
if rotate_angle:
img = img.rotate(rotate_angle, expand=1)
if bbox:
img = img.crop((
bbox[0][0],
bbox[0][1],
bbox[2][0],
bbox[2][1]
))
img.save(image_path)
@shared_task
def crop_image_auto(image_path):
if not imghdr.what(image_path):
return
img = Image.open(image_path)
img = rotateByExif(img)
try:
cropped_img, _, bbox, intersections = autocrop(img)
except Exception:
cropped_img = img
bbox = None
intersections = None
# TODO: save bbox an intersections in photo.bbox_cords and photo.intersections
cropped_img.save(image_path)
@shared_task
def delete_folder(folder_path):
shutil.rmtree(folder_path, ignore_errors=True)
@shared_task
def generate_photolog_from_latex(title, date, render_date, start_slide_image, slides, id_to_name, work_dir, out_file):
template_name = 'photolog.tex'
context = {
'content': generate_tex(title, date, render_date, start_slide_image, slides, id_to_name, work_dir)
}
pdf_bytes = compile_template_to_pdf(template_name, context)
with open(out_file, 'wb+') as file:
file.write(pdf_bytes)
def max_resize_image_chain(full_file_name, max_width, work_folder_name=uuid4()):
BASE_DIR = settings.CELERY_WORK_DIR
folder = os.path.join(BASE_DIR, str(work_folder_name))
local_file = os.path.join(folder, full_file_name.split('/')[-1])
bucket = settings.AWS_STORAGE_BUCKET_NAME
chain(
download_s3_file.si(
folder,
full_file_name,
bucket
),
max_resize_image.si(
local_file,
max_width=max_width
),
upload_s3_file.si(
local_file,
full_file_name,
bucket
),
delete_folder.si(
folder
),
)()
def crop_image_bbox_chain(photo_pk, full_file_name, bbox, rotate_angle, cropped_img_name=None, work_folder_name=uuid4()):
BASE_DIR = settings.CELERY_WORK_DIR
if not cropped_img_name:
cropped_img_name = os.path.join('cropped_images', str(uuid4()) + '.png')
folder = os.path.join(BASE_DIR, str(work_folder_name))
local_file = os.path.join(folder, os.path.basename(full_file_name))
bucket = settings.AWS_STORAGE_BUCKET_NAME
s3_upload_name = 'cropped_images/' + os.path.basename(cropped_img_name)
chain(
download_s3_file.si(
folder,
full_file_name,
bucket
),
crop_image_bbox.si(
local_file,
bbox,
rotate_angle
),
upload_s3_file.si(
local_file,
s3_upload_name,
bucket
),
update_model_field.si(# photo_log.Photo(pk=photo_pk).cropped_image = s3_upload_name
'photo_log', 'Photo', photo_pk,
'cropped_image', s3_upload_name
),
delete_folder.si(
folder
),
)()
def crop_image_auto_chain(photo_pk, full_file_name, cropped_img_name=None, work_folder_name=uuid4()):
BASE_DIR = settings.CELERY_WORK_DIR
if not cropped_img_name:
cropped_img_name = os.path.join('cropped_images', str(uuid4()) + '.png')
folder = os.path.join(BASE_DIR, str(work_folder_name))
local_file = os.path.join(folder, os.path.basename(full_file_name))
bucket = settings.AWS_STORAGE_BUCKET_NAME
s3_upload_name = 'cropped_images/' + os.path.basename(cropped_img_name)
chain(
download_s3_file.si(
folder,
full_file_name,
bucket
),
crop_image_auto.si(
local_file
),
upload_s3_file.si(
local_file,
s3_upload_name,
bucket
),
update_model_field.si(# photo_log.Photo(pk=photo_pk).cropped_image = s3_upload_name
'photo_log', 'Photo', photo_pk,
'cropped_image', s3_upload_name
),
delete_folder.si(
folder
),
)()
def get_photo_log_assets_tasks():
BASE_DIR = settings.CELERY_WORK_DIR
folder = os.path.join(BASE_DIR, 'photolog_assets')
bucket = settings.AWS_STORAGE_BUCKET_NAME
download_tasks = []
asset_files = [
'Gill Sans MT Bold.ttf',
'Gill Sans MT Medium.ttf',
'smile.png',
'wood_floor.jpg'
]
for file in asset_files:
path = os.path.join(folder, file)
if not os.path.exists(path):
download_tasks.append(
download_s3_file.si(
folder,
'photolog_assets/' + file,
bucket
)
)
return download_tasks
def generate_photo_log_chain(photo_log_id, work_folder_name=uuid4()):
from photo_log.models import Photo, PhotoLog
BASE_DIR = settings.CELERY_WORK_DIR
folder = os.path.join(BASE_DIR, str(work_folder_name))
bucket = settings.AWS_STORAGE_BUCKET_NAME
photo_log = PhotoLog.objects.get(pk=photo_log_id)
slides = photo_log.slides
slides = [photo for slide in slides for photo in slide] # flatten slides lists
if photo_log.start_slide_image:
slides.append(photo_log.start_slide_image)
photos = Photo.objects.filter(pk__in=slides).values('id', 'original_image', 'cropped_image')
photo_id_to_file_name = {}
download_files_tasks = []
for photo in photos:
image = photo['cropped_image']
if not image:
image = photo['original_image']
download_files_tasks.append(
download_s3_file.si(
folder,
image,
bucket
)
)
photo_id_to_file_name[photo['id']], _ = os.path.splitext(os.path.basename(image))
download_files_tasks.extend(get_photo_log_assets_tasks())
download_files_tasks = chord( group(download_files_tasks), chordfinisher.si() )
pdf_file = photo_log.pdf.name
if pdf_file:
pdf_file = os.path.basename(pdf_file)
else:
pdf_file = os.path.join(str(uuid4()) + '.pdf')
pdf_file = os.path.join(folder, pdf_file)
pdf_s3_path = 'photolog_pdf/' + os.path.basename(pdf_file)
chain(
download_files_tasks,
generate_photolog_from_latex.si(
photo_log.title,
photo_log.date,
photo_log.render_date,
photo_log.start_slide_image,
photo_log.slides,
photo_id_to_file_name,
folder,
pdf_file
),
upload_s3_file.si(
pdf_file,
pdf_s3_path,
bucket
),
update_model_field.si(
'photo_log', 'PhotoLog', photo_log.id,
'pdf', pdf_s3_path
),
delete_folder.si(
folder
)
)()

View File

@ -1,20 +1,37 @@
amqp==5.1.1
asgiref==3.4.1
async-timeout==4.0.2
attrs==21.4.0
autobahn==22.4.2
Automat==20.2.0
beautifulsoup4==4.10.0
billiard==3.6.4.0
boto3==1.24.11
botocore==1.27.11
celery==5.2.7
certifi==2021.10.8
cffi==1.15.0
channels==3.0.4
charset-normalizer==2.0.7
click==8.1.3
click-didyoumean==0.3.0
click-plugins==1.1.1
click-repl==0.2.0
constantly==15.1.0
coreapi==2.3.3
coreschema==0.0.4
cryptography==37.0.2
daphne==3.0.2
Deprecated==1.2.13
Django==3.2.8
django-celery-results==2.3.1
django-colorfield==0.7.1
django-cors-headers==3.12.0
django-extensions==3.1.5
django-filter==21.1
django-model-utils==4.2.0
django-redis==5.2.0
django-storages==1.12.3
django-tex==1.1.10
djangorestframework==3.13.1
drf-yasg==1.20.0
@ -27,6 +44,8 @@ incremental==21.3.0
inflection==0.5.1
itypes==1.2.0
Jinja2==3.1.2
jmespath==1.0.0
kombu==5.2.4
lxml==4.6.3
Markdown==3.3.7
MarkupSafe==2.1.1
@ -35,6 +54,7 @@ numpy==1.22.0
opencv-python==4.5.5.62
packaging==21.3
Pillow==9.0.0
prompt-toolkit==3.0.29
psycopg2==2.9.1
pyasn1==0.4.8
pyasn1-modules==0.2.8
@ -45,9 +65,11 @@ pytesseract==0.3.8
python-dateutil==2.8.2
pytz==2021.3
PyWavelets==1.2.0
redis==4.3.3
requests==2.26.0
ruamel.yaml==0.17.21
ruamel.yaml.clib==0.2.6
s3transfer==0.6.0
scikit-image==0.19.1
scipy==1.7.3
service-identity==21.1.0
@ -61,4 +83,7 @@ txaio==22.2.1
typing_extensions==4.2.0
uritemplate==4.1.1
urllib3==1.26.7
vine==5.0.0
wcwidth==0.2.5
wrapt==1.14.1
zope.interface==5.4.0

0
scripts/__init__.py Normal file
View File

View File

@ -0,0 +1,37 @@
from photo_log.models import Photo
from photo_log import tasks
from celery import chain
import os
def run():
BASE_DIR = '/home/marc/www-staging/celery/'
photo_pk = 126
full_file_name = Photo.objects.get(pk=photo_pk).original_image.name
folder = os.path.join(BASE_DIR, str(photo_pk))
local_file = os.path.join(folder, full_file_name.split('/')[-1])
bucket = 'zierle-training'
res = chain(
tasks.download_s3_file.si(
folder,
full_file_name,
bucket
),
tasks.max_resize_image.si(
local_file,
max_width=75
),
tasks.upload_s3_file.si(
local_file,
full_file_name,
bucket
)
)()
print(res.get())

View File

@ -0,0 +1,5 @@
from photo_log import tasks
def run():
tasks.generate_photo_log_chain(45)