refimgs now contain face, orig_w, orig_h and face_locns. This is done via json.* to allow arrays to be saved/loaded back into face_locn - not useful for refimg as there has to be only 1, but tested for images where there will be many faces. This commit has a fair few changes. So overall, no more refimg menus/creation. You now create a person (the add button is hidden until you save), when you save you go back to the person you created rather than the list of persons. From there you can click add ref img, and it will create a thumbnail, and draw a green box around the face locations based on the data. Persons can have many refimgs, and they will all work the same, be formatted prettily no matter how many you have. Each refimg "tab" not only has the thumbnail, but also a red X click to delete button that will remove all refimg data and connection to the person table too. This all works/is tested.
This commit is contained in:
22
TODO
22
TODO
@@ -1,20 +1,21 @@
|
|||||||
## GENERAL
|
## GENERAL
|
||||||
|
|
||||||
* refimg
|
|
||||||
- remove AI menu from top-level -> make a sub-of Person, and just have Match or AI
|
|
||||||
- store the face locations? (good for debugging later, and we calc them as part of GenFace anyway) ...
|
|
||||||
(really, doing this properly, we shoudl keep the face locns for ALL images)...
|
|
||||||
---> may well mean in a week or so, we move to the new DB structure, and START FORM SCRATCH)
|
|
||||||
|
|
||||||
* allow rotate of image (permanently on FS, so its right everywhere)
|
* allow rotate of image (permanently on FS, so its right everywhere)
|
||||||
|
|
||||||
* improve photo browser -> view file, rather than just allowing browser to show image
|
* improve photo browser -> view file, rather than just allowing browser to show image
|
||||||
|
|
||||||
|
* face locations:
|
||||||
|
START FORM SCRATCH so all images have face_locn data
|
||||||
|
right now GenThumb is in shared, and does width, height as well --> in person.py BUT need this for pa_job_manager
|
||||||
|
|
||||||
* allow for threshold/settings to be tweaked from the GUI
|
* allow for threshold/settings to be tweaked from the GUI
|
||||||
- it would be good to then say, just run the scanner against this image or maybe this DIR, to see how it IDs ppl
|
- it would be good to then say, just run the scanner against this image or maybe this DIR, to see how it IDs ppl
|
||||||
---> settings for default value
|
---> settings for default value
|
||||||
---> override table to do per file combos?
|
---> override table to do per file combos?
|
||||||
|
|
||||||
|
* refimg
|
||||||
|
- remove AI menu from top-level -> make a sub-of Person, and just have Match or AI
|
||||||
|
|
||||||
* fix up logging in general
|
* fix up logging in general
|
||||||
* comment your code
|
* comment your code
|
||||||
* more OO goodness :)
|
* more OO goodness :)
|
||||||
@@ -33,9 +34,6 @@
|
|||||||
|
|
||||||
*** Need to use thread-safe sessions per Thread, half-assed version did not work
|
*** Need to use thread-safe sessions per Thread, half-assed version did not work
|
||||||
|
|
||||||
- would it be quicker/smarter to use md5 hash matching on import (and if
|
|
||||||
so, not re-do face* ) ???
|
|
||||||
|
|
||||||
need a manual button to restart a job in the GUI,
|
need a manual button to restart a job in the GUI,
|
||||||
(based on file-level optims, just run the job as new and it will optim over already done parts and continue)
|
(based on file-level optims, just run the job as new and it will optim over already done parts and continue)
|
||||||
|
|
||||||
@@ -67,7 +65,6 @@
|
|||||||
need to copy into here the jquery/fa files so we don't need internet to function
|
need to copy into here the jquery/fa files so we don't need internet to function
|
||||||
- for that matter run lightspeed against all this
|
- for that matter run lightspeed against all this
|
||||||
|
|
||||||
|
|
||||||
timelineview? (I think maybe sunburst for large amounts of files, then maybe something more timeline-series for drilling in?)
|
timelineview? (I think maybe sunburst for large amounts of files, then maybe something more timeline-series for drilling in?)
|
||||||
(vertical timeline, date has thumbnails (small) horizontally along
|
(vertical timeline, date has thumbnails (small) horizontally along
|
||||||
a page, etc.?
|
a page, etc.?
|
||||||
@@ -81,3 +78,8 @@
|
|||||||
* exif processing?
|
* exif processing?
|
||||||
* location stuff - test a new photo from my camera out
|
* location stuff - test a new photo from my camera out
|
||||||
-- image is in dir, need to look at exifread output
|
-- image is in dir, need to look at exifread output
|
||||||
|
|
||||||
|
### FUTURE:
|
||||||
|
* can emby use nfo for images (for AI/tags?)
|
||||||
|
-NO sadly
|
||||||
|
|
||||||
|
|||||||
15
person.py
15
person.py
@@ -10,6 +10,7 @@ from werkzeug import secure_filename
|
|||||||
from shared import GenFace, GenThumb
|
from shared import GenFace, GenThumb
|
||||||
from face import FaceRefimgLink
|
from face import FaceRefimgLink
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
@@ -21,6 +22,9 @@ class Refimg(db.Model):
|
|||||||
id = db.Column(db.Integer, db.Sequence('refimg_id_seq'), primary_key=True )
|
id = db.Column(db.Integer, db.Sequence('refimg_id_seq'), primary_key=True )
|
||||||
fname = db.Column(db.String(256), unique=True, nullable=False)
|
fname = db.Column(db.String(256), unique=True, nullable=False)
|
||||||
face = db.Column(db.LargeBinary, unique=True, nullable=False)
|
face = db.Column(db.LargeBinary, unique=True, nullable=False)
|
||||||
|
orig_w = db.Column(db.Integer)
|
||||||
|
orig_h = db.Column(db.Integer)
|
||||||
|
face_locn = db.Column(db.String)
|
||||||
thumbnail = db.Column(db.String, unique=True, nullable=False)
|
thumbnail = db.Column(db.String, unique=True, nullable=False)
|
||||||
created_on = db.Column(db.Float)
|
created_on = db.Column(db.Float)
|
||||||
|
|
||||||
@@ -41,7 +45,7 @@ class Person(db.Model):
|
|||||||
tag = db.Column(db.String(48), unique=False, nullable=False)
|
tag = db.Column(db.String(48), unique=False, nullable=False)
|
||||||
surname = db.Column(db.String(48), unique=False, nullable=False)
|
surname = db.Column(db.String(48), unique=False, nullable=False)
|
||||||
firstname = db.Column(db.String(48), unique=False, nullable=False)
|
firstname = db.Column(db.String(48), unique=False, nullable=False)
|
||||||
refimg = db.relationship('Refimg', secondary=PersonRefimgLink.__table__)
|
refimg = db.relationship('Refimg', secondary=PersonRefimgLink.__table__, order_by=Refimg.id)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<tag: {}, firstname: {}, surname: {}, refimg: {}>".format(self.tag,self.firstname, self.surname, self.refimg)
|
return "<tag: {}, firstname: {}, surname: {}, refimg: {}>".format(self.tag,self.firstname, self.surname, self.refimg)
|
||||||
@@ -94,7 +98,7 @@ def new_person():
|
|||||||
db.session.add(person)
|
db.session.add(person)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
st.SetMessage( "Created new Person ({})".format(person.tag) )
|
st.SetMessage( "Created new Person ({})".format(person.tag) )
|
||||||
return redirect( '/persons' )
|
return redirect( f'/person/{person.id}' )
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
st.SetAlert( "danger" )
|
st.SetAlert( "danger" )
|
||||||
st.SetMessage( "<b>Failed to add Person:</b> {}".format(e.orig) )
|
st.SetMessage( "<b>Failed to add Person:</b> {}".format(e.orig) )
|
||||||
@@ -141,6 +145,8 @@ def person(id):
|
|||||||
return render_template("person.html", form=form, page_title=page_title)
|
return render_template("person.html", form=form, page_title=page_title)
|
||||||
else:
|
else:
|
||||||
person = Person.query.get(id)
|
person = Person.query.get(id)
|
||||||
|
for r in person.refimg:
|
||||||
|
r.face_locn=json.loads(r.face_locn)
|
||||||
form = PersonForm(request.values, obj=person)
|
form = PersonForm(request.values, obj=person)
|
||||||
return render_template("person.html", person=person, form=form, page_title = page_title)
|
return render_template("person.html", person=person, form=form, page_title = page_title)
|
||||||
|
|
||||||
@@ -164,8 +170,9 @@ def add_refimg():
|
|||||||
|
|
||||||
fname = f"/tmp/{fname}"
|
fname = f"/tmp/{fname}"
|
||||||
f.save( fname )
|
f.save( fname )
|
||||||
refimg.thumbnail = GenThumb( fname )
|
refimg.thumbnail, refimg.orig_w, refimg.orig_h = GenThumb( fname )
|
||||||
refimg.face = GenFace( fname )
|
refimg.face, face_locn = GenFace( fname )
|
||||||
|
refimg.face_locn = json.dumps(face_locn)
|
||||||
os.remove(fname)
|
os.remove(fname)
|
||||||
person.refimg.append(refimg)
|
person.refimg.append(refimg)
|
||||||
db.session.add(person)
|
db.session.add(person)
|
||||||
|
|||||||
@@ -1,67 +1,118 @@
|
|||||||
{% extends "base.html" %} {% block main_content %}
|
{% extends "base.html" %} {% block main_content %}
|
||||||
<div class="container-fluid">
|
<script>
|
||||||
<h3 class="offset-lg-3">{{page_title}}</h3>
|
// Define this once and before it will be called, hence at the top of this file
|
||||||
<form id="pfm" class="form form-inline" action="" method="POST">
|
function DrawRefimg(img, canvas, orig_face )
|
||||||
{% for field in form %}
|
{
|
||||||
{% if field.type == 'HiddenField' or field.type == 'CSRFTokenField' %}
|
// FIXME: should get this from shared.py, not sure why this doesnt work at present
|
||||||
{{field}}<br>
|
thumbsize=256
|
||||||
{% elif field.type != 'SubmitField' %}
|
|
||||||
<div class="form-row col-lg-12">
|
context=canvas.getContext('2d')
|
||||||
{{ field.label( class="col-lg-3" ) }}
|
// another call to this func will occur on load, so skip this one
|
||||||
{{ field( class="form-control col" ) }}
|
if( img.width == 0 )
|
||||||
</div class="form-row col-lg-12">
|
return
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
// only set canvas.width once we have valid img dimensions
|
||||||
<div class="form-row col-lg-12">
|
canvas.width=img.width/2
|
||||||
<span class="col-lg-3"><center>Reference Images:</center></span>
|
|
||||||
{% for ref_img in person.refimg %}
|
// actually draw the pixel images to the canvas at the right size
|
||||||
{% set offset="" %}
|
context.drawImage(img, 0, 0, img.width/(img.height/canvas.height), canvas.height);
|
||||||
{% if (loop.index % 10) == 0 %}
|
|
||||||
{% set offset= "offset-lg-3" %}
|
// draw rectangle on face
|
||||||
{% endif %}
|
context.beginPath();
|
||||||
<div id="RI{{ref_img.id}}" class="px-0 col-lg-1 w-100 {{offset}}">
|
new_x=(orig_face.x/orig_face.orig_w)*img.width/(img.height/canvas.height)
|
||||||
<input type="hidden" id="ref-img-id-{{ref_img.id}}" name="ref-img-id-{{ref_img.id}}" value="1"></input>
|
new_y=(orig_face.y/orig_face.orig_h)*thumbsize/(img.height/canvas.height)
|
||||||
<figure style="border: 1px solid #5bc0de; border-radius: 3px;" class="figure my-auto h-100 w-100">
|
new_w=(orig_face.w/orig_face.orig_w)*img.width/(img.height/canvas.height)
|
||||||
<div style="position:relative">
|
new_h=(orig_face.h/orig_face.orig_h)*thumbsize/(img.height/canvas.height)
|
||||||
<center><img height="128" class="thumb" src="data:image/jpeg;base64,{{ref_img.thumbnail}}"></img></center>
|
context.rect(new_x, new_y, new_w, new_h)
|
||||||
<div style="position:absolute; top: 2; right: 2;">
|
context.lineWidth = 2;
|
||||||
<button type="button" style="font-size:12px" class="btn btn-danger"
|
context.strokeStyle = 'green';
|
||||||
onClick="DelImg({{ref_img.id}})">X</button>
|
context.stroke();
|
||||||
</div>
|
}
|
||||||
<figcaption class="figure-caption text-center text-wrap text-break">{{ref_img.fname}}</figcaption>
|
</script>
|
||||||
</div>
|
|
||||||
</figure>
|
<div class="container-fluid">
|
||||||
</div id="/RI*">
|
<h3 class="offset-lg-3">{{page_title}}</h3>
|
||||||
{% endfor %}
|
<form id="pfm" class="form form-inline" action="" method="POST">
|
||||||
</div class="form-row col-lg-12">
|
{% for field in form %}
|
||||||
<div class="form-row col-lg-12">
|
{% if field.type == 'HiddenField' or field.type == 'CSRFTokenField' %}
|
||||||
<br>
|
{{field}}<br>
|
||||||
</div class="form-row">
|
{% elif field.type != 'SubmitField' %}
|
||||||
<div class="form-row col-lg-12">
|
<div class="form-row col-lg-12">
|
||||||
{{ form.save( id="save", class="btn btn-primary offset-lg-3 col-lg-2" )}}
|
{{ field.label( class="col-lg-3" ) }}
|
||||||
{% if 'Edit' in page_title %}
|
{{ field( class="form-control col" ) }}
|
||||||
{{ form.delete( class="btn btn-outline-danger col-lg-2" )}}
|
</div class="form-row col-lg-12">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div class="form-row">
|
{% endfor %}
|
||||||
</form>
|
<div class="form-row col-lg-12">
|
||||||
<form id="new_ri" class="form" action="{{url_for('add_refimg')}}" method="POST" enctype="multipart/form-data">
|
<span class="col-lg-3"><center>Reference Images:</center></span>
|
||||||
<input type="hidden" name="person_id" value="{{person.id}}"></input>
|
{% for refimg in person.refimg %}
|
||||||
<label class="btn btn-success offset-lg-3 col-lg-2">
|
{% set offset="" %}
|
||||||
Add reference image
|
{% if (loop.index % 10) == 0 %}
|
||||||
<input name="refimg_file" type="file" onChange="$('#new_ri').submit()" style="display:none;" id="new_file_chooser">
|
{% set offset= "offset-lg-3" %}
|
||||||
</label>
|
{% endif %}
|
||||||
</form>
|
<div id="RI{{refimg.id}}" class="px-0 col-lg-1 w-100 {{offset}}">
|
||||||
</div class="row">
|
<center>
|
||||||
</div class="container">
|
<input type="hidden" id="ref-img-id-{{refimg.id}}" name="ref-img-id-{{refimg.id}}" value="1"></input>
|
||||||
|
<figure style="border: 1px solid #5bc0de; border-radius: 3px;" class="figure my-auto h-100 w-100">
|
||||||
|
<div style="position:relative">
|
||||||
|
<canvas id="c_{{refimg.id}}" height="128"></canvas>
|
||||||
|
<script>
|
||||||
|
var im_{{refimg.id}}=new Image();
|
||||||
|
im_{{refimg.id}}.src="data:image/jpeg;base64,{{refimg.thumbnail}}";
|
||||||
|
|
||||||
|
// store this stuff in an javascript Object to use when document is ready event is triggered
|
||||||
|
var orig_face_{{refimg.id}}=new Object;
|
||||||
|
orig_face_{{refimg.id}}.x = {{refimg.face_locn[0][3]}}
|
||||||
|
orig_face_{{refimg.id}}.y = {{refimg.face_locn[0][0]}}
|
||||||
|
orig_face_{{refimg.id}}.w = {{refimg.face_locn[0][1]}}-{{refimg.face_locn[0][3]}}
|
||||||
|
orig_face_{{refimg.id}}.h = {{refimg.face_locn[0][2]}}-{{refimg.face_locn[0][0]}}
|
||||||
|
orig_face_{{refimg.id}}.orig_w = {{refimg.orig_w}}
|
||||||
|
orig_face_{{refimg.id}}.orig_h = {{refimg.orig_h}}
|
||||||
|
|
||||||
|
// when the document is ready, then DrawRefimg
|
||||||
|
$(function() { DrawRefimg(im_{{refimg.id}}, c_{{refimg.id}}, orig_face_{{refimg.id}} ) });
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div style="position:absolute; top: 2; right: 2;">
|
||||||
|
<button type="button" style="font-size:12px" class="btn btn-danger"
|
||||||
|
onClick="DelImg({{refimg.id}})">X</button>
|
||||||
|
</div>
|
||||||
|
<figcaption class="figure-caption text-center text-wrap text-break">{{refimg.fname}}</figcaption>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
</center>
|
||||||
|
</div id="/RI*">
|
||||||
|
{% endfor %}
|
||||||
|
</div class="form-row col-lg-12">
|
||||||
|
<div class="form-row col-lg-12">
|
||||||
|
<br>
|
||||||
|
</div class="form-row">
|
||||||
|
<div class="form-row col-lg-12">
|
||||||
|
{{ form.save( id="save", class="btn btn-primary offset-lg-3 col-lg-2" )}}
|
||||||
|
{% if 'Edit' in page_title %}
|
||||||
|
{{ form.delete( class="btn btn-outline-danger col-lg-2" )}}
|
||||||
|
{% endif %}
|
||||||
|
</div class="form-row">
|
||||||
|
</form>
|
||||||
|
{% if person.id %}
|
||||||
|
<form id="new_ri" class="form" action="{{url_for('add_refimg')}}" method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="person_id" value="{{person.id}}"></input>
|
||||||
|
<label class="btn btn-success offset-lg-3 col-lg-2">
|
||||||
|
Add reference image
|
||||||
|
<input name="refimg_file" type="file" onChange="$('#new_ri').submit()" style="display:none;" id="new_file_chooser">
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div class="container">
|
||||||
{% endblock main_content %}
|
{% endblock main_content %}
|
||||||
|
|
||||||
{% block script_content %}
|
{% block script_content %}
|
||||||
<script>
|
<script>
|
||||||
function DelImg(ri_num)
|
function DelImg(ri_num)
|
||||||
{
|
{
|
||||||
$('#RI'+ri_num).remove()
|
$('#RI'+ri_num).remove()
|
||||||
$('#pfm').submit()
|
$('#pfm').submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock script_content %}
|
{% endblock script_content %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user