diff --git a/TODO b/TODO index 116e2c7..31c3637 100644 --- a/TODO +++ b/TODO @@ -1,9 +1,5 @@ ## GENERAL - * allow for and implement: default_refimg_model and default_scan_model - - cnn for refimgs should be defaul, for scan use hog - - research upsample val... - * viewer needs to allow toggle to scan_model (and prob. right-click on file... AI (with CNN) AI (with hog) - I think go to html5 toggles for: face, distance (only shows if you toggle face on), drop-down for model (allow change to cnn and reprocess) - show matching face distance in viewer diff --git a/pa_job_manager.py b/pa_job_manager.py index 211bdac..0088aa1 100644 --- a/pa_job_manager.py +++ b/pa_job_manager.py @@ -190,11 +190,12 @@ class Settings(Base): import_path = Column(String) storage_path = Column(String) recycle_bin_path = Column(String) - default_model = Column(Integer,ForeignKey('ai_model.id'), unique=True, nullable=False) + 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) def __repr__(self): - return f"" + return f"" class PersonRefimgLink(Base): __tablename__ = "person_refimg_link" @@ -225,6 +226,7 @@ class Refimg(Base): orig_w = Column(Integer) orig_h = Column(Integer) face_locn = Column(String) + model_used = Column(Integer, ForeignKey("ai_model.id") ) def __repr__(self): return f"" @@ -259,7 +261,6 @@ class FaceRefimgLink(Base): __tablename__ = "face_refimg_link" face_id = Column(Integer, ForeignKey("face.id"), primary_key=True ) refimg_id = Column(Integer, ForeignKey("refimg.id"), primary_key=True ) - model_used = Column(Integer, ForeignKey("ai_model.id") ) face_distance = Column(Integer) def __repr__(self): @@ -981,6 +982,10 @@ def JobRunAIOn(job): else: job.refimgs=session.query(Refimg).join(PersonRefimgLink).join(Person).filter(Person.tag==which_person).all() + if not job.refimgs: + FinishJob(job, "Failed Processesing AI - the person(s) you chose has no reference images!", "Failed" ) + return + # start by working out how many images in this selection we will need face match on job.num_files = 0 for jex in job.extra: @@ -1060,25 +1065,6 @@ def GenHashAndThumb(job, e): e.file_details.last_hash_date = time.time() return -def lookForPersonInImage(job, person, unknown_encoding, e): - FinishJob( job, "THIS CODE HAS BEEN REMOVED, need to use new Face* tables, and rethink", "Failed" ) - return - - -def generateUnknownEncodings(im): - unknown_image = numpy.array(im) - face_locations = face_recognition.face_locations(unknown_image) - if not face_locations: - return None - unknown_encodings = face_recognition.face_encodings(unknown_image, known_face_locations=face_locations) - return unknown_encodings - - -def compareAI(known_encoding, unknown_encoding): - results = face_recognition.compare_faces([known_encoding], unknown_encoding, tolerance=0.55) - return results - - def ProcessFilesInDir(job, e, file_func, count_dirs): if DEBUG==1: print( f"DEBUG: ProcessFilesInDir: {e.FullPathOnFS()}") @@ -1353,11 +1339,11 @@ def InitialValidationChecks(): FinishJob(job,"Finished Initial Validation Checks") return -def AddFaceToFile( locn_data, face_data, file_eid ): +def AddFaceToFile( locn_data, face_data, file_eid, model ): face = Face( face=face_data.tobytes(), locn=json.dumps(locn_data) ) session.add(face) session.commit() - ffl = FaceFileLink( face_id=face.id, file_eid=file_eid ) + ffl = FaceFileLink( face_id=face.id, file_eid=file_eid, model_used=model ) session.add(ffl) session.commit() return face @@ -1367,10 +1353,10 @@ def DelFacesForFile( eid ): session.commit() return -def MatchRefimgToFace( refimg_id, face_id, model, face_dist ): +def MatchRefimgToFace( refimg_id, face_id, face_dist ): # remove any match to this face from previous attempts, and 'replace' with new one session.query(FaceRefimgLink).filter(FaceRefimgLink.face_id==face_id).delete() - rfl = FaceRefimgLink( refimg_id = refimg_id, face_id = face_id, model_used=model, face_distance=face_dist ) + rfl = FaceRefimgLink( refimg_id = refimg_id, face_id = face_id, face_distance=face_dist ) session.add(rfl) session.commit() return @@ -1398,24 +1384,24 @@ def ScanFileForPerson( job, e, force=False ): DelFacesForFile( e.id ) file_h.faces_created_on = 0 + # get default_scan_model from settings (test this) + settings = session.query(Settings).first() + model=settings.default_scan_model + threshold = settings.default_threshold + # optimise: dont rescan if we already have faces if file_h.faces_created_on == 0: if DEBUG: AddLogForJob( job, f"DEBUG: {e.name} is missing unknown faces, generating them" ) im = face_recognition.load_image_file(e.FullPathOnFS()) - # TODO: use setting to use model - face_locations = face_recognition.face_locations(im) + model=AIModel.query.get(model) + face_locations = face_recognition.face_locations(im, model=model.name) unknown_encodings = face_recognition.face_encodings(im, known_face_locations=face_locations) for locn, face in zip( face_locations, unknown_encodings ): - AddFaceToFile( locn, face, e.id ) + AddFaceToFile( locn, face, e.id, model ) file_h.faces_created_on = time.time() session.commit() - # get default_model from settings (test this) - settings = session.query(Settings).first() - model=settings.default_model - threshold = settings.default_threshold - faces = session.query(Face).join(FaceFileLink).filter(FaceFileLink.file_eid==e.id).all() # if there are no faces for this file, then dont go any futher if not faces: @@ -1437,7 +1423,7 @@ def ScanFileForPerson( job, e, force=False ): for face in faces: who, fd = BestFaceMatch(dist, face.id, threshold ) if who != None: - MatchRefimgToFace( who, face.id, model, fd ) + MatchRefimgToFace( who, face.id, fd ) AddLogForJob(job, f'WE MATCHED: {name[who]} with file: {e.name} - face distance of {fd}') del( dist[who] ) return diff --git a/person.py b/person.py index b3c438c..908f2ab 100644 --- a/person.py +++ b/person.py @@ -2,6 +2,7 @@ from wtforms import SubmitField, StringField, HiddenField, validators, Form from flask_wtf import FlaskForm from flask import request, render_template, redirect, url_for from main import db, app, ma +from settings import Settings, AIModel from sqlalchemy import Sequence from sqlalchemy.exc import SQLAlchemyError from status import st, Status @@ -27,6 +28,7 @@ class Refimg(db.Model): face_locn = db.Column(db.String) thumbnail = db.Column(db.String, unique=True, nullable=False) created_on = db.Column(db.Float) + model_used = db.Column(db.Integer, db.ForeignKey("ai_model.id") ) person = db.relationship( 'Person', secondary="person_refimg_link", uselist=False, viewonly=True ) def __repr__(self): @@ -123,7 +125,7 @@ def person(id): # do the linkage tables by hand db.session.execute( f"delete from face_refimg_link frl where refimg_id in ( select refimg_id from person_refimg_link where person_id = {id} )" ) db.session.execute( f"delete from person_refimg_link where person_id = {id}" ) - person = Person.query.filter(Person.id==id).delete() + Person.query.filter(Person.id==id).delete() db.session.commit() return redirect( f'/persons' ) elif request.form and form.validate(): @@ -132,9 +134,11 @@ def person(id): if "ref-img-id-{}".format(ref_img.id) in request.form: new_refs.append(ref_img) if new_refs != person.refimg: - deld = list(set(person.refimg) - set(new_refs)) - st.SetMessage( f"Successfully Updated Person: removed reference image {deld[0].fname}" ) + deld = list(set(person.refimg) - set(new_refs)); + print(deld) person.refimg = new_refs + Refimg.query.filter(Refimg.id==deld[0].id).delete() + st.SetMessage( f"Successfully Updated Person: removed reference image {deld[0].fname}" ) else: st.SetMessage("Successfully Updated Person: (From: {}, {}, {})".format(person.tag, person.firstname, person.surname) ) person.tag = request.form['tag'] @@ -176,8 +180,11 @@ def add_refimg(): fname = f"/tmp/{fname}" f.save( fname ) refimg.thumbnail, refimg.orig_w, refimg.orig_h = GenThumb( fname ) - refimg.face, face_locn = GenFace( fname ) + settings = Settings.query.first() + model=AIModel.query.get(settings.default_refimg_model) + refimg.face, face_locn = GenFace( fname, model=model.name ) refimg.face_locn = json.dumps(face_locn) + refimg.model_used = settings.default_refimg_model os.remove(fname) person.refimg.append(refimg) db.session.add(person) diff --git a/settings.py b/settings.py index 3d71249..1a0590a 100644 --- a/settings.py +++ b/settings.py @@ -29,11 +29,12 @@ class Settings(db.Model): import_path = db.Column(db.String) storage_path = db.Column(db.String) recycle_bin_path = db.Column(db.String) - default_model = db.Column(db.Integer,db.ForeignKey('ai_model.id'), unique=True, nullable=False) + default_refimg_model = db.Column(db.Integer,db.ForeignKey('ai_model.id'), unique=True, nullable=False) + default_scan_model = db.Column(db.Integer,db.ForeignKey('ai_model.id'), unique=True, nullable=False) default_threshold = db.Column(db.Integer) def __repr__(self): - return f"" + return f"" ################################################################################ # Helper class that inherits a .dump() method to turn class Settings into json / useful in jinja2 @@ -54,7 +55,8 @@ class SettingsForm(FlaskForm): import_path = StringField('Path(s) to import from:', [validators.DataRequired()]) storage_path = StringField('Path to store sorted images to:', [validators.DataRequired()]) recycle_bin_path = StringField('Path to temporarily store deleted images in:', [validators.DataRequired()]) - default_model = SelectField( 'default_model', choices=[(c.id, c.name) for c in AIModel.query.order_by('id')] ) + default_refimg_model = SelectField( 'default_refimg_model', choices=[(c.id, c.name) for c in AIModel.query.order_by('id')] ) + default_scan_model = SelectField( 'default_scan_model', choices=[(c.id, c.name) for c in AIModel.query.order_by('id')] ) default_threshold = StringField('Face Distance threshold (below is a match):', [validators.DataRequired()]) submit = SubmitField('Save' ) @@ -78,7 +80,8 @@ def settings(): s.import_path = request.form['import_path'] s.storage_path = request.form['storage_path'] s.recycle_bin_path = request.form['recycle_bin_path'] - s.default_model = request.form['default_model'] + s.default_refimg_model = request.form['default_refimg_model'] + s.default_scan_model = request.form['default_scan_model'] s.default_threshold = request.form['default_threshold'] db.session.commit() return redirect( '/settings' ) diff --git a/shared.py b/shared.py index 6b639c4..5e558ec 100644 --- a/shared.py +++ b/shared.py @@ -96,9 +96,10 @@ def GenThumb(fname): print( f"GenThumb failed: {e}") return None, None, None -def GenFace(fname): +def GenFace(fname, model): img = face_recognition.load_image_file(fname) - location = face_recognition.face_locations(img) + # TODO: change face_locations call basedon model + location = face_recognition.face_locations(img, model=model) encodings = face_recognition.face_encodings(img, known_face_locations=location) if len(encodings): return encodings[0].tobytes(), location diff --git a/tables.sql b/tables.sql index e270955..8945121 100644 --- a/tables.sql +++ b/tables.sql @@ -1,15 +1,16 @@ alter database PA set timezone to 'Australia/Victoria'; -- these are hard-coded at present, not sure I can reflexively find models from API? -create table AI_MODEL ( ID integer, NAME varchar(24), constraint PK_AI_MODEL primary key(ID) ); -insert into AI_MODEL values ( 1, 'hog' ); -insert into AI_MODEL values ( 2, 'cnn' ); +create table AI_MODEL ( ID integer, NAME varchar(24), DESCRIPTION varchar(80), constraint PK_AI_MODEL primary key(ID) ); +insert into AI_MODEL values ( 1, 'hog', 'normal' ); +insert into AI_MODEL values ( 2, 'cnn', 'slower but more accurate' ); create table SETTINGS( ID integer, IMPORT_PATH varchar, STORAGE_PATH varchar, RECYCLE_BIN_PATH varchar, - DEFAULT_MODEL integer, DEFAULT_THRESHOLD float, + DEFAULT_REFIMG_MODEL integer, DEFAULT_SCAN_MODEL integer, DEFAULT_THRESHOLD float, constraint PK_SETTINGS_ID primary key(ID), - constraint FK_DEFAULT_MODEL foreign key (DEFAULT_MODEL) references AI_MODEL(ID) ); + constraint FK_DEFAULT_REFIMG_MODEL foreign key (DEFAULT_REFIMG_MODEL) references AI_MODEL(ID), + constraint FK_DEFAULT_SCAN_MODEL foreign key (DEFAULT_SCAN_MODEL) references AI_MODEL(ID) ); create table PA_USER( ID integer, dn varchar, constraint PK_PA_USER_ID primary key(ID) ); @@ -49,8 +50,9 @@ create table ENTRY_DIR_LINK ( entry_id integer, dir_eid integer, create table PERSON ( ID integer, TAG varchar(48), FIRSTNAME varchar(48), SURNAME varchar(48), constraint PK_PERSON_ID primary key(ID) ); -create table REFIMG ( ID integer, FNAME varchar(128), FACE bytea, ORIG_W integer, ORIG_H integer, FACE_LOCN varchar(32), CREATED_ON float, THUMBNAIL varchar, - constraint PK_REFIMG_ID primary key(ID) ); +create table REFIMG ( ID integer, FNAME varchar(128), FACE bytea, ORIG_W integer, ORIG_H integer, FACE_LOCN varchar(32), CREATED_ON float, THUMBNAIL varchar, MODEL_USED integer, + constraint PK_REFIMG_ID primary key(ID), + constraint FK_REFIMG_MODEL_USED foreign key (MODEL_USED) references AI_MODEL(ID) ); create table FACE( ID integer, FACE bytea, LOCN varchar(32), constraint PK_FACE_ID primary key(ID) ); @@ -60,11 +62,10 @@ create table FACE_FILE_LINK( FACE_ID integer, FILE_EID integer, MODEL_USED integ constraint FK_FFL_FILE_EID foreign key (FILE_EID) references FILE(EID), constraint FK_FFL_MODEL_USED foreign key (MODEL_USED) references AI_MODEL(ID) ); -create table FACE_REFIMG_LINK( FACE_ID integer, REFIMG_ID integer, MODEL_USED integer, FACE_DISTANCE integer, +create table FACE_REFIMG_LINK( FACE_ID integer, REFIMG_ID integer, FACE_DISTANCE integer, constraint PK_FRL_FACE_ID_REFIMG_ID primary key(FACE_ID, REFIMG_ID), constraint FK_FRL_FACE_ID foreign key (FACE_ID) references FACE(ID) on delete cascade, - constraint FK_FRL_REFIMG_ID foreign key (REFIMG_ID) references REFIMG(ID), - constraint FK_FRL_MODEL_USED foreign key (MODEL_USED) references AI_MODEL(ID) ); + constraint FK_FRL_REFIMG_ID foreign key (REFIMG_ID) references REFIMG(ID) ); create table PERSON_REFIMG_LINK ( PERSON_ID integer, REFIMG_ID integer, constraint PK_PRL primary key(PERSON_ID, REFIMG_ID), @@ -120,6 +121,6 @@ insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'mum', 'Mandy', ' insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'cam', 'Cameron', 'De Paoli' ); insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'mich', 'Michelle', 'De Paoli' ); -- DEV: -insert into SETTINGS ( id, import_path, storage_path, recycle_bin_path, default_model, default_threshold ) values ( (select nextval('SETTINGS_ID_SEQ')), '/home/ddp/src/photoassistant/images_to_process/#c:/Users/cam/Desktop/code/python/photoassistant/photos/#/home/ddp/src/photoassistant/new_img_dir/', '/home/ddp/src/photoassistant/storage/#c:/Users/cam/Desktop/code/python/photoassistant/storage/', '/home/ddp/src/photoassistant/.pa_bin/#c:/Users/cam/Desktop/code/python/photoassistant/.pa_bin/', 1, '0.55' ); +insert into SETTINGS ( id, import_path, storage_path, recycle_bin_path, default_refimg_model, default_scan_model, default_threshold ) values ( (select nextval('SETTINGS_ID_SEQ')), '/home/ddp/src/photoassistant/images_to_process/#c:/Users/cam/Desktop/code/python/photoassistant/photos/#/home/ddp/src/photoassistant/new_img_dir/', '/home/ddp/src/photoassistant/storage/#c:/Users/cam/Desktop/code/python/photoassistant/storage/', '/home/ddp/src/photoassistant/.pa_bin/#c:/Users/cam/Desktop/code/python/photoassistant/.pa_bin/', 2, 1, '0.55' ); -- PROD: ---insert into SETTINGS ( id, import_path, storage_path, recycle_bin_path, default_model, default_threshold ) values ( (select nextval('SETTINGS_ID_SEQ')), '/export/docker/storage/Camera_uploads/', '/export/docker/storage/photos/', '/export/docker/storage/.pa_bin/', 1, '0.55' ); +--insert into SETTINGS ( id, import_path, storage_path, recycle_bin_path, default_refimg_model, default_scan_model, default_threshold ) values ( (select nextval('SETTINGS_ID_SEQ')), '/export/docker/storage/Camera_uploads/', '/export/docker/storage/photos/', '/export/docker/storage/.pa_bin/', 2, 1, '0.55' ); diff --git a/templates/person.html b/templates/person.html index 443c6a9..b505aef 100644 --- a/templates/person.html +++ b/templates/person.html @@ -83,6 +83,9 @@ {% endfor %} +

@@ -100,7 +103,7 @@