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:
5
BUGs
5
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?
|
||||
|
||||
1
TODO
1
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?)
|
||||
|
||||
@@ -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
|
||||
|
||||
####################################################################################################################################
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
19
shared.py
19
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
|
||||
|
||||
Reference in New Issue
Block a user