""" file containing all functions to handle routes relating to AI functionality """ # pylint: disable=singleton-comparison from sqlalchemy import func, desc, asc, distinct, text from flask import request, render_template, redirect, make_response from flask_login import login_required from PIL import Image import io import base64 from main import db, app from path import PathType from files import Entry, File from job import JobExtra, NewJob from face import Face, FaceFileLink, FaceRefimgLink, FaceNoMatchOverride, FaceForceMatchOverride from person import Refimg, Person, PersonRefimgLink ################################################################################ @app.route("/ai_stats", methods=["GET"]) @login_required def ai_stats(): """ route to handle URL: /ai_stats --- responses: 200: description: renders ai_stats.html to display counts of how many matches for each person we have """ stats=Person.query.join(PersonRefimgLink).join(Refimg).join(FaceRefimgLink).with_entities( func.count( Person.tag ).label('count'), Person.tag ).group_by( Person.tag ).order_by( desc(text('count')),asc(Person.tag)).all() fstats={} fstats["files_with_a_face"] = FaceFileLink.query.with_entities( func.count(distinct(FaceFileLink.file_eid))).first()[0] fstats["files_with_a_match"] = FaceFileLink.query.join(Face).join(FaceRefimgLink).with_entities( func.count(distinct(FaceFileLink.file_eid)) ).first()[0] fstats["files_with_missing_matches"] = Face.query.join(FaceRefimgLink, isouter=True).join(FaceFileLink).filter(FaceRefimgLink.refimg_id==None).with_entities( func.count(distinct(FaceFileLink.file_eid)) ).first()[0] fstats["all_faces"] = FaceFileLink.query.with_entities( func.count(distinct(FaceFileLink.face_id)) ).first()[0] fstats["all_matched_faces"] = FaceRefimgLink.query.with_entities( func.count(distinct(FaceRefimgLink.face_id)) ).first()[0] sql="select count(f.id) from face f left join face_refimg_link frl on f.id = frl.face_id where frl.refimg_id is null" fstats["all_unmatched_faces"] = Face.query.join(FaceRefimgLink, isouter=True).filter(FaceRefimgLink.refimg_id==None).with_entities( func.count(distinct(Face.id)) ).first()[0] # files_with_no_matches? return render_template("ai_stats.html", page_title="AI Statistics", stats=stats, num_stats=len(stats), fstats=fstats ) ################################################################################ @app.route("/run_ai_on", methods=["POST"]) @login_required def run_ai_on(): """ route to handle URL: /run_ai_on this route creates a job for the job manager to scan for face(s) with AI on the files/dirs passed in as form variables named eid-X, where X=0, 1, 2, etc. and each eid-X contains an eid from the database for the dir/file entry jobextras created containing entry ids (eid-0, eid-1, and person=all|dad, etc. Room to consider threshold, algo, etc. --- responses: 302: description: redirects to /jobs page showing all jobs (including this new one) """ jex=[] for el in request.form: jex.append( JobExtra( name=el, value=str(request.form[el]) ) ) NewJob( "run_ai_on", num_files=0, wait_for=None, jex=jex, desc="Look for face(s) in selected file(s)" ) return redirect("/jobs") ################################################################################ @app.route("/run_ai_on_import", methods=["GET"]) @login_required def run_ai_on_import(): """ route to handle URL: /run_ai_on_import this route creates a job for the job manager to scan for the all faces with AI on all the files in the import dir --- responses: 302: description: redirects to /jobs page showing all jobs (including this new one) """ jex=[] ptype=PathType.query.filter(PathType.name=="Import").first() jex.append( JobExtra( name="person", value="all" ) ) jex.append( JobExtra( name="path_type", value=str(ptype.id) ) ) NewJob( "run_ai_on_path", num_files=0, wait_for=None, jex=jex, desc="Look for face(s) in import path(s)") return redirect("/jobs") ################################################################################ @app.route("/run_ai_on_storage", methods=["GET"]) @login_required def run_ai_on_storage(): """ route to handle URL: /run_ai_on_storage this route creates a job for the job manager to scan for the all faces with AI on all the files in the storage dir --- responses: 302: description: redirects to /jobs page showing all jobs (including this new one) """ jex=[] ptype=PathType.query.filter(PathType.name=="Storage").first() jex.append( JobExtra( name="person", value="all" ) ) jex.append( JobExtra( name="path_type", value=str(ptype.id) ) ) NewJob( "run_ai_on_path", num_files=0, wait_for=None, jex=jex, desc="Look for face(s) in storage path(s)") return redirect("/jobs") ################################################################################ @app.route("/unmatched_faces", methods=["GET"]) @login_required def unmatched_faces(): """ route to handle URL: /unmatched_faces --- responses: 200: description: renders faces.html to show up to 10 faces that AI has found, that have no matching person """ # get overrides and exclude them as they have been processed already fnmo_ids = [id[0] for id in FaceNoMatchOverride.query.with_entities(FaceNoMatchOverride.face_id).all()] fmo_ids = [id[0] for id in FaceForceMatchOverride.query.with_entities(FaceForceMatchOverride.face_id).all()] faces=Face.query.join(FaceFileLink).join(FaceRefimgLink, isouter=True).filter(FaceRefimgLink.refimg_id==None) \ .filter(Face.id.not_in(fnmo_ids+fmo_ids)).order_by(Face.h.desc()).limit(10).all() for face in faces: f = Entry.query.join(File).join(FaceFileLink).filter(FaceFileLink.face_id==face.id).first() face.file_eid=f.id face.url=f.FullPathOnFS() face.filename=f.name x=face.face_left*0.95 y=face.face_top*0.95 x2=face.face_right*1.05 y2=face.face_bottom*1.05 im = Image.open(f.FullPathOnFS()) region = im.crop((x, y, x2, y2)) img_bytearray = io.BytesIO() region.save(img_bytearray, format="JPEG") img_bytearray = img_bytearray.getvalue() face.img = base64.b64encode(img_bytearray) face.img = str(face.img)[2:-1] return render_template("faces.html", faces=faces) ################################################################################ @app.route("/get_face_from_image/", methods=["POST"]) @login_required def get_face_from_image(face_id): """ route to handle URL: /get_face_from_image/ this is called in Ajax, when we manually override a face that is currently unmatched load the original full image, find the current face's coords, grab pixels 10% larger and return it so we can show it in the dbox, and be able to pass it around for refimg creation (if needed) --- responses: 200: description: Base64-encoded image of face AI found returned successfully content: text/plain: schema: type: string format: binary description: Base64-encoded image data """ face=Face.query.get(face_id) f = Entry.query.join(File).join(FaceFileLink).filter(FaceFileLink.face_id==face_id).first() x=face.face_left*0.95 y=face.face_top*0.95 x2=face.face_right*1.05 y2=face.face_bottom*1.05 im = Image.open(f.FullPathOnFS()) region = im.crop((x, y, x2, y2)) img_bytearray = io.BytesIO() region.save(img_bytearray, format="JPEG") img_bytearray = img_bytearray.getvalue() face_img = base64.b64encode(img_bytearray) face_img = str(face_img)[2:-1] return make_response( face_img )