can now add/remove overrides (manual or no matchx3) in any sequence of load/reload, or back-to-back and it all works

This commit is contained in:
2022-06-13 18:30:13 +10:00
parent 83819a0cb3
commit b935aa8ab8
5 changed files with 179 additions and 74 deletions

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, FaceNoMatchOverride, FaceManualOverride
from face import Face, FaceFileLink, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceManualOverride
# pylint: disable=no-member
@@ -737,6 +737,7 @@ def view(id):
face.no_match_override=fnmo
mo = FaceManualOverride.query.filter(FaceManualOverride.face_id==face.id).first()
if mo:
mo.type = FaceOverrideType.query.filter( FaceOverrideType.name== 'Manual match to existing person' ).first()
face.manual_override=mo
@@ -753,7 +754,8 @@ def view(id):
st.SetMessage( msg, "warning" )
return redirect("/")
else:
return render_template("viewer.html", current=int(id), eids=eids, objs=objs, OPT=OPT )
NMO_data = FaceOverrideType.query.all()
return render_template("viewer.html", current=int(id), eids=eids, objs=objs, OPT=OPT, NMO_data=NMO_data )
##################################################################################
# /view/id -> grabs data from DB and views it (POST -> set state, redirect to GET)

View File

@@ -1,4 +1,3 @@
// work out new width for canvas. depending on whether width > height of the
// image, then use the width of the image (with the specified gap) otherwise
// the height > width, so scale the new width based on height ratio of
// image to window
@@ -116,13 +115,13 @@ function DrawImg()
// this face has an override so diff colour
if( objs[current].faces[i].override )
{
context.strokeStyle = 'blue'
DrawLabelOnFace( objs[current].faces[i].override.who )
}
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 )
{
str=objs[current].faces[i].who
@@ -130,6 +129,7 @@ function DrawImg()
str += "("+objs[current].faces[i].distance+")"
DrawLabelOnFace( str )
}
}
context.stroke();
}
}
@@ -230,7 +230,7 @@ $(document).ready( function()
{
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 }
item_list['remove_override_force_match']={ 'name': 'Remove override for this face', 'which_face': i, 'id': objs[current].faces[i].id }
}
else if( objs[current].faces[i].who )
{
@@ -241,10 +241,9 @@ $(document).ready( function()
{
item_list['no_match_new_person']={ 'name': 'Add as reference image to NEW person', 'which_face': i, 'id': objs[current].faces[i].id }
item_list['no_match_new_refimg']={ 'name': 'Add as reference image to EXISTING person', 'which_face': i, 'id': objs[current].faces[i].id }
item_list['no_match_override_match']={ 'name': 'Manually match to existing person', '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_ignore']={ 'name': 'Ignore this face', 'which_face': i, 'id': objs[current].faces[i].id }
for( var el in NMO ) {
item_list['NMO_'+el]={'type_id': NMO[el].type_id, 'name': 'Override: ' + NMO[el].name, 'which_face': i, 'id': objs[current].faces[i].id }
}
}
delete item_list['not_a_face']
$('#canvas').prop('menu_item', item_list )
@@ -264,17 +263,24 @@ $(document).ready( function()
} );
// quick wrapper function to make calling this ajax code simpler in SearchForPerson
function OverrideForceMatch( person_id, face_id, face_pos )
function OverrideForceMatch( person_id, face_id, face_pos, type_id )
{
// we have type_id passed in, so dig the NMO out, and use that below (its really just for name, but in case we change that in the DB)
for( el in NMO )
{
if( NMO[el].type_id == type_id )
{
fm_idx=el
break
}
}
ofm='&person_id='+person_id+'&face_id='+face_id
$.ajax({ type: 'POST', data: ofm, url: '/override_force_match', success: function(data) {
// on success, remember the original data, apply override values, close the dbox, force face drawing boxes on and redraw the face with the new override
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].old_distance=objs[current].faces[face_pos].distance
objs[current].faces[face_pos].who=data.person_tag
objs[current].faces[face_pos].distance="N/A"
objs[current].faces[face_pos].override=1
objs[current].faces[face_pos].override={}
objs[current].faces[face_pos].override.who=data.person_tag
objs[current].faces[face_pos].override.distance='N/A'
objs[current].faces[face_pos].override.type_id=NMO[fm_idx].id
objs[current].faces[face_pos].override.type_name=NMO[fm_idx].name
$('#dbox').modal('hide')
$('#faces').prop('checked',true)
@@ -286,7 +292,7 @@ function OverrideForceMatch( person_id, face_id, face_pos )
// 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, face_pos)
function SearchForPerson(face_id, face_pos, type_id)
{
// make URI safe
who = encodeURIComponent( $('#stext').val() )
@@ -296,12 +302,8 @@ function SearchForPerson(face_id, face_pos)
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 class="link-primary" onClick="OverrideForceMatch('+person.id+','
+face_id+','+face_pos+')">'+person.firstname+' '+person.surname+' ('+person.tag+')</a><br>'
*/
content+= '<a role=button class="link link-primary" onClick="OverrideForceMatch('+person.id+','
+face_id+','+face_pos+')">'+person.firstname+' '+person.surname+' ('+person.tag+')</a><br>'
+face_id+','+face_pos+','+type_id+')">'+person.firstname+' '+person.surname+' ('+person.tag+')</a><br>'
}
$('#search_person_results').html( content )
}
@@ -309,16 +311,12 @@ function SearchForPerson(face_id, face_pos)
return false
}
function RemoveOverride()
function RemoveOverrideForceMatch(face_pos)
{
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',
$.ajax({ type: 'POST', data: d, url: '/remove_override_force_match',
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()
@@ -328,6 +326,37 @@ function RemoveOverride()
return false
}
function RemoveOverrideNoMatch(face_pos, type_id)
{
d='&face_id='+objs[current].faces[face_pos].id+'&type_id='+type_id
$.ajax({ type: 'POST', data: d, url: '/remove_override_no_match',
success: function(data) {
delete objs[current].faces[face_pos].override
$('#dbox').modal('hide')
DrawImg()
return false
}
} )
return false
}
function AddNoMatchOverride(type_id, face_id, face_pos, type_id)
{
d='&type_id='+type_id+'&face_id='+face_id
$.ajax({ type: 'POST', data: d, url: '/add_no_match_override',
success: function(data) {
objs[current].faces[face_pos].override={}
objs[current].faces[face_pos].override.who=NMO[type_id].name
objs[current].faces[face_pos].override.distance='N/A'
objs[current].faces[face_pos].override.type_id=type_id
objs[current].faces[face_pos].override.type_name=NMO[type_id].name
$('#dbox').modal('hide')
$('#faces').prop('checked',true)
DrawImg()
}
} )
}
// 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
@@ -344,41 +373,59 @@ function FaceDBox(key, item)
}
} )
div+='</p>'
if ( key == 'remove_override' )
if ( key == 'remove_override_force_match' )
{
div+='<div class="row col-12">remove this override (force match to: ' + objs[current].faces[face_pos].who + ')'
if( objs[current].faces[face_pos].override.type_name == 'Manual match to existing person' )
div+='<div class="row col-12">remove this override (force match to: ' + objs[current].faces[face_pos].override.who + ')</div>'
else
div+='<div class="row col-12">remove this override (no match)</div>'
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+='<button class="btn btn-outline-danger col-6" type="button" '
if( objs[current].faces[face_pos].override.type_name == 'Manual match to existing person' )
div+='onClick="RemoveOverrideForceMatch(' +face_pos+ ')">Remove</button>'
else
div+='onClick="RemoveOverrideNoMatch(' +face_pos+','+objs[current].faces[face_pos].override.type_id+ ')">Remove</button>'
div+='</div>'
}
if ( key == 'no_match_new_person' )
{
div+='<br>create new person'
div+='<br>create new person NOT YET'
}
if ( key == 'no_match_new_refimg' || key == 'no_match_override_match' )
if ( key == 'no_match_new_refimg' )
{
div+='<h5>search for existing person:</h5>'
div+=
`
<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'] + ',' + face_pos
div+=`)">Search</button>
</div>
<div id="search_person_results">
</div>`
div+='<h5>search for existing person: NOT YET</h5>'
}
if ( key == 'wrong_person' )
{
div+='<br>wrong person, so mark this as the wrong person/refimg connection, for face#' + item[key]['which_face']
div+='<br>face db id: ' + item[key]['id']
}
if ( key == 'no_match_no_face' || key == 'no_match_too_young' || key == 'no_match_ignore' )
if( /NMO_/.test(key) )
{
div+='<br>just track this against face#' + item[key]['which_face']
div+='<br>face db id: ' + item[key]['id']
if( item[key].name == 'Override: Manual match to existing person' )
{
div+='<h5>search for existing person:</h5>'
div+=
`
<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'] + ',' + face_pos + ',' + item[key].type_id
div+=`)">Search</button>
</div>
<div id="search_person_results">
</div>`
}
else
{
type_id=item[key].type_id
face_id=item[key].id
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="AddNoMatchOverride('+type_id+','+face_id+','+face_pos+','+type_id+ ')">Apply Override</button>'
div+='</div>'
}
}
$('#dbox-title').html(item[key]['name'])
$('#dbox-content').html(div)

View File

@@ -272,15 +272,15 @@ def override_force_match():
return resp
################################################################################
# /remove_override -> POST
# /remove_override_force_match -> POST
################################################################################
@app.route("/remove_override", methods=["POST"])
@app.route("/remove_override_force_match", methods=["POST"])
@login_required
def remove_override():
def remove_override_force_match():
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}" )
print( f"Remove override force match of face_id={face_id} to person_tag={person_tag}" )
FaceManualOverride.query.filter( FaceManualOverride.face_id==face_id ).delete()
db.session.commit()
@@ -288,3 +288,47 @@ def remove_override():
# this will reply to the Ajax / POST, and cause the page to re-draw with new face override
resp={}
return resp
################################################################################
# /remove_override_no_match -> POST
################################################################################
@app.route("/remove_override_no_match", methods=["POST"])
@login_required
def remove_override_no_match():
face_id = request.form['face_id']
type_id = request.form['type_id']
print( f"Remove override of no match (type_id={type_id}) for face_id={face_id}" )
FaceNoMatchOverride.query.filter( FaceNoMatchOverride.face_id==face_id, FaceNoMatchOverride.type_id==type_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
################################################################################
# /add_no_match_override -> POST
################################################################################
@app.route("/add_no_match_override", methods=["POST"])
@login_required
def add_no_match_override():
face_id = request.form['face_id']
f = Face.query.get(face_id);
if not f:
raise Exception("could not find face to add override too!")
type_id = request.form['type_id']
t = FaceOverrideType.query.get(type_id);
if not t:
raise Exception("could not find override_type to add override for!")
nmo = FaceNoMatchOverride( face_id=f.id, face=f.face, type_id=t.id )
db.session.add( nmo )
db.session.commit()
print( f"Placing an override of NO Match for face_id {face_id}" )
# this will reply to the Ajax / POST, and cause the page to re-draw with new face override to person_tag
resp={}
resp['type']=t.name
return resp

View File

@@ -95,10 +95,10 @@ create table FACE_REFIMG_LINK( FACE_ID integer, REFIMG_ID integer, FACE_DISTANCE
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')), 'Manual match to existing person' );
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.

View File

@@ -23,6 +23,7 @@
var throbber=0
var objs=[]
var NMO=[]
var current={{current}}
var eids="{{eids}}"
var eid_lst=eids.split(",")
@@ -49,22 +50,33 @@
data['distance']="{{face.refimg_lnk.face_distance|round(2)}}"
{% endif %}
{% if face.no_match_override %}
data['override']=1
data['no_match_override'] = {
data['override'] = {
'face_id' : '{{face.no_match_override.face_id}}',
'type' : '{{face.no_match_override.type.name}}',
'type_id' : '{{face.no_match_override.type.id}}',
'type_name': '{{face.no_match_override.type.name}}',
'who' : '{{face.no_match_override.type.name}}',
'distance' : 'N/A'
}
{% endif %}
{% if face.manual_override %}
data['override']=1
data['who']='{{face.manual_override.person.tag}}'
data['distance']="N/A"
data['override'] = {
'face_id' : '{{face.manual_override.face_id}}',
'type_id' : '{{face.manual_override.type.id}}',
'type_name': '{{face.manual_override.type.name}}',
'who' : '{{face.manual_override.person.tag}}',
'distance' : 'N/A'
}
{% endif %}
e.faces.push( data )
{% endfor %}
objs[{{id}}]=e
{% endfor %}
{% for el in NMO_data %}
NMO[{{el.id}}] = { 'type_id': {{el.id}}, 'name': '{{el.name}}' }
{% endfor %}
function PrettyFname(fname)
{