Files
photoassistant/person.py

389 lines
17 KiB
Python

from wtforms import SubmitField, StringField, HiddenField, validators, Form
from flask_wtf import FlaskForm
from flask import request, render_template, redirect, url_for
from main import db, app, ma
from settings import Settings, AIModel
from sqlalchemy import Sequence
from sqlalchemy.exc import SQLAlchemyError
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, FaceOverrideType, FaceNoMatchOverride, FaceManualOverride
import os
import json
import time
from PIL import Image
import base64
from io import BytesIO
import os.path
# pylint: disable=no-member
################################################################################
# Class describing Refimg in DB via sqlalchemy
# fname: original file name of refimg
# face: actual binary of numpy data for this refimg's face (always only 1)
# orig*: width/height of original image, because when we show in person, it get scaled
# face_locn: location of ace - we need to know to draw green box around face locn (with orig* above)
# thumbnail: image data of actual img. once we load refimg, we only store this data, not the orig file
# model_used: which AI model (cnn or hog) used to create face
# person: read-only convenience field not in DB, just used in html
################################################################################
class Refimg(db.Model):
id = db.Column(db.Integer, db.Sequence('refimg_id_seq'), primary_key=True )
fname = db.Column(db.String(256), unique=True, nullable=False)
face = db.Column(db.LargeBinary, unique=True, nullable=False)
orig_w = db.Column(db.Integer)
orig_h = db.Column(db.Integer)
face_locn = db.Column(db.String)
thumbnail = db.Column(db.String, unique=True, nullable=False)
created_on = db.Column(db.Float)
model_used = db.Column(db.Integer, db.ForeignKey("ai_model.id") )
person = db.relationship( 'Person', secondary="person_refimg_link", uselist=False, viewonly=True )
def __repr__(self):
return "<id: {}, fname: {}>".format(self.id, self.fname )
################################################################################
# Class describing Person to Refimg link in DB via sqlalchemy
################################################################################
class PersonRefimgLink(db.Model):
__tablename__ = "person_refimg_link"
person_id = db.Column(db.Integer, db.ForeignKey('person.id'), unique=True, nullable=False, primary_key=True)
refimg_id = db.Column(db.Integer, db.ForeignKey('refimg.id'), unique=True, nullable=False, primary_key=True)
def __repr__(self):
return f"<person_id: {self.person_id}, refimg_id: {self.refimg_id}>"
################################################################################
# Class describing Person in DB via sqlalchemy
################################################################################
class Person(db.Model):
id = db.Column(db.Integer, db.Sequence('person_id_seq'), primary_key=True )
tag = db.Column(db.String(48), unique=False, nullable=False)
surname = db.Column(db.String(48), unique=False, nullable=False)
firstname = db.Column(db.String(48), unique=False, nullable=False)
refimg = db.relationship('Refimg', secondary=PersonRefimgLink.__table__, order_by=Refimg.id)
def __repr__(self):
return "<tag: {}, firstname: {}, surname: {}, refimg: {}>".format(self.tag,self.firstname, self.surname, self.refimg)
################################################################################
# Helper class that inherits a .dump() method to turn class Person into json / useful in jinja2
################################################################################
class PersonSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Person
ordered = True
################################################################################
# Helper class that defines a form for person, used to make html <form>, with field validation (via wtforms)
################################################################################
class PersonForm(FlaskForm):
id = HiddenField()
tag = StringField('Tag (searchable name):', [validators.DataRequired()])
firstname = StringField('FirstName(s):', [validators.DataRequired()])
surname = StringField('Surname:', [validators.DataRequired()])
save = SubmitField('Save' )
delete = SubmitField('Delete' )
################################################################################
# Helper functions
# AddRefimgToPerson( filename, person )
################################################################################
def AddRefimgToPerson( filename, person ):
refimg = Refimg( fname=os.path.basename( filename ) )
try:
refimg.thumbnail, refimg.orig_w, refimg.orig_h = GenThumb( filename )
settings = Settings.query.first()
model=AIModel.query.get(settings.default_refimg_model)
refimg.face, face_locn = GenFace( filename, model=model.name )
refimg.face_locn = json.dumps(face_locn)
refimg.model_used = settings.default_refimg_model
refimg.created_on = time.time()
os.remove(filename)
person.refimg.append(refimg)
db.session.add(person)
db.session.add(refimg)
db.session.commit()
st.SetMessage( f"Associated new Refimg ({refimg.fname}) with person: {person.tag}" )
except SQLAlchemyError as e:
st.SetMessage( f"<b>Failed to add Refimg:</b>&nbsp;{e.orig}", "danger" )
except Exception as e:
st.SetMessage( f"<b>Failed to modify Refimg:</b>&nbsp;{e}", "danger" )
return
################################################################################
# Routes for person data
#
# /persons -> GET only -> prints out list of all persons
################################################################################
@app.route("/persons", methods=["GET"])
@login_required
def persons():
persons = Person.query.order_by(Person.tag).all()
# get the num matches for each person too, and allow link for it
stats = list( db.session.execute( "select p.tag, count(f.id) from person p, face f, face_file_link ffl, face_refimg_link frl, person_refimg_link prl where p.id = prl.person_id and prl.refimg_id = frl.refimg_id and frl.face_id = ffl.face_id and ffl.face_id = f.id group by p.tag" ) )
for p in persons:
if not p.refimg:
continue
for s in stats:
if p.tag == s[0]:
p.num_matches = s[1];
break
return render_template("persons.html", persons=persons)
################################################################################
# /person -> GET/POST -> creates a new person and shows new creation
################################################################################
@app.route("/person", methods=["GET", "POST"])
@login_required
def new_person():
form = PersonForm(request.form)
page_title='Create new Person'
if request.method=='POST':
person = Person( tag=request.form["tag"], surname=request.form["surname"], firstname=request.form["firstname"] )
try:
db.session.add(person)
db.session.commit()
st.SetMessage( "Created new Person ({})".format(person.tag) )
return redirect( url_for( 'person', id=person.id) )
except SQLAlchemyError as e:
st.SetMessage( f"<b>Failed to add Person:</b>&nbsp;{e.orig}", "danger" )
return redirect( url_for( '/persons') )
else:
return render_template("person.html", person=None, form=form, page_title=page_title )
################################################################################
# /person/<id> -> GET/POST(save or delete) -> shows/edits/delets a single person
################################################################################
@app.route("/person/<id>", methods=["GET", "POST"])
@login_required
def person(id):
form = PersonForm(request.form)
page_title='Edit Person'
if request.method == 'POST':
try:
person = Person.query.get(id)
if 'delete' in request.form:
st.SetMessage("Successfully deleted Person: ({})".format( person.tag ) )
# do linkages by hand, or one day replace with delete cascade in the DB defintions
db.session.execute( f"delete from face_refimg_link frl where refimg_id in ( select refimg_id from person_refimg_link where person_id = {id} )" )
db.session.execute( f"delete from person_refimg_link where person_id = {id}" )
db.session.execute( f"delete from refimg where id not in ( select refimg_id from person_refimg_link )" )
# now can delete the person entry with no foreign key data left
Person.query.filter(Person.id==id).delete()
db.session.commit()
return redirect( url_for( 'persons' ) )
elif request.form and form.validate():
new_refs=[]
for ref_img in person.refimg:
if "ref-img-id-{}".format(ref_img.id) in request.form:
new_refs.append(ref_img)
if new_refs != person.refimg:
deld = list(set(person.refimg) - set(new_refs));
person.refimg = new_refs
# delete the "match" between a face found in a file and this ref img
FaceRefimgLink.query.filter(FaceRefimgLink.refimg_id==deld[0].id).delete()
Refimg.query.filter(Refimg.id==deld[0].id).delete()
st.SetMessage( f"Successfully Updated Person: removed reference image {deld[0].fname}" )
else:
st.SetMessage("Successfully Updated Person: (From: {}, {}, {})".format(person.tag, person.firstname, person.surname) )
person.tag = request.form['tag']
person.surname = request.form['surname']
person.firstname = request.form['firstname']
st.AppendMessage(" To: ({}, {}, {})".format(person.tag, person.firstname, person.surname) )
db.session.add(person)
db.session.commit()
return redirect( url_for( 'person', id=person.id) )
except SQLAlchemyError as e:
st.SetMessage( f"<b>Failed to modify Person:</b>&nbsp;{e}", "danger" )
return redirect( url_for( 'persons' ) )
else:
person = Person.query.get(id)
if not person:
st.SetMessage( f"No such person with id: {id}", "danger" )
return render_template("base.html" )
for r in person.refimg:
r.face_locn=json.loads(r.face_locn)
form = PersonForm(request.values, obj=person)
return render_template("person.html", person=person, form=form, page_title = page_title)
################################################################################
# /add_refimg -> POST(add new refimg to a person)
################################################################################
@app.route("/add_refimg", methods=["POST"])
@login_required
def add_refimg():
# now save into the DB
person = Person.query.get(request.form['person_id']);
if not person:
raise Exception("could not find person to add reference image too!")
try:
# save the actual uploaded image to reference_images/
f=request.files['refimg_file']
fname=secure_filename(f.filename)
if fname == "":
raise Exception("invalid filename")
fname = f"/tmp/{fname}"
f.save( fname )
except Exception as e:
st.SetMessage( f"<b>Failed to load reference image:</b>&nbsp;{e}", "danger" )
AddRefimgToPerson( fname, person )
return redirect( url_for( 'person', id=person.id) )
################################################################################
# /find_persons/<who> -> POST (an arbitrary string to find a person (via
# name/tag match)
################################################################################
@app.route("/find_persons/<who>", methods=["POST"])
@login_required
def find_persons(who):
resp={}
people = Person.query.filter( Person.tag.ilike(f'%{who}%') | Person.firstname.ilike(f'%{who}%') | Person.surname.ilike(f'%{who}%') ).all();
for p in people:
resp[p.id]={}
resp[p.id]['id']=p.id
resp[p.id]['tag']=p.tag
resp[p.id]['firstname']=p.firstname
resp[p.id]['surname']=p.surname
return resp
################################################################################
# /add_refimg_to_person/ -> POST
################################################################################
@app.route("/add_refimg_to_person", methods=["POST"])
@login_required
def add_refimg_to_person():
resp={}
f = Face.query.get( request.form['face_id'] )
p = Person.query.get( request.form['person_id'] )
file_eid = request.form['file_eid']
refimg_data = request.form['refimg_data']
# undo the munging sending via http has done
refimg_data=refimg_data.replace(' ', '+' )
print( refimg_data )
# convert b64 encoded to a temp file to process...
bytes_decoded = base64.b64decode(refimg_data)
img = Image.open(BytesIO(bytes_decoded))
out_jpg = img.convert("RGB")
# save file to /tmp/<p.tag>
fname="/tmp/" + p.tag + '.jpg'
out_jpg.save(fname)
# add this fname (of temp refimg) to person
AddRefimgToPerson( fname, p )
# DDP:
# need to create a new job to re-do AI now we have a new refimg in the mix
resp['who']=p.tag
resp['distance']='0.0'
return resp
################################################################################
# /override_force_match -> POST
################################################################################
@app.route("/override_force_match", methods=["POST"])
@login_required
def override_force_match():
person_id = request.form['person_id']
p = Person.query.get(person_id);
if not p:
raise Exception( f"could not find person (id={person_id}) 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!")
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_force_match -> POST
################################################################################
@app.route("/remove_override_force_match", methods=["POST"])
@login_required
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 force match of 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
################################################################################
# /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