From de81db9412d13d1ba7f6f5a8bd7252ca5f050363 Mon Sep 17 00:00:00 2001 From: Damien De Paoli Date: Tue, 18 Jan 2022 20:59:39 +1100 Subject: [PATCH] unmatched faces now sorts size of face desc, and is slightly prettier -- still slow and only takes you to the file anyway, might optimise that later. still no code to auto deal with unmatched face, but will add some capabilities next. Also, remembered last dir when file_ip/sp/bin. Also throw error if try to find unknown person - happened since I allowed the back/forward. --- TODO | 29 +++++++++++----------- ai.py | 3 ++- face.py | 9 ++++++- internal/js/face.js | 16 +----------- options.py | 16 +++++++++--- pa_job_manager.py | 6 ++++- person.py | 4 +++ tables.sql | 4 +-- templates/base.html | 4 +++ templates/faces.html | 58 +++++++++++++++++++------------------------- templates/prefs.html | 4 ++- 11 files changed, 80 insertions(+), 73 deletions(-) diff --git a/TODO b/TODO index 99555aa..936415a 100644 --- a/TODO +++ b/TODO @@ -1,19 +1,17 @@ ## GENERAL - *** Need to double-check scheduled jobs running in PROD (can use new pa_job_manager.log) - - * remember last import dir, so you can just go straight back to it - - * when hitting back button to a search, it doesnt handle the post, etc. - $(document).ready(function() { - window.onpopstate = function() { - # this seems to work, but feels like no protection at all??? - # (what about back when it goes onto a POST of deleting a file!) - window.history.back() - }; - }); - -- maybe window.history.replace() is needed on unsafe URLs? + * optimise run_ai_on (and for that matter getfiledetails, etc.)... + - e.g. with last scan*, STORE: new files? + - if last scan no new files, dont getfiledetails, don't re-run ai job * per file you could select an unknown face and add it as a ref img to an existing person, or make a new person and attach? + * [DONE] order/ find face with largest size and at least show that as unmatched + - could also try to check it vs. other faces, if it matches more than say 10? we offer it up as a required ref img, then cut that face (with margin) out and use it is a new ref image / person + * on viewer: allow face to be used to create person, add to existing person, and allow 'ignore', mark as 'not a face', etc. -> all into DB + - so need face 'treatment' -> could be matched via face_refimg_link, but also could be 'ignore' or 'not a face', in each case we could exclude those faces from + matching for the future, and reporting on matches, etc. + - context-menu with rects on a canvas + https://stackoverflow.com/questions/31601393/create-context-menu-using-jquery-with-html-5-canvas + - also allow joblog search from the viewer for that file... * delete folder @@ -46,14 +44,15 @@ * comment your code -> only html files remaining - * from menu, we could try to get smart/fancy... say find face with largest size, check it vs. other faces, if it matches more than say 10? we offer it up as a required ref img, then cut that face (with margin) out and use it is a new ref image / person - - read that guys face matching / clustering / nearest neighbour examples, for a whole new AI capability + * read that guys face matching / clustering / nearest neighbour examples, for a whole new AI capability https://www.pyimagesearch.com/2018/07/09/face-clustering-with-python/ * fix up logging in general * support animated gifs in html5 canvas + * think about security - in job_mgr anywhere I can os.replace/remove NEED to protect, etc + ## DB * Dir can have date in the DB, so we can do Oldest/Newest dirs in Folder view diff --git a/ai.py b/ai.py index 66abcb0..363dd5b 100644 --- a/ai.py +++ b/ai.py @@ -91,7 +91,7 @@ def run_ai_on_storage(): @app.route("/unmatched_faces") @login_required def unmatched_faces(): - faces=Face.query.join(FaceFileLink).join(FaceRefimgLink, isouter=True).filter(FaceRefimgLink.refimg_id==None).limit(10).all() + faces=Face.query.join(FaceFileLink).join(FaceRefimgLink, isouter=True).filter(FaceRefimgLink.refimg_id==None).order_by(Face.h.desc()).limit(10).all() imgs={} for face in faces: face.locn=json.loads("["+face.locn+"]") @@ -102,6 +102,7 @@ def unmatched_faces(): y=face.locn[0][0]*0.95 x2=face.locn[0][1]*1.05 y2=face.locn[0][2]*1.05 + im = Image.open(f.FullPathOnFS()) region = im.crop((x, y, x2, y2)) img_bytearray = io.BytesIO() diff --git a/face.py b/face.py index d84ad3e..2fb73cf 100644 --- a/face.py +++ b/face.py @@ -2,6 +2,12 @@ from main import db, app, ma from sqlalchemy import Sequence from sqlalchemy.exc import SQLAlchemyError +# DEL ME SOON +from flask_login import login_required +from flask import render_template +import json + + # pylint: disable=no-member ################################################################################ @@ -17,6 +23,8 @@ class Face(db.Model): id = db.Column(db.Integer, db.Sequence('face_id_seq'), primary_key=True ) face = db.Column( db.LargeBinary ) locn = db.Column( db.String ) + w = db.Column( db.Integer ) + h = db.Column( db.Integer ) refimg_lnk = db.relationship("FaceRefimgLink", uselist=False, viewonly=True) facefile_lnk = db.relationship("FaceFileLink", uselist=False, viewonly=True) refimg =db.relationship("Refimg", secondary="face_refimg_link", uselist=False) @@ -53,4 +61,3 @@ class FaceRefimgLink(db.Model): def __repr__(self): return f"" + return f"" ################################################################################ @@ -85,14 +87,16 @@ class Options(PA): self.how_many=pref.how_many self.offset=pref.st_offset self.size=pref.size + self.root=pref.root + self.cwd=pref.cwd else: self.grouping="None" self.how_many="50" self.offset="0" self.size="128" + self.root='static/' + self.path_type + self.cwd=self.root - self.cwd='static/' + self.path_type - self.root=self.cwd # the above are defaults, if we are here, then we have current values, use them instead if they are set -- AI: searches dont set them so then we use those in the DB first if request.method=="POST": @@ -134,7 +138,7 @@ class Options(PA): pref=PA_PREF.query.filter(PA_PREF.pa_user_dn==current_user.dn,PA_PREF.path_type==self.path_type).first() if not pref: pref=PA_PREF( pa_user_dn=current_user.dn, path_type=self.path_type, noo=self.noo, grouping=self.grouping, how_many=self.how_many, - st_offset=self.offset, size=self.size, folders=self.folders) + st_offset=self.offset, size=self.size, folders=self.folders, root=self.root, cwd=self.cwd) else: pref.noo=self.noo pref.grouping=self.grouping @@ -142,10 +146,14 @@ class Options(PA): pref.st_offset=self.offset pref.size=self.size pref.folders=self.folders + pref.root = self.root + pref.cwd = self.cwd db.session.add(pref) db.session.commit() + return + ################################################################################ # /prefs -> GET only -> prints out list of all prefs (simple for now) ################################################################################ diff --git a/pa_job_manager.py b/pa_job_manager.py index 40d19e0..0b0d726 100644 --- a/pa_job_manager.py +++ b/pa_job_manager.py @@ -320,6 +320,8 @@ class Face(Base): id = Column(Integer, Sequence('face_id_seq'), primary_key=True ) face = Column( LargeBinary ) locn = Column(String) + w = Column(Integer) + h = Column(Integer) def __repr__(self): return f" + // do our own back button handling, TODO: 'dangerous' URLs, will be replaced with GETs first via history.replaceState() + window.onpopstate = function(e) { + window.history.back() + } function SetViewingOptionsForSearchForm() { if( $('#noo').length ) diff --git a/templates/faces.html b/templates/faces.html index 9e73ab9..bbcde68 100644 --- a/templates/faces.html +++ b/templates/faces.html @@ -6,42 +6,34 @@

Unmatched Faces

{% for f in faces %} -
+
- - - - - - - - - - ' + +
+
+ + -
{{f.id}} -
-
- + // when the document is ready, then DrawRefimg + $(function() { DrawRefimg( fig_{{f.id}}, im_{{f.id}}, c_{{f.id}}, orig_face_{{f.id}} ) }); + +
Face #{{f.id}} +
+ +
{% endfor %} diff --git a/templates/prefs.html b/templates/prefs.html index 63e5eb5..1539ab5 100644 --- a/templates/prefs.html +++ b/templates/prefs.html @@ -5,7 +5,7 @@ - + {% for pref in prefs %} @@ -18,6 +18,8 @@ + + {% endfor %}
PathNew or OldestHow ManyFolders?Group byThumb sizeFullscreenDB retrieve offset
PathNew or OldestHow ManyFolders?Group byThumb sizeFullscreenDB retrieve offsetRootcwd
{{pref.size}} {{pref.fullscreen}} {{pref.st_offset}}{{pref.root}}{{pref.cwd}}