first pass of allowing to scan for all files in import and storage paths <- works, and showing all unknown faces to handle them somehow - very rudimentary
This commit is contained in:
2
TODO
2
TODO
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
* move all unsorted photos/* -> import/
|
* move all unsorted photos/* -> import/
|
||||||
|
|
||||||
* add an option on the person menu to run_ai_on all photos (or at least import/storage)
|
|
||||||
|
|
||||||
* per file you could select an unknown face and add it as a ref img to an existing person, or make a new person and attach?
|
* per file you could select an unknown face and add it as a ref img to an existing person, or make a new person and attach?
|
||||||
|
|
||||||
* when search, have a way to hide deleted files
|
* when search, have a way to hide deleted files
|
||||||
|
|||||||
63
ai.py
63
ai.py
@@ -5,9 +5,14 @@ from main import db, app, ma
|
|||||||
from sqlalchemy import Sequence
|
from sqlalchemy import Sequence
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from status import st, Status
|
from status import st, Status
|
||||||
from files import Entry, File
|
from path import Path, PathType
|
||||||
|
from files import Entry, Dir, File, PathDirLink
|
||||||
from person import Refimg, Person, PersonRefimgLink
|
from person import Refimg, Person, PersonRefimgLink
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
from job import Job, JobExtra, Joblog, NewJob
|
from job import Job, JobExtra, Joblog, NewJob
|
||||||
from face import Face, FaceFileLink, FaceRefimgLink
|
from face import Face, FaceFileLink, FaceRefimgLink
|
||||||
@@ -52,3 +57,59 @@ def run_ai_on():
|
|||||||
job=NewJob( "run_ai_on", 0, None, jex )
|
job=NewJob( "run_ai_on", 0, None, jex )
|
||||||
st.SetMessage( f"Created <a href=/job/{job.id}>Job #{job.id}</a> to Look for face(s) in selected file(s)")
|
st.SetMessage( f"Created <a href=/job/{job.id}>Job #{job.id}</a> to Look for face(s) in selected file(s)")
|
||||||
return render_template("base.html")
|
return render_template("base.html")
|
||||||
|
|
||||||
|
@app.route("/run_ai_on_import")
|
||||||
|
@login_required
|
||||||
|
def run_ai_on_import():
|
||||||
|
jex=[]
|
||||||
|
jex.append( JobExtra( name=f"person", value="all" ) )
|
||||||
|
paths=Path.query.join(PathType).filter(PathType.name=='Import').all()
|
||||||
|
path_cnt=0
|
||||||
|
for p in paths:
|
||||||
|
d = Dir.query.join(PathDirLink).filter(PathDirLink.path_id==p.id).filter(Dir.rel_path=='').first()
|
||||||
|
jex.append( JobExtra( name=f"eid-{path_cnt}", value=f"{d.eid}" ) )
|
||||||
|
path_cnt+=1
|
||||||
|
print(jex)
|
||||||
|
job=NewJob( "run_ai_on", 0, None, jex )
|
||||||
|
st.SetMessage( f"Created <a href=/job/{job.id}>Job #{job.id}</a> to Look for face(s) in import path(s)")
|
||||||
|
return render_template("base.html")
|
||||||
|
|
||||||
|
@app.route("/run_ai_on_storage")
|
||||||
|
@login_required
|
||||||
|
def run_ai_on_storage():
|
||||||
|
jex=[]
|
||||||
|
jex.append( JobExtra( name=f"person", value="all" ) )
|
||||||
|
paths=Path.query.join(PathType).filter(PathType.name=='Storage').all()
|
||||||
|
path_cnt=0
|
||||||
|
for p in paths:
|
||||||
|
d = Dir.query.join(PathDirLink).filter(PathDirLink.path_id==p.id).filter(Dir.rel_path=='').first()
|
||||||
|
jex.append( JobExtra( name=f"eid-{path_cnt}", value=f"{d.eid}" ) )
|
||||||
|
path_cnt+=1
|
||||||
|
print(jex)
|
||||||
|
job=NewJob( "run_ai_on", 0, None, jex )
|
||||||
|
st.SetMessage( f"Created <a href=/job/{job.id}>Job #{job.id}</a> to Look for face(s) in storage path(s)")
|
||||||
|
return render_template("base.html")
|
||||||
|
|
||||||
|
@app.route("/unmatched_faces")
|
||||||
|
@login_required
|
||||||
|
def unmatched_faces():
|
||||||
|
faces=Face.query.join(FaceFileLink).join(FaceRefimgLink, isouter=True).filter(FaceRefimgLink.refimg_id==None).limit(10).all()
|
||||||
|
imgs={}
|
||||||
|
for face in faces:
|
||||||
|
face.locn=json.loads("["+face.locn+"]")
|
||||||
|
f = Entry.query.join(File).join(FaceFileLink).filter(FaceFileLink.face_id==face.id).first()
|
||||||
|
face.file_eid=f.id
|
||||||
|
face.url=f.FullPathOnFS()
|
||||||
|
x=face.locn[0][3]*0.95
|
||||||
|
y=face.locn[0][0]*0.95
|
||||||
|
x2=face.locn[0][1]*1.05
|
||||||
|
y2=face.locn[0][2]*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)
|
||||||
|
|||||||
29
internal/js/face.js
Normal file
29
internal/js/face.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Define this once and before it will be called, hence at the top of this file
|
||||||
|
function DrawRefimg(fig, img, canvas, orig_face )
|
||||||
|
{
|
||||||
|
// FIXME: should get this from shared.py, not sure why this doesnt work at present
|
||||||
|
thumbsize=256
|
||||||
|
|
||||||
|
context=canvas.getContext('2d')
|
||||||
|
// another call to this func will occur on load, so skip this one
|
||||||
|
if( img.width == 0 )
|
||||||
|
return
|
||||||
|
|
||||||
|
// only set canvas.width once we have valid img dimensions
|
||||||
|
canvas.width=img.width/2
|
||||||
|
|
||||||
|
// actually draw the pixel images to the canvas at the right size
|
||||||
|
context.drawImage(img, 0, 0, img.width/(img.height/canvas.height), canvas.height);
|
||||||
|
fig.width(canvas.width)
|
||||||
|
|
||||||
|
// draw rectangle on face
|
||||||
|
context.beginPath();
|
||||||
|
new_x=(orig_face.x/orig_face.orig_w)*img.width/(img.height/canvas.height)
|
||||||
|
new_y=(orig_face.y/orig_face.orig_h)*thumbsize/(img.height/canvas.height)
|
||||||
|
new_w=(orig_face.w/orig_face.orig_w)*img.width/(img.height/canvas.height)
|
||||||
|
new_h=(orig_face.h/orig_face.orig_h)*thumbsize/(img.height/canvas.height)
|
||||||
|
context.rect(new_x, new_y, new_w, new_h)
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeStyle = 'green';
|
||||||
|
context.stroke();
|
||||||
|
}
|
||||||
@@ -74,7 +74,10 @@
|
|||||||
<div class="dropdown-menu" aria-labelledby="PersonMenu">
|
<div class="dropdown-menu" aria-labelledby="PersonMenu">
|
||||||
<a class="dropdown-item" href="{{url_for('new_person')}}">Create Person</a>
|
<a class="dropdown-item" href="{{url_for('new_person')}}">Create Person</a>
|
||||||
<a class="dropdown-item" href="{{url_for('persons')}}">Show People</a>
|
<a class="dropdown-item" href="{{url_for('persons')}}">Show People</a>
|
||||||
<a class="dropdown-item" href="{{url_for('aistats')}}">View stats of matched faces</a>
|
<a class="dropdown-item" href="{{url_for('aistats')}}">View stats of matches</a>
|
||||||
|
<a class="dropdown-item" href="{{url_for('run_ai_on_import')}}">Match in import path(s) </a>
|
||||||
|
<a class="dropdown-item" href="{{url_for('run_ai_on_storage')}}">Match in storage path(s) </a>
|
||||||
|
<a class="dropdown-item" href="{{url_for('unmatched_faces')}}">Show unmatched</a>
|
||||||
</div>
|
</div>
|
||||||
</div class="nav-item dropdown">
|
</div class="nav-item dropdown">
|
||||||
<div class="nav-item dropdown">
|
<div class="nav-item dropdown">
|
||||||
|
|||||||
50
templates/faces.html
Normal file
50
templates/faces.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block main_content %}
|
||||||
|
<script src="{{ url_for( 'internal', filename='js/face.js')}}"></script>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<h3>Unmatched Faces</h3>
|
||||||
|
<div class="row mt-3">
|
||||||
|
{% for f in faces %}
|
||||||
|
<div id="F{{f.id}}" class="col-2 px-0">
|
||||||
|
<form id="_fm" method="POST" action="/view/{{f.file_eid}}">
|
||||||
|
<input type="hidden" name="eids" value="{{f.file_eid}},">
|
||||||
|
<input type="hidden" name="noo" value="newest">
|
||||||
|
<input type="hidden" name="cwd" value="/">
|
||||||
|
<input type="hidden" name="root" value="/">
|
||||||
|
<input type="hidden" name="size" value="128">
|
||||||
|
<input type="hidden" name="grouping" value="1">
|
||||||
|
<input type="hidden" name="offset" value="0">
|
||||||
|
<input type="hidden" name="folders" value="false">
|
||||||
|
<input type="hidden" name="how_many" value="1">
|
||||||
|
<input type="hidden" name="orig_url" value="{{request.path}}">'
|
||||||
|
|
||||||
|
<figure id="fig_{{f.id}}">
|
||||||
|
<div style="position:relative">
|
||||||
|
<canvas id="c_{{f.id}}" height="128"></canvas>
|
||||||
|
<script>
|
||||||
|
var im_{{f.id}}=new Image();
|
||||||
|
im_{{f.id}}.src="data:image/jpeg;base64,{{f.img}}";
|
||||||
|
fig_{{f.id}}=$('#fig_{{f.id}}')
|
||||||
|
// store this stuff in an javascript Object to use when document is ready event is triggered
|
||||||
|
var orig_face_{{f.id}}=new Object;
|
||||||
|
orig_face_{{f.id}}.x = {{f.locn[0][3]}}
|
||||||
|
orig_face_{{f.id}}.y = {{f.locn[0][0]}}
|
||||||
|
orig_face_{{f.id}}.w = {{f.locn[0][1]}}-{{f.locn[0][3]}}
|
||||||
|
orig_face_{{f.id}}.h = {{f.locn[0][2]}}-{{f.locn[0][0]}}
|
||||||
|
orig_face_{{f.id}}.orig_w = orig_face_{{f.id}}.w
|
||||||
|
orig_face_{{f.id}}.orig_h = orig_face_{{f.id}}.h
|
||||||
|
|
||||||
|
// when the document is ready, then DrawRefimg
|
||||||
|
$(function() { DrawRefimg( fig_{{f.id}}, im_{{f.id}}, c_{{f.id}}, orig_face_{{f.id}} ) });
|
||||||
|
</script>
|
||||||
|
<figcaption>{{f.id}}</figcation>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
<button>Go</button>
|
||||||
|
</form>
|
||||||
|
</div id="/F*">
|
||||||
|
{% endfor %}
|
||||||
|
</div class="row">
|
||||||
|
</div class="container-fluid">
|
||||||
|
{% endblock main_content %}
|
||||||
@@ -1,35 +1,5 @@
|
|||||||
{% extends "base.html" %} {% block main_content %}
|
{% extends "base.html" %} {% block main_content %}
|
||||||
<script>
|
<script src="{{ url_for( 'internal', filename='js/face.js')}}"></script>
|
||||||
// Define this once and before it will be called, hence at the top of this file
|
|
||||||
function DrawRefimg(fig, img, canvas, orig_face )
|
|
||||||
{
|
|
||||||
// FIXME: should get this from shared.py, not sure why this doesnt work at present
|
|
||||||
thumbsize=256
|
|
||||||
|
|
||||||
context=canvas.getContext('2d')
|
|
||||||
// another call to this func will occur on load, so skip this one
|
|
||||||
if( img.width == 0 )
|
|
||||||
return
|
|
||||||
|
|
||||||
// only set canvas.width once we have valid img dimensions
|
|
||||||
canvas.width=img.width/2
|
|
||||||
|
|
||||||
// actually draw the pixel images to the canvas at the right size
|
|
||||||
context.drawImage(img, 0, 0, img.width/(img.height/canvas.height), canvas.height);
|
|
||||||
fig.width(canvas.width)
|
|
||||||
|
|
||||||
// draw rectangle on face
|
|
||||||
context.beginPath();
|
|
||||||
new_x=(orig_face.x/orig_face.orig_w)*img.width/(img.height/canvas.height)
|
|
||||||
new_y=(orig_face.y/orig_face.orig_h)*thumbsize/(img.height/canvas.height)
|
|
||||||
new_w=(orig_face.w/orig_face.orig_w)*img.width/(img.height/canvas.height)
|
|
||||||
new_h=(orig_face.h/orig_face.orig_h)*thumbsize/(img.height/canvas.height)
|
|
||||||
context.rect(new_x, new_y, new_w, new_h)
|
|
||||||
context.lineWidth = 2;
|
|
||||||
context.strokeStyle = 'green';
|
|
||||||
context.stroke();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row col-12"><h3 class="offset-3">{{page_title}}</h3></div>
|
<div class="row col-12"><h3 class="offset-3">{{page_title}}</h3></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user