Files
photoassistant/ai.py

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, 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/<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 )