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+='
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