180 lines
7.8 KiB
Python
180 lines
7.8 KiB
Python
""" file containing all functions to handle routes relating to AI functionality """
|
|
|
|
# pylint: disable=singleton-comparison
|
|
|
|
from sqlalchemy import func, desc, asc, distinct
|
|
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=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/<face_id>", 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()
|
|
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 )
|