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

This commit is contained in:
2022-07-15 17:18:31 +10:00
parent 6d1801dce9
commit 84a4cf7cf8
6 changed files with 57 additions and 14 deletions

5
BUGs
View File

@@ -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?

1
TODO
View File

@@ -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?)

View File

@@ -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"<id: {self.id}, import_path: {self.import_path}, storage_path: {self.storage_path}, recycle_bin_path: {self.recycle_bin_path}, default_refimg_model: {self.default_refimg_model}, default_scan_model: {self.default_scan_model}, default_threshold: {self.default_threshold}, face_size_limit: {self.face_size_limit}, scheduled_import_scan:{self.scheduled_import_scan}, scheduled_storage_scan: {self.scheduled_storage_scan}, scheduled_bin_cleanup: {self.scheduled_bin_cleanup}, bin_cleanup_file_age: {self.bin_cleanup_file_age}>"
return f"<id: {self.id}, import_path: {self.import_path}, storage_path: {self.storage_path}, recycle_bin_path: {self.recycle_bin_path}, auto_rotate: {self.auto_rotate}, default_refimg_model: {self.default_refimg_model}, default_scan_model: {self.default_scan_model}, default_threshold: {self.default_threshold}, face_size_limit: {self.face_size_limit}, scheduled_import_scan:{self.scheduled_import_scan}, scheduled_storage_scan: {self.scheduled_storage_scan}, scheduled_bin_cleanup: {self.scheduled_bin_cleanup}, bin_cleanup_file_age: {self.bin_cleanup_file_age}>"
################################################################################
# Class describing Person to Refimg link in DB via sqlalchemy
@@ -422,6 +423,37 @@ class PA_JobManager_FE_Message(Base):
def __repr__(self):
return "<id: {}, job_id: {}, alert: {}, message: {}".format(self.id, self.job_id, self.alert, self.message)
class PA_UserState(Base):
__tablename__ = "pa_user_state"
id = Column(Integer, Sequence('pa_user_state_id_seq'), primary_key=True )
pa_user_dn = Column(String, ForeignKey('pa_user.dn'), primary_key=True )
last_used = Column(DateTime(timezone=True))
path_type = Column(String, primary_key=True, unique=False, nullable=False )
noo = Column(String, unique=False, nullable=False )
grouping = Column(String, unique=False, nullable=False )
how_many = Column(Integer, unique=False, nullable=False )
st_offset = Column(Integer, unique=False, nullable=False )
size = Column(Integer, unique=False, nullable=False )
folders = Column(Boolean, unique=False, nullable=False )
root = Column(String, unique=False, nullable=False )
cwd = Column(String, unique=False, nullable=False )
## for now being lazy and not doing a separate table until I settle on needed fields and when
# only used if ptype == View
view_eid = Column(Integer, unique=False, nullable=False )
orig_ptype = Column(String, unique=False, nullable=False )
# only used if view and orig_ptype was search
orig_search_term = Column(String, unique=False, nullable=False )
orig_url = Column(String, unique=False, nullable=False )
current = Column(Integer)
first_eid = Column(Integer)
last_eid = Column(Integer)
num_entries = Column(Integer)
def __repr__(self):
return f"<pa_user_dn: {self.pa_user_dn}, path_type: {self.path_type}, noo: {self.noo}, grouping: {self.grouping}, how_many: {self.how_many}, st_offset: {self.st_offset}, size: {self.size}, folders: {self.folders}, root: {self.root}, cwd: {self.cwd}, view_eid: {self.view_eid}, orig_ptype: {self.orig_ptype}, orig_search_term: {self.orig_search_term}, orig_url: {self.orig_url}, current={self.current}, first_eid={self.first_eid}, last_eid={self.last_eid}, num_entries={self.num_entries}>"
##############################################################################
# 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
####################################################################################################################################

View File

@@ -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 )

View File

@@ -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

View File

@@ -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:
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_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