update file to use new pylint settings, added types and using docstrings in goolge format with partial openapi spec
This commit is contained in:
159
ai.py
159
ai.py
@@ -1,90 +1,140 @@
|
||||
from wtforms import SubmitField, StringField, HiddenField, validators, Form
|
||||
from flask_wtf import FlaskForm
|
||||
""" file containing all functions to handle routes relating to AI functionality """
|
||||
|
||||
# pylint: disable=singleton-comparison
|
||||
|
||||
from flask import request, render_template, redirect, make_response
|
||||
from main import db, app, ma
|
||||
from sqlalchemy import Sequence
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from path import Path, PathType
|
||||
from files import Entry, Dir, File, PathDirLink
|
||||
from person import Refimg, Person, PersonRefimgLink
|
||||
from flask_login import login_required, current_user
|
||||
from flask_login import login_required
|
||||
from PIL import Image
|
||||
import io
|
||||
import base64
|
||||
|
||||
from job import Job, JobExtra, Joblog, NewJob
|
||||
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
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
################################################################################
|
||||
# /ai_stats -> placholder for some sort of stats
|
||||
################################################################################
|
||||
@app.route("/ai_stats", methods=["GET"])
|
||||
@login_required
|
||||
def ai_stats():
|
||||
stats = 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 order by 2 desc" )
|
||||
cnt_res = db.session.execute( "select count(1) from ( select p.tag 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 ) as foo" )
|
||||
""" 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 = 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 order by 2 desc" } )
|
||||
cnt_res = db.session.execute( {
|
||||
"select count(1) from "
|
||||
" ( select p.tag 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 ) as foo" } )
|
||||
num_stats=cnt_res.first()[0]
|
||||
|
||||
fstats={}
|
||||
fstats['files_with_a_face'] = db.session.execute( "select count(distinct file_eid) as count from face_file_link" ).first()[0]
|
||||
fstats['files_with_a_match'] = db.session.execute( "select count(distinct ffl.file_eid) as count from face_file_link ffl, face_refimg_link frl where frl.face_id = ffl.face_id" ).first()[0]
|
||||
fstats['files_with_missing_matches'] = db.session.execute( "select count(distinct ffl.file_eid) from face f left join face_refimg_link frl on f.id = frl.face_id join face_file_link ffl on f.id = ffl.face_id where frl.refimg_id is null" ).first()[0]
|
||||
fstats["files_with_a_face"] = db.session.execute( "select count(distinct file_eid) as count from face_file_link" ).first()[0]
|
||||
sql="select count(distinct ffl.file_eid) as count from face_file_link ffl, face_refimg_link frl where frl.face_id = ffl.face_id"
|
||||
fstats["files_with_a_match"] = db.session.execute( sql ).first()[0]
|
||||
sql={"select count(distinct ffl.file_eid) from face f left join "
|
||||
" face_refimg_link frl on f.id = frl.face_id join face_file_link ffl on f.id = ffl.face_id where frl.refimg_id is null" }
|
||||
fstats["files_with_missing_matches"] = db.session.execute( sql ).first()[0]
|
||||
|
||||
# files_with_no_matches?
|
||||
|
||||
fstats['all_faces'] = db.session.execute( "select count(distinct face_id) as count from face_file_link" ).first()[0]
|
||||
fstats['all_matched_faces'] = db.session.execute( "select count(distinct face_id) as count from face_refimg_link" ).first()[0]
|
||||
fstats['all_unmatched_faces'] = db.session.execute( "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" ).first()[0]
|
||||
fstats["all_faces"] = db.session.execute( "select count(distinct face_id) as count from face_file_link" ).first()[0]
|
||||
fstats["all_matched_faces"] = db.session.execute( "select count(distinct face_id) as count from face_refimg_link" ).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"] = db.session.execute( sql).first()[0]
|
||||
|
||||
return render_template("ai_stats.html", page_title='AI Statistics', stats=stats, num_stats=num_stats, fstats=fstats )
|
||||
return render_template("ai_stats.html", page_title="AI Statistics", stats=stats, num_stats=num_stats, fstats=fstats )
|
||||
|
||||
|
||||
################################################################################
|
||||
# /run_ai_on -> creates a job, with extras containing entry ids (eid-0, eid-1,
|
||||
# etc.) and person=all|dad, etc. Room to consider threshold, algo, etc.
|
||||
################################################################################
|
||||
@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=f"{el}", value=request.form[el] ) )
|
||||
job=NewJob( "run_ai_on", num_files=0, wait_for=None, jex=jex, desc="Look for face(s) in selected file(s)" )
|
||||
jex.append( JobExtra( name=el, value=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=f"person", value="all" ) )
|
||||
jex.append( JobExtra( name=f"path_type", value=ptype.id ) )
|
||||
job=NewJob( "run_ai_on_path", num_files=0, wait_for=None, jex=jex, desc="Look for face(s) in import path(s)")
|
||||
ptype=PathType.query.filter(PathType.name=="Import").first()
|
||||
jex.append( JobExtra( name="person", value="all" ) )
|
||||
jex.append( JobExtra( name="path_type", value=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=f"person", value="all" ) )
|
||||
jex.append( JobExtra( name=f"path_type", value=ptype.id ) )
|
||||
job=NewJob( "run_ai_on_path", num_files=0, wait_for=None, jex=jex, desc="Look for face(s) in storage path(s)")
|
||||
ptype=PathType.query.filter(PathType.name=="Storage").first()
|
||||
jex.append( JobExtra( name="person", value="all" ) )
|
||||
jex.append( JobExtra( name="path_type", value=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()
|
||||
imgs={}
|
||||
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()
|
||||
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
|
||||
@@ -96,7 +146,7 @@ def unmatched_faces():
|
||||
im = Image.open(f.FullPathOnFS())
|
||||
region = im.crop((x, y, x2, y2))
|
||||
img_bytearray = io.BytesIO()
|
||||
region.save(img_bytearray, format='JPEG')
|
||||
region.save(img_bytearray, format="JPEG")
|
||||
img_bytearray = img_bytearray.getvalue()
|
||||
face.img = base64.b64encode(img_bytearray)
|
||||
face.img = str(face.img)[2:-1]
|
||||
@@ -104,14 +154,29 @@ def unmatched_faces():
|
||||
return render_template("faces.html", faces=faces)
|
||||
|
||||
|
||||
# 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)
|
||||
@app.route("/get_face_from_image/<face_id>", methods=["POST"])
|
||||
################################################################################
|
||||
@app.route("/get_face_from_image/<face_id:int>", methods=["POST"])
|
||||
@login_required
|
||||
def get_face_from_image(face_id):
|
||||
""" route to handle URL: /get_face_from_image/<face_id:int>
|
||||
|
||||
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()
|
||||
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
|
||||
@@ -120,7 +185,7 @@ def get_face_from_image(face_id):
|
||||
im = Image.open(f.FullPathOnFS())
|
||||
region = im.crop((x, y, x2, y2))
|
||||
img_bytearray = io.BytesIO()
|
||||
region.save(img_bytearray, format='JPEG')
|
||||
region.save(img_bytearray, format="JPEG")
|
||||
img_bytearray = img_bytearray.getvalue()
|
||||
face_img = base64.b64encode(img_bytearray)
|
||||
face_img = str(face_img)[2:-1]
|
||||
|
||||
Reference in New Issue
Block a user