From 84a4cf7cf80bc71b6fdfe7a2b9f6a48c9936395f Mon Sep 17 00:00:00 2001 From: Damien De Paoli Date: Fri, 15 Jul 2022 17:18:31 +1000 Subject: [PATCH] added ability to auto-rotate jpegs as we import them. The auto-rotation uses /usr/bin/exifautotran which rotates losslessly, and we optimised to then not also re-rotate the thumbmail. This address a few bugs in the 90s, including the one where Mandys photos were not getting faces (they were rotated), and without really doing anything the odd one where we sometimes lost tmp_locn on first load after db recreation - I cant reproduce so ignoring it --- BUGs | 5 +---- TODO | 1 + pa_job_manager.py | 41 ++++++++++++++++++++++++++++++++++++++--- person.py | 3 ++- settings.py | 2 +- shared.py | 19 ++++++++++++++----- 6 files changed, 57 insertions(+), 14 deletions(-) diff --git a/BUGs b/BUGs index ab7241f..4d138d0 100644 --- a/BUGs +++ b/BUGs @@ -1,5 +1,2 @@ -### Next: 92 +### Next: 94 BUG-85: once we rebuild data from scratch, need to reset just clean out pa_user_state's -BUG-91: face_recognition not working on many of Mandy's newer phone images - -- its when a photo is rotated -- can use: - exifautotran file.jpg, and then all should work (no loss), could then skip fix in thumbs too? diff --git a/TODO b/TODO index faa2533..ed1ee4d 100644 --- a/TODO +++ b/TODO @@ -7,6 +7,7 @@ [DONE] - ignore/not a face/too young [DONE] - redraw 'ignore's as a greyed out box? [DONE] - menu should only allow override IF we have put override on... + all NMO's need to handle delete data and rebuild / allow recreation of content form FS (not just test, it causes a bug now / db constraint violation) --> need to test the 'override' when we re-ai-match (AFTER re-build from FS) * run_ai_on throws log line, for matching even if there are no faces, would be less noisy to not do that (or should say no faces?) diff --git a/pa_job_manager.py b/pa_job_manager.py index 3838091..5f9a1f3 100644 --- a/pa_job_manager.py +++ b/pa_job_manager.py @@ -238,6 +238,7 @@ class Settings(Base): import_path = Column(String) storage_path = Column(String) recycle_bin_path = Column(String) + auto_rotate = Column(Boolean) default_refimg_model = Column(Integer,ForeignKey('ai_model.id'), unique=True, nullable=False) default_scan_model = Column(Integer,ForeignKey('ai_model.id'), unique=True, nullable=False) default_threshold = Column(Integer) @@ -248,7 +249,7 @@ class Settings(Base): bin_cleanup_file_age = Column(Integer) def __repr__(self): - return f"" + return f"" ################################################################################ # Class describing Person to Refimg link in DB via sqlalchemy @@ -422,6 +423,37 @@ class PA_JobManager_FE_Message(Base): def __repr__(self): return "" + + ############################################################################## # MessageToFE(): sends a specific alert/messasge for a given job via the DB to # the front end @@ -789,6 +821,7 @@ def JobScanStorageDir(job): ############################################################################## def JobForceScan(job): JobProgressState( job, "In Progress" ) + session.query(PA_UserState).delete() session.query(FaceFileLink).delete() session.query(FaceRefimgLink).delete() session.query(Face).delete() @@ -1526,7 +1559,8 @@ def JobTransformImage(job): out = im.rotate(int(amt), expand=True) out.save( e.FullPathOnFS() ) print( f"JobTransformImage DONE transform: job={job.id}, id={id}, amt={amt}" ) - e.file_details.thumbnail, _ , _ = GenThumb( e.FullPathOnFS() ) + settings = session.query(Settings).first() + e.file_details.thumbnail, _ , _ = GenThumb( e.FullPathOnFS(), settings.auto_rotate ) e.file_details.hash = md5( job, e ) print( f"JobTransformImage DONE thumb: job={job.id}, id={id}, amt={amt}" ) session.add(e) @@ -1642,7 +1676,8 @@ def isImage(file): #################################################################################################################################### def GenImageThumbnail(job, file): ProcessFileForJob( job, "Generate Thumbnail from Image file: {}".format( file ), file ) - thumb, _, _ = GenThumb(file) + settings = session.query(Settings).first() + thumb, _, _ = GenThumb(file, settings.auto_rotate) return thumb #################################################################################################################################### diff --git a/person.py b/person.py index d364412..36a6c17 100644 --- a/person.py +++ b/person.py @@ -97,7 +97,8 @@ class PersonForm(FlaskForm): def AddRefimgToPerson( filename, person ): refimg = Refimg( fname=os.path.basename( filename ) ) try: - refimg.thumbnail, refimg.orig_w, refimg.orig_h = GenThumb( filename ) + #False == dont autorotate, its not needed on this image + refimg.thumbnail, refimg.orig_w, refimg.orig_h = GenThumb( filename, False ) settings = Settings.query.first() model=AIModel.query.get(settings.default_refimg_model) refimg.face, face_locn = GenFace( filename, model=model.name ) diff --git a/settings.py b/settings.py index 5233c0c..8cda88d 100644 --- a/settings.py +++ b/settings.py @@ -1,11 +1,11 @@ from wtforms import SubmitField, StringField, IntegerField, FloatField, HiddenField, validators, Form, SelectField, BooleanField from flask_wtf import FlaskForm from flask import request, render_template, redirect, url_for -from main import db, app, ma from sqlalchemy import Sequence from sqlalchemy.exc import SQLAlchemyError from status import st, Status from flask_login import login_required, current_user +from main import db, app, ma # pylint: disable=no-member diff --git a/shared.py b/shared.py index c45cef9..1e20c69 100644 --- a/shared.py +++ b/shared.py @@ -3,6 +3,7 @@ import os import face_recognition import io import base64 +import subprocess from PIL import Image, ImageOps class PA: @@ -114,13 +115,21 @@ def SymlinkName(ptype, path, file): # generates the thumbnail for an image - uses THUMBSIZE, and deals with non RGB images, and rotated images (based on exif) # returns data for thumbnail and original width and height, which gets stored in DB. Used when re-scaling viewed thumbs (refimgs on person page) -def GenThumb(fname): +def GenThumb(fname,auto_rotate): try: - im_orig = Image.open(fname) - if im_orig.format == 'JPEG': - im = ImageOps.exif_transpose(im_orig) + if auto_rotate: + # run cmdline util to re-orient jpeg (only changes if needed, and does it losslessly) + print( f"exifautotran {fname}") + p = subprocess.run(["/usr/bin/exifautotran",fname] ) + im=Image.open(fname) + # if we don't autorotate/touch the original, we still want the thumbnail oriented the right way else: - im = im_orig + im_orig = Image.open(fname) + if im_orig.format == 'JPEG': + im = ImageOps.exif_transpose(im_orig) + else: + im = im_orig + # if mode isn't RGB thumbnail fails, so force it if needed if im.mode != "RGB": im = im.convert('RGB') orig_w, orig_h = im.size