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 # 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 "".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"" ################################################################################ # 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 "".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
, 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' ) ################################################################################ # 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"Failed to add Person: {e.orig}", "danger" ) return redirect( url_for( '/persons') ) else: return render_template("person.html", person=None, form=form, page_title=page_title ) ################################################################################ # /person/ -> GET/POST(save or delete) -> shows/edits/delets a single person ################################################################################ @app.route("/person/", 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"Failed to modify Person: {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!") f=request.files['refimg_file'] refimg = Refimg( fname=f.filename ) try: # save the actual uploaded image to reference_images/ fname=secure_filename(f.filename) if fname == "": raise Exception("invalid filename") fname = f"/tmp/{fname}" f.save( fname ) refimg.thumbnail, refimg.orig_w, refimg.orig_h = GenThumb( fname ) settings = Settings.query.first() model=AIModel.query.get(settings.default_refimg_model) refimg.face, face_locn = GenFace( fname, model=model.name ) refimg.face_locn = json.dumps(face_locn) refimg.model_used = settings.default_refimg_model refimg.created_on = time.time() os.remove(fname) 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"Failed to add Refimg: {e.orig}", "danger" ) except Exception as e: st.SetMessage( f"Failed to modify Refimg: {e}", "danger" ) return redirect( url_for( 'person', id=person.id) ) ################################################################################ # /find_persons/ -> POST (an arbitrary string to find a person (via # name/tag match) ################################################################################ @app.route("/find_persons/", 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 ################################################################################ # /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("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!") 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