From a53d4896b03602dd4048d196183ad16ed3fd46ec Mon Sep 17 00:00:00 2001 From: Damien De Paoli Date: Sat, 11 Jun 2022 22:41:31 +1000 Subject: [PATCH] now have functional add/remove manual override to existing person --- face.py | 52 ++++++++------ files.py | 16 +++-- internal/js/view_support.js | 137 +++++++++++++++++++++++------------- person.py | 37 ++++++++-- tables.sql | 6 +- templates/viewer.html | 11 +++ 6 files changed, 175 insertions(+), 84 deletions(-) diff --git a/face.py b/face.py index ef1ab95..6ec5f5f 100644 --- a/face.py +++ b/face.py @@ -1,6 +1,7 @@ from main import db, app, ma from sqlalchemy import Sequence from sqlalchemy.exc import SQLAlchemyError +from shared import PA # DEL ME SOON @@ -64,24 +65,33 @@ class FaceRefimgLink(db.Model): return f"" + +class FaceNoMatchOverride(db.Model): + __tablename__ = "face_no_match_override" + id = db.Column(db.Integer, db.Sequence('face_override_id_seq'), primary_key=True ) + face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True ) + type_id = db.Column(db.Integer, db.ForeignKey("face_override_type.id")) + type = db.relationship("FaceOverrideType") + face = db.Column( db.LargeBinary ) + + def __repr__(self): + return f"" + + +class FaceManualOverride(db.Model): + __tablename__ = "face_manual_override" + id = db.Column(db.Integer, db.Sequence('face_override_id_seq'), primary_key=True ) + face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True ) + face = db.Column( db.LargeBinary ) + person_id = db.Column(db.Integer, db.ForeignKey("person.id"), primary_key=True ) + person = db.relationship("Person") + + def __repr__(self): + return f"" diff --git a/files.py b/files.py index 88458d0..e5d2a69 100644 --- a/files.py +++ b/files.py @@ -32,7 +32,7 @@ from person import Refimg, Person, PersonRefimgLink from settings import Settings, SettingsIPath, SettingsSPath, SettingsRBPath from shared import SymlinkName from dups import Duplicates -from face import Face, FaceFileLink, FaceRefimgLink +from face import Face, FaceFileLink, FaceRefimgLink, FaceNoMatchOverride, FaceManualOverride # pylint: disable=no-member @@ -666,7 +666,7 @@ def viewlist(): resp['objs'][e.id]['name'] = e.name resp['objs'][e.id]['type'] = e.type.name if e.file_details.faces: - # model is used for whole file, so set it at that level (based on first face) + # model is used for whole file, so set it at that level (based on first face) resp['objs'][e.id]['face_model'] = e.file_details.faces[0].facefile_lnk.model_used resp['objs'][e.id]['faces'] = [] @@ -727,11 +727,17 @@ def view(id): if face.locn: face.tmp_locn = json.loads(face.locn) else: - # this at least stops a 500 server error - seems to occur when - # DB contains disconnected faces with no locn data - BUG: 87 + # this at least stops a 500 server error - seems to occur when + # DB contains disconnected faces with no locn data - BUG: 87 face.tmp_locn = [ 0,0,0,0 ] st.SetMessage( f"For some reason this face does not have a locn: face_id={face.id} tell ddp", "warning" ) - + # now get any relevant override and store it in objs... + fnmo = FaceNoMatchOverride.query.filter(FaceNoMatchOverride.face_id==face.id).first() + if fnmo: + face.no_match_override=fnmo + mo = FaceManualOverride.query.filter(FaceManualOverride.face_id==face.id).first() + if mo: + face.manual_override=mo eids=eids.rstrip(",") diff --git a/internal/js/view_support.js b/internal/js/view_support.js index 4d1fb19..de9a663 100644 --- a/internal/js/view_support.js +++ b/internal/js/view_support.js @@ -26,6 +26,32 @@ function NewHeight() return im.height*gap / (im.width/window.innerWidth) } +function DrawLabelOnFace(str) +{ + // finish face box, need to clear out new settings for // transparent backed-name tag + context.stroke(); + context.beginPath() + context.lineWidth = 0.1 + context.font = "30px Arial" + context.globalAlpha = 0.6 + + bbox = context.measureText(str); + f_h=bbox.fontBoundingBoxAscent + if( bbox.fontBoundingBoxDescent ) + f_h += bbox.fontBoundingBoxDescent + f_h -= 8 + context.rect( x+w/2-bbox.width/2, y-f_h, bbox.width, f_h ) + context.fillStyle="white" + context.fill() + context.stroke(); + context.beginPath() + context.globalAlpha = 1.0 + context.font = "30px Arial" + context.textAlign = "center" + context.fillStyle = context.strokeStyle + context.fillText(str, x+w/2, y-2) +} + // This draws the image, it can be called on resize events, img.src finishing // loading or explicitly on page load. Will also deal with all state/toggles // for items like name, grayscale, etc. @@ -87,46 +113,23 @@ function DrawImg() context.beginPath() context.rect( x, y, w, h ) context.lineWidth = 2 - context.strokeStyle = 'green' + + // this face has an override so diff colour + if( objs[current].faces[i].override ) + context.strokeStyle = 'blue' + else + context.strokeStyle = 'green' + + if( objs[current].faces[i].no_match_override) + DrawLabelOnFace( objs[current].faces[i].no_match_override.type ) + if( objs[current].faces[i].who ) - { - // finish face box, need to clear out new settings for - // transparent backed-name tag - context.stroke(); - context.beginPath() - context.lineWidth = 0.1 - context.font = "30px Arial" - context.globalAlpha = 0.6 - str=objs[current].faces[i].who + { + str=objs[current].faces[i].who if( $('#distance').prop('checked') ) str += "("+objs[current].faces[i].distance+")" - - bbox = context.measureText(str); - f_h=bbox.fontBoundingBoxAscent - if( bbox.fontBoundingBoxDescent ) - f_h += bbox.fontBoundingBoxDescent - f_h -= 8 - context.rect( x+w/2-bbox.width/2, y-f_h, bbox.width, f_h ) - context.fillStyle="white" - context.fill() - context.stroke(); - context.beginPath() - context.globalAlpha = 1.0 - context.font = "30px Arial" - context.textAlign = "center" - context.fillStyle = "green" - context.fillText(str, x+w/2, y-2) - } - /* can use to show lower left coords of a face for debugging - else - { - context.font = "14px Arial" - context.textAlign = "center" - context.fillStyle = "black" - context.fillText( 'x=' + objs[current].faces[i].x + ', y=' + objs[current].faces[i].y, x+w/2, y-2) - context.fillText( 'x=' + objs[current].faces[i].x + ', y=' + objs[current].faces[i].y, x+w/2, y-2) - } - */ + DrawLabelOnFace( str ) + } context.stroke(); } } @@ -225,7 +228,11 @@ $(document).ready( function() if( x >= fx && x <= fx+fw && y >= fy && y <= fy+fh ) { - if( objs[current].faces[i].who ) + if( objs[current].faces[i].override ) + { + item_list['remove_override']={ 'name': 'Remove override for this face', 'which_face': i, 'id': objs[current].faces[i].id } + } + else if( objs[current].faces[i].who ) { item_list['match']={ 'name': objs[current].faces[i].who, 'which_face': i, 'id': objs[current].faces[i].id } item_list['wrong_person']={ 'name': 'wrong person', 'which_face': i, 'id': objs[current].faces[i].id } @@ -238,7 +245,6 @@ $(document).ready( function() item_list['no_match_no_face']={ 'name': 'Mark as not a face', 'which_face': i, 'id': objs[current].faces[i].id } item_list['no_match_too_young']={ 'name': 'Mark as face too young', 'which_face': i, 'id': objs[current].faces[i].id } item_list['no_match_ignore']={ 'name': 'Ignore this face', 'which_face': i, 'id': objs[current].faces[i].id } - item_list['remove_override']={ 'name': 'Remove override for this face', 'which_face': i, 'id': objs[current].faces[i].id } } delete item_list['not_a_face'] $('#canvas').prop('menu_item', item_list ) @@ -258,14 +264,19 @@ $(document).ready( function() } ); // quick wrapper function to make calling this ajax code simpler in SearchForPerson -function OverrideForceMatch( person, face ) +function OverrideForceMatch( person_id, face_id, face_pos ) { - ofm='&person_id='+person+'&face_id='+face + ofm='&person_id='+person_id+'&face_id='+face_id // on success, close the dbox, force face drawing on and redraw the face with the new override $.ajax({ type: 'POST', data: ofm, url: '/override_force_match', success: function(data) { - $('#dbox').modal('hide'); - $('#faces').prop('checked',true); - DrawImg(); + if( objs[current].faces[face_pos].who ) + objs[current].faces[face_pos].old_who=objs[current].faces[face_pos].who + objs[current].faces[face_pos].who=data.person_tag + objs[current].faces[face_pos].override=1 + + $('#dbox').modal('hide') + $('#faces').prop('checked',true) + DrawImg() } } ) } @@ -273,7 +284,7 @@ function OverrideForceMatch( person, face ) // function to facilitate adding a face match override to this "found" person // uses Ajax to the f/e to get any person matching #stext's content (via any name/tag) // and displays results in #search_person_results -function SearchForPerson(face_id) +function SearchForPerson(face_id, face_pos) { // make URI safe who = encodeURIComponent( $('#stext').val() ) @@ -283,7 +294,8 @@ function SearchForPerson(face_id) content='Click one of the link(s) below to manually connect this face as once-off connection to the person:

' for( var key in data ) { var person = data[key]; - content+= ''+person.firstname+' '+person.surname+'('+person.tag+')
' + content+= ''+person.firstname+' '+person.surname+' ('+person.tag+')
' } $('#search_person_results').html( content ) } @@ -291,14 +303,34 @@ function SearchForPerson(face_id) return false } +function RemoveOverride() +{ + d='&face_id='+objs[current].faces[face_pos].id+'&person_tag='+objs[current].faces[face_pos].who+ + '&file_eid='+current + $.ajax({ type: 'POST', data: d, url: '/remove_override', + success: function(data) { + if( objs[current].faces[face_pos].old_who ) + objs[current].faces[face_pos].who=objs[current].faces[face_pos].old_who + else + delete objs[current].faces[face_pos].who + delete objs[current].faces[face_pos].override + $('#dbox').modal('hide') + DrawImg() + return false + } + } ) + return false +} + // function that is called when we click on a face in the viewer and we want to // potentially override the non-match / match... it shows the face, and then // based on which menu item got us here, shows appropriate text to do next action function FaceDBox(key, item) { + face_pos=item[key]['which_face'] div ='

' - div+='Face position #' + item[key]['which_face'] + div+='Face position #' + face_pos div+='

' $.ajax({ type: 'POST', data: null, url: '/get_face_from_image/'+item[key]['id'], success: function(img_data) { @@ -306,6 +338,15 @@ function FaceDBox(key, item) } } ) div+='

' + if ( key == 'remove_override' ) + { + div+='
remove this override (force match to: ' + objs[current].faces[face_pos].who + ')' + div+='
' + div+='' + div+='' + div+='
' + } if ( key == 'no_match_new_person' ) { div+='
create new person' @@ -317,7 +358,7 @@ function FaceDBox(key, item) `
diff --git a/person.py b/person.py index adf324b..5b602ec 100644 --- a/person.py +++ b/person.py @@ -9,7 +9,7 @@ from status import st, Status from flask_login import login_required, current_user from werkzeug.utils import secure_filename from shared import GenFace, GenThumb -from face import Face, FaceRefimgLink +from face import Face, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceManualOverride import os import json import time @@ -243,7 +243,6 @@ def find_persons(who): resp[p.id]['firstname']=p.firstname resp[p.id]['surname']=p.surname - print( resp ) return resp ################################################################################ @@ -253,15 +252,39 @@ def find_persons(who): @login_required def override_force_match(): person_id = request.form['person_id'] - face_id = request.form['face_id'] p = Person.query.get(person_id); if not p: raise Exception("could not find person to add override too!") + + face_id = request.form['face_id'] f = Face.query.get(face_id); if not f: raise Exception("could not find face to add override for!") - print( f"Being asked to force an override match for face_id {face_id}, for person: {p.tag} -- doing nothing for now" ) - st.SetMessage( f"Being asked to force an override match for face_id {face_id}, for person: {p.tag} -- doing nothing for now" ) - # might need to do something smarter here (reload to old view is good though & happens now, not sure why (last url)?) - return "ok" + mo = FaceManualOverride( face_id=f.id, face=f.face, person_id=p.id ) + db.session.add( mo ) + db.session.commit() + + print( f"Placing an override match with face_id {face_id}, for person: {p.tag}" ) + # this will reply to the Ajax / POST, and cause the page to re-draw with new face override to person_tag + resp={} + resp['person_tag']=p.tag + return resp + +################################################################################ +# /remove_override -> POST +################################################################################ +@app.route("/remove_override", methods=["POST"]) +@login_required +def remove_override(): + face_id = request.form['face_id'] + person_tag = request.form['person_tag'] + file_eid = request.form['file_eid'] + print( f"Remove override with face_id={face_id} to person_tag={person_tag}" ) + + FaceManualOverride.query.filter( FaceManualOverride.face_id==face_id ).delete() + db.session.commit() + + # this will reply to the Ajax / POST, and cause the page to re-draw with new face override + resp={} + return resp diff --git a/tables.sql b/tables.sql index 10b1a1f..10029c3 100644 --- a/tables.sql +++ b/tables.sql @@ -103,13 +103,13 @@ insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_S -- keep non-redundant FACE because, when we rebuild data we may have a null FACE_ID, but still want to connect to this override -- from a previous AI pass... (would happen if we delete a file and then reimport/scan it), OR, more likely we change (say) a threshold, etc. -- any reordering of faces, generates new face_ids... (but if the face data was the same, then this override should stand) -create table FACE_NO_MATCH_OVERRIDE ( ID integer, FACE_ID integer, TYPE integer, FACE bytea, +create table FACE_NO_MATCH_OVERRIDE ( ID integer, FACE_ID integer, TYPE_ID integer, FACE bytea, constraint FK_FNMO_FACE_ID foreign key (FACE_ID) references FACE(ID), - constraint FK_FNMO_TYPE foreign key (TYPE) references FACE_OVERRIDE_TYPE(ID), + constraint FK_FNMO_TYPE foreign key (TYPE_ID) references FACE_OVERRIDE_TYPE(ID), constraint PK_FNMO_ID primary key(ID) ); -- manual match goes to person not refimg, so on search, etc. we deal with this anomaly (via sql not ORM) -create table FACE_MANUAL_OVERRIDE ( ID integer, FACE_ID integer, PERSON_ID integer, TYPE integer, constraint PK_FACE_MANUAL_OVERRIDE_ID primary key(ID) ); +create table FACE_MANUAL_OVERRIDE ( ID integer, FACE_ID integer, PERSON_ID integer, FACE bytea, constraint PK_FACE_MANUAL_OVERRIDE_ID primary key(ID) ); create table PERSON_REFIMG_LINK ( PERSON_ID integer, REFIMG_ID integer, constraint PK_PRL primary key(PERSON_ID, REFIMG_ID), diff --git a/templates/viewer.html b/templates/viewer.html index 414f98e..cd5a9e0 100644 --- a/templates/viewer.html +++ b/templates/viewer.html @@ -48,6 +48,17 @@ data['who']='{{face.refimg.person.tag}}' data['distance']="{{face.refimg_lnk.face_distance|round(2)}}" {% endif %} + {% if face.no_match_override %} + data['override']=1 + data['no_match_override'] = { + 'face_id' : '{{face.no_match_override.face_id}}', + 'type' : '{{face.no_match_override.type.name}}', + } + {% endif %} + {% if face.manual_override %} + data['override']=1 + data['who']='{{face.manual_override.person.tag}}' + {% endif %} e.faces.push( data ) {% endfor %} objs[{{id}}]=e