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, PA from face import Face, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceForceMatchOverride from path import Path, PathType from job import JobExtra, NewJob 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(PA,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) face_top = db.Column(db.Integer) face_right = db.Column(db.Integer) face_bottom = db.Column(db.Integer) face_left = db.Column(db.Integer) 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 ) ################################################################################ # Class describing Person to Refimg link in DB via sqlalchemy ################################################################################ class PersonRefimgLink(PA,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) ################################################################################ # Class describing Person in DB via sqlalchemy ################################################################################ class Person(PA,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) ################################################################################ # 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' ) ################################################################################ # Helper functions # AddRefimgToPerson( filename, person ) ################################################################################ def AddRefimgToPerson( filename, person ): refimg = Refimg( fname=os.path.basename( filename ) ) try: #False == dont autorotate, its not needed on this image refimg.thumbnail, refimg.orig_w, refimg.orig_h = GenThumb( filename, False ) settings = Settings.query.first() model=AIModel.query.get(settings.default_refimg_model) refimg.face, face_locn = GenFace( filename, model=model.name ) try: os.remove(filename) except Exception as e: # can fail "silently" here, if the face_locn worked, great, its only # a tmp file in /tmp - if not, the next if will send a msg to the front-end print( f"Failed to delete tmp file for refimg addition: {e}" ) if not face_locn: st.SetMessage( f"Failed to find face in Refimg:" ) raise Exception("Could not find face in uploaded reference image" ) return # DEL THIS NEXT LINE (BUG-108) refimg.face_locn = json.dumps(face_locn) refimg.face_top = face_locn[0] refimg.face_right = face_locn[1] refimg.face_bottom = face_locn[2] refimg.face_left = face_locn[3] refimg.model_used = settings.default_refimg_model refimg.created_on = time.time() 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 ################################################################################ # TempRefimgFile: helper function that takes data POST'd (from dialog box to # add face to new/existing person). Converts data into a jpg file to be used by # wrapper funcs to AI / refimg <-> person. filename will be .jpg ################################################################################ def TempRefimgFile( data, tag ): # undo the munging sending via http has done data=data.replace(' ', '+' ) # convert b64 encoded to a temp file to process... bytes_decoded = base64.b64decode(data) img = Image.open(BytesIO(bytes_decoded)) out_jpg = img.convert("RGB") # save file to /tmp/.jpg fname="/tmp/" + tag + ".jpg" out_jpg.save(fname) return fname ################################################################################ # 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 ) @app.route("/match_with_create_person", methods=["POST"]) @login_required def match_with_create_person(): p = Person( tag=request.form["tag"], surname=request.form["surname"], firstname=request.form["firstname"] ) # add this fname (of temp refimg) to person fname=TempRefimgFile( request.form['refimg_data'], p.tag ) AddRefimgToPerson( fname, p ) resp={} resp['who']=p.tag resp['distance']='0.0' return resp ################################################################################ # /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: # in case DB data gets broken, just fix it - still keeps happening if r.face_locn[0]=='{': r.face_locn[0]='[' r.face_locn[-1]=']' r.tmp_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"Failed to load reference image: {e}", "danger" ) AddRefimgToPerson( fname, person ) 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 ################################################################################ # /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'] ) # add this fname (of temp refimg) to person fname=TempRefimgFile( request.form['refimg_data'], p.tag ) AddRefimgToPerson( fname, p ) if request.form['search'] == "true": jex=[] ptype=PathType.query.filter(PathType.name=='Import').first() jex.append( JobExtra( name=f"person", value="all" ) ) jex.append( JobExtra( name=f"path_type", value=ptype.id ) ) job=NewJob( "run_ai_on_path", 0, None, jex ) st.SetMessage( f"Created Job #{job.id} to Look for face(s) in import path(s)") jex=[] ptype=PathType.query.filter(PathType.name=='Storage').first() jex.append( JobExtra( name=f"person", value="all" ) ) jex.append( JobExtra( name=f"path_type", value=ptype.id ) ) job=NewJob( "run_ai_on_path", 0, None, jex ) st.SetMessage( f"Created Job #{job.id} to Look for face(s) in storage path(s)") resp['who']=p.tag resp['distance']='0.0' return resp ################################################################################ # /add_force_match_override -> POST ################################################################################ @app.route("/add_force_match_override", methods=["POST"]) @login_required def add_force_match_override(): 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 = FaceForceMatchOverride( face_id=f.id, person_id=p.id ) db.session.add( mo ) db.session.commit() jex=[] jex.append( JobExtra( name="which", value="add_force_match_override" ) ) jex.append( JobExtra( name="face_id", value=f.id ) ) jex.append( JobExtra( name="person_id", value=p.id ) ) # dont do status update here, the F/E is in the middle of a dbox, just send metadata through to the B/E NewJob( "metadata", 0, None, jex ) 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_force_match_override -> POST ################################################################################ @app.route("/remove_force_match_override", methods=["POST"]) @login_required def remove_force_match_override(): 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}" ) FaceForceMatchOverride.query.filter( FaceForceMatchOverride.face_id==face_id ).delete() db.session.commit() # needed to use person_id in job below (allows consistent processing in job_mgr) p=Person.query.filter(Person.tag==person_tag).one() jex=[] jex.append( JobExtra( name="which", value="remove_force_match_override" ) ) jex.append( JobExtra( name="face_id", value=face_id ) ) jex.append( JobExtra( name="person_id", value=p.id ) ) # dont do status update here, the F/E is in the middle of a dbox, just send metadata through to the B/E NewJob( "metadata", 0, None, jex ) # this will reply to the Ajax / POST, and cause the page to re-draw with new face override resp={} return resp ################################################################################ # /remove_no_match_override -> POST ################################################################################ @app.route("/remove_no_match_override", methods=["POST"]) @login_required def remove_no_match_override(): 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() jex=[] jex.append( JobExtra( name="which", value="remove_no_match_override" ) ) jex.append( JobExtra( name="face_id", value=face_id ) ) jex.append( JobExtra( name="type_id", value=type_id ) ) # dont do status update here, the F/E is in the middle of a dbox, just send metadata through to the B/E NewJob( "metadata", 0, None, jex ) # 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, type_id=t.id ) db.session.add( nmo ) db.session.commit() jex=[] jex.append( JobExtra( name="which", value="add_no_match_override" ) ) jex.append( JobExtra( name="face_id", value=f.id ) ) jex.append( JobExtra( name="type_id", value=t.id ) ) # dont do status update here, the F/E is in the middle of a dbox, just send metadata through to the B/E NewJob( "metadata", 0, None, jex ) 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 @app.route('/fix_faces') @login_required def fix_faces(): for r in Refimg.query.all(): print( f"BEFORE: ref# {r.id}, face_locn = {r.face_locn}, top={r.face_top}, right={r.face_right}, bottom={r.face_bottom}, left={r.face_left}" ) tmp=json.loads(r.face_locn) r.face_top = tmp[0] r.face_right = tmp[1] r.face_bottom = tmp[2] r.face_left = tmp[3] db.session.add(r) print( f" FIXED: ref# {r.id}, face_locn = {r.face_locn}, top={r.face_top}, right={r.face_right}, bottom={r.face_bottom}, left={r.face_left}" ) for f in Face.query.all(): print( f"BEFORE: face# {f.id}, locn = {f.locn}, top={f.face_top}, right={f.face_right}, bottom={f.face_bottom}, left={f.face_left}" ) tmp=json.loads(f.locn) f.face_top = tmp[0] f.face_right = tmp[1] f.face_bottom = tmp[2] f.face_left = tmp[3] db.session.add(f) print( f" FIXED: ref# {f.id}, locn = {f.locn}, top={f.face_top}, right={f.face_right}, bottom={f.face_bottom}, left={f.face_left}" ) db.session.commit() print("faces should be fixed") return render_template("base.html" )