now have functional add/remove manual override to existing person

This commit is contained in:
2022-06-11 22:41:31 +10:00
parent 8c78d9e633
commit a53d4896b0
6 changed files with 175 additions and 84 deletions

52
face.py
View File

@@ -1,6 +1,7 @@
from main import db, app, ma from main import db, app, ma
from sqlalchemy import Sequence from sqlalchemy import Sequence
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from shared import PA
# DEL ME SOON # DEL ME SOON
@@ -64,24 +65,33 @@ class FaceRefimgLink(db.Model):
return f"<face_id: {self.face_id}, refimg_id={self.refimg_id}, face_distance: {self.face_distance}" return f"<face_id: {self.face_id}, refimg_id={self.refimg_id}, face_distance: {self.face_distance}"
### DDP: todo next, make these into sqlachemy classes, THEN person.py add the override in ORM, THEN draw override in blue/not green in DrawImg class FaceOverrideType(db.Model):
### THEN find_person needs to call appropriate override func (OR pass in type?) and get it to be smarter - that sounds okay actually __tablename__ = "face_override_type"
# create table FACE_OVERRIDE_TYPE ( ID integer, NAME varchar unique, constraint PK_FACE_OVERRIDE_TYPE_ID primary key(ID) ); id = db.Column(db.Integer, db.Sequence('face_override_type_id_seq'), primary_key=True )
#create sequence FACE_OVERRIDE_TYPE_ID_SEQ; name = db.Column( db.String )
#create sequence FACE_OVERRIDE_ID_SEQ;
#create table FACE_OVERRIDE_TYPE ( ID integer, NAME varchar unique, constraint PK_FACE_OVERRIDE_TYPE_ID primary key(ID) ); def __repr__(self):
#insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Not a face' ); return f"<id: {self.id}, name={self.name}>"
#insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Too young' );
#insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Ignore face' ); class FaceNoMatchOverride(db.Model):
#insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Manual match' ); __tablename__ = "face_no_match_override"
# id = db.Column(db.Integer, db.Sequence('face_override_id_seq'), primary_key=True )
#-- keep non-redundant FACE because, when we rebuild data we may have a null FACE_ID, but still want to connect to this override face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True )
#-- 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. type_id = db.Column(db.Integer, db.ForeignKey("face_override_type.id"))
#-- any reordering of faces, generates new face_ids... (but if the face data was the same, then this override should stand) type = db.relationship("FaceOverrideType")
#create table FACE_NO_MATCH_OVERRIDE ( ID integer, FACE_ID integer, TYPE integer, FACE bytea, face = db.Column( db.LargeBinary )
# constraint FK_FNMO_FACE_ID foreign key (FACE_ID) references FACE(ID),
# constraint FK_FNMO_TYPE foreign key (TYPE) references FACE_OVERRIDE_TYPE(ID), def __repr__(self):
# constraint PK_FNMO_ID primary key(ID) ); return f"<id: {self.id}, face_id={self.face_id}, type: {self.type}>"
#
#-- 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) ); 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"<id: {self.id}, face_id={self.face_id}, person_id={self.person_id}>"

View File

@@ -32,7 +32,7 @@ from person import Refimg, Person, PersonRefimgLink
from settings import Settings, SettingsIPath, SettingsSPath, SettingsRBPath from settings import Settings, SettingsIPath, SettingsSPath, SettingsRBPath
from shared import SymlinkName from shared import SymlinkName
from dups import Duplicates from dups import Duplicates
from face import Face, FaceFileLink, FaceRefimgLink from face import Face, FaceFileLink, FaceRefimgLink, FaceNoMatchOverride, FaceManualOverride
# pylint: disable=no-member # pylint: disable=no-member
@@ -731,7 +731,13 @@ def view(id):
# DB contains disconnected faces with no locn data - BUG: 87 # DB contains disconnected faces with no locn data - BUG: 87
face.tmp_locn = [ 0,0,0,0 ] 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" ) 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(",") eids=eids.rstrip(",")

View File

@@ -26,6 +26,32 @@ function NewHeight()
return im.height*gap / (im.width/window.innerWidth) 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 // 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 // loading or explicitly on page load. Will also deal with all state/toggles
// for items like name, grayscale, etc. // for items like name, grayscale, etc.
@@ -87,46 +113,23 @@ function DrawImg()
context.beginPath() context.beginPath()
context.rect( x, y, w, h ) context.rect( x, y, w, h )
context.lineWidth = 2 context.lineWidth = 2
// this face has an override so diff colour
if( objs[current].faces[i].override )
context.strokeStyle = 'blue'
else
context.strokeStyle = 'green' 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 ) 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') ) if( $('#distance').prop('checked') )
str += "("+objs[current].faces[i].distance+")" str += "("+objs[current].faces[i].distance+")"
DrawLabelOnFace( str )
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)
}
*/
context.stroke(); context.stroke();
} }
} }
@@ -225,7 +228,11 @@ $(document).ready( function()
if( x >= fx && x <= fx+fw && y >= fy && y <= fy+fh ) 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['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 } 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_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_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['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'] delete item_list['not_a_face']
$('#canvas').prop('menu_item', item_list ) $('#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 // 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 // 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) { $.ajax({ type: 'POST', data: ofm, url: '/override_force_match', success: function(data) {
$('#dbox').modal('hide'); if( objs[current].faces[face_pos].who )
$('#faces').prop('checked',true); objs[current].faces[face_pos].old_who=objs[current].faces[face_pos].who
DrawImg(); 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 // 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) // 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 // and displays results in #search_person_results
function SearchForPerson(face_id) function SearchForPerson(face_id, face_pos)
{ {
// make URI safe // make URI safe
who = encodeURIComponent( $('#stext').val() ) 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:<br><br>' content='Click one of the link(s) below to manually connect this face as once-off connection to the person:<br><br>'
for( var key in data ) { for( var key in data ) {
var person = data[key]; var person = data[key];
content+= '<a onClick="OverrideForceMatch('+person.id+','+face_id+')">'+person.firstname+' '+person.surname+'('+person.tag+')</a><br>' content+= '<a class="link-primary" onClick="OverrideForceMatch('+person.id+','
+face_id+','+face_pos+')">'+person.firstname+' '+person.surname+' ('+person.tag+')</a><br>'
} }
$('#search_person_results').html( content ) $('#search_person_results').html( content )
} }
@@ -291,14 +303,34 @@ function SearchForPerson(face_id)
return false 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 // 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 // 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 // based on which menu item got us here, shows appropriate text to do next action
function FaceDBox(key, item) function FaceDBox(key, item)
{ {
face_pos=item[key]['which_face']
div ='<p>' div ='<p>'
div+='Face position #' + item[key]['which_face'] div+='Face position #' + face_pos
div+='<div id="face_img"></div>' div+='<div id="face_img"></div>'
$.ajax({ type: 'POST', data: null, url: '/get_face_from_image/'+item[key]['id'], $.ajax({ type: 'POST', data: null, url: '/get_face_from_image/'+item[key]['id'],
success: function(img_data) { success: function(img_data) {
@@ -306,6 +338,15 @@ function FaceDBox(key, item)
} }
} ) } )
div+='</p>' div+='</p>'
if ( key == 'remove_override' )
{
div+='<div class="row col-12">remove this override (force match to: ' + objs[current].faces[face_pos].who + ')'
div+='<div class="row">'
div+='<button class="btn btn-outline-info col-6" type="button" onClick="$(\'#dbox\').modal(\'hide\'); return false">Cancel</button>'
div+='<button class="btn btn-outline-danger col-6" type="button" '+
'onClick="RemoveOverride(' +face_pos+ ')">Remove</button>'
div+='</div>'
}
if ( key == 'no_match_new_person' ) if ( key == 'no_match_new_person' )
{ {
div+='<br>create new person' div+='<br>create new person'
@@ -317,7 +358,7 @@ function FaceDBox(key, item)
` `
<div class="input-group mb-3"><input type="text" class="form-control" id="stext" placeholder="tag/name"> <div class="input-group mb-3"><input type="text" class="form-control" id="stext" placeholder="tag/name">
<button class="btn btn-outline-success" type="button" onClick="SearchForPerson(` <button class="btn btn-outline-success" type="button" onClick="SearchForPerson(`
div+= item[key]['id'] div+= item[key]['id'] + ',' + face_pos
div+=`)">Search</button> div+=`)">Search</button>
</div> </div>
<div id="search_person_results"> <div id="search_person_results">

View File

@@ -9,7 +9,7 @@ from status import st, Status
from flask_login import login_required, current_user from flask_login import login_required, current_user
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from shared import GenFace, GenThumb from shared import GenFace, GenThumb
from face import Face, FaceRefimgLink from face import Face, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceManualOverride
import os import os
import json import json
import time import time
@@ -243,7 +243,6 @@ def find_persons(who):
resp[p.id]['firstname']=p.firstname resp[p.id]['firstname']=p.firstname
resp[p.id]['surname']=p.surname resp[p.id]['surname']=p.surname
print( resp )
return resp return resp
################################################################################ ################################################################################
@@ -253,15 +252,39 @@ def find_persons(who):
@login_required @login_required
def override_force_match(): def override_force_match():
person_id = request.form['person_id'] person_id = request.form['person_id']
face_id = request.form['face_id']
p = Person.query.get(person_id); p = Person.query.get(person_id);
if not p: if not p:
raise Exception("could not find person to add override too!") raise Exception("could not find person to add override too!")
face_id = request.form['face_id']
f = Face.query.get(face_id); f = Face.query.get(face_id);
if not f: if not f:
raise Exception("could not find face to add override for!") 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" ) mo = FaceManualOverride( face_id=f.id, face=f.face, person_id=p.id )
st.SetMessage( f"Being asked to force an override match for face_id {face_id}, for person: {p.tag} -- doing nothing for now" ) db.session.add( mo )
# might need to do something smarter here (reload to old view is good though & happens now, not sure why (last url)?) db.session.commit()
return "ok"
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

View File

@@ -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 -- 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. -- 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) -- 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_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) ); 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) -- 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, create table PERSON_REFIMG_LINK ( PERSON_ID integer, REFIMG_ID integer,
constraint PK_PRL primary key(PERSON_ID, REFIMG_ID), constraint PK_PRL primary key(PERSON_ID, REFIMG_ID),

View File

@@ -48,6 +48,17 @@
data['who']='{{face.refimg.person.tag}}' data['who']='{{face.refimg.person.tag}}'
data['distance']="{{face.refimg_lnk.face_distance|round(2)}}" data['distance']="{{face.refimg_lnk.face_distance|round(2)}}"
{% endif %} {% 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 ) e.faces.push( data )
{% endfor %} {% endfor %}
objs[{{id}}]=e objs[{{id}}]=e