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 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, FaceNoMatchOverride, FaceManualOverride from face import Face, FaceFileLink, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceManualOverride
# pylint: disable=no-member # pylint: disable=no-member
@@ -737,6 +737,7 @@ def view(id):
face.no_match_override=fnmo face.no_match_override=fnmo
mo = FaceManualOverride.query.filter(FaceManualOverride.face_id==face.id).first() mo = FaceManualOverride.query.filter(FaceManualOverride.face_id==face.id).first()
if mo: if mo:
mo.type = FaceOverrideType.query.filter( FaceOverrideType.name== 'Manual match to existing person' ).first()
face.manual_override=mo face.manual_override=mo
@@ -753,7 +754,8 @@ def view(id):
st.SetMessage( msg, "warning" ) st.SetMessage( msg, "warning" )
return redirect("/") return redirect("/")
else: 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/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 // 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 // the height > width, so scale the new width based on height ratio of
// image to window // image to window
@@ -116,19 +115,20 @@ function DrawImg()
// this face has an override so diff colour // this face has an override so diff colour
if( objs[current].faces[i].override ) 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 )
{ {
str=objs[current].faces[i].who context.strokeStyle = 'blue'
if( $('#distance').prop('checked') ) DrawLabelOnFace( objs[current].faces[i].override.who )
str += "("+objs[current].faces[i].distance+")" }
DrawLabelOnFace( str ) else
{
context.strokeStyle = 'green'
if( objs[current].faces[i].who )
{
str=objs[current].faces[i].who
if( $('#distance').prop('checked') )
str += "("+objs[current].faces[i].distance+")"
DrawLabelOnFace( str )
}
} }
context.stroke(); context.stroke();
} }
@@ -230,7 +230,7 @@ $(document).ready( function()
{ {
if( objs[current].faces[i].override ) 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 ) 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_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_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 } for( var el in NMO ) {
item_list['no_match_no_face']={ 'name': 'Mark as not a face', 'which_face': i, 'id': objs[current].faces[i].id } item_list['NMO_'+el]={'type_id': NMO[el].type_id, 'name': 'Override: ' + NMO[el].name, '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 }
} }
delete item_list['not_a_face'] delete item_list['not_a_face']
$('#canvas').prop('menu_item', item_list ) $('#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 // 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 ofm='&person_id='+person_id+'&face_id='+face_id
$.ajax({ type: 'POST', data: ofm, url: '/override_force_match', success: function(data) { $.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 objs[current].faces[face_pos].override={}
if( objs[current].faces[face_pos].who ) objs[current].faces[face_pos].override.who=data.person_tag
objs[current].faces[face_pos].old_who=objs[current].faces[face_pos].who objs[current].faces[face_pos].override.distance='N/A'
objs[current].faces[face_pos].old_distance=objs[current].faces[face_pos].distance objs[current].faces[face_pos].override.type_id=NMO[fm_idx].id
objs[current].faces[face_pos].who=data.person_tag objs[current].faces[face_pos].override.type_name=NMO[fm_idx].name
objs[current].faces[face_pos].distance="N/A"
objs[current].faces[face_pos].override=1
$('#dbox').modal('hide') $('#dbox').modal('hide')
$('#faces').prop('checked',true) $('#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 // 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, face_pos) function SearchForPerson(face_id, face_pos, type_id)
{ {
// make URI safe // make URI safe
who = encodeURIComponent( $('#stext').val() ) 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>' 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 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+',' 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 ) $('#search_person_results').html( content )
} }
@@ -309,16 +311,12 @@ function SearchForPerson(face_id, face_pos)
return false 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+ d='&face_id='+objs[current].faces[face_pos].id+'&person_tag='+objs[current].faces[face_pos].who+
'&file_eid='+current '&file_eid='+current
$.ajax({ type: 'POST', data: d, url: '/remove_override', $.ajax({ type: 'POST', data: d, url: '/remove_override_force_match',
success: function(data) { 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 delete objs[current].faces[face_pos].override
$('#dbox').modal('hide') $('#dbox').modal('hide')
DrawImg() DrawImg()
@@ -328,6 +326,37 @@ function RemoveOverride()
return false 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 // 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
@@ -344,42 +373,60 @@ function FaceDBox(key, item)
} }
} ) } )
div+='</p>' 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+='<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-info col-6" type="button" onClick="$(\'#dbox\').modal(\'hide\'); return false">Cancel</button>'
div+='<button class="btn btn-outline-danger col-6" type="button" '+ div+='<button class="btn btn-outline-danger col-6" type="button" '
'onClick="RemoveOverride(' +face_pos+ ')">Remove</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>' div+='</div>'
} }
if ( key == 'no_match_new_person' ) 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+='<h5>search for existing person: NOT YET</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>`
}
if ( key == 'wrong_person' ) 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>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'] 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'] if( item[key].name == 'Override: Manual match to existing person' )
div+='<br>face db id: ' + item[key]['id'] {
} 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-title').html(item[key]['name'])
$('#dbox-content').html(div) $('#dbox-content').html(div)
$('#dbox').modal('show') $('#dbox').modal('show')

View File

@@ -270,17 +270,17 @@ def override_force_match():
resp={} resp={}
resp['person_tag']=p.tag resp['person_tag']=p.tag
return resp 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 @login_required
def remove_override(): def remove_override_force_match():
face_id = request.form['face_id'] face_id = request.form['face_id']
person_tag = request.form['person_tag'] person_tag = request.form['person_tag']
file_eid = request.form['file_eid'] 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() FaceManualOverride.query.filter( FaceManualOverride.face_id==face_id ).delete()
db.session.commit() 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 # this will reply to the Ajax / POST, and cause the page to re-draw with new face override
resp={} resp={}
return 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_TYPE_ID_SEQ;
create sequence FACE_OVERRIDE_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) ); 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')), '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')), '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')), '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 -- 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.

View File

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