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 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"<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
### THEN find_person needs to call appropriate override func (OR pass in type?) and get it to be smarter - that sounds okay actually
# create table FACE_OVERRIDE_TYPE ( ID integer, NAME varchar unique, constraint PK_FACE_OVERRIDE_TYPE_ID primary key(ID) );
#create sequence FACE_OVERRIDE_TYPE_ID_SEQ;
#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) );
#insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Not a face' );
#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' );
#insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Manual match' );
#
#-- 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,
# 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 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) );
class FaceOverrideType(db.Model):
__tablename__ = "face_override_type"
id = db.Column(db.Integer, db.Sequence('face_override_type_id_seq'), primary_key=True )
name = db.Column( db.String )
def __repr__(self):
return f"<id: {self.id}, name={self.name}>"
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"<id: {self.id}, face_id={self.face_id}, type: {self.type}>"
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 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(",")

View File

@@ -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:<br><br>'
for( var key in data ) {
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 )
}
@@ -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 ='<p>'
div+='Face position #' + item[key]['which_face']
div+='Face position #' + face_pos
div+='<div id="face_img"></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+='</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' )
{
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">
<button class="btn btn-outline-success" type="button" onClick="SearchForPerson(`
div+= item[key]['id']
div+= item[key]['id'] + ',' + face_pos
div+=`)">Search</button>
</div>
<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 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

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
-- 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),

View File

@@ -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