Commit dfc3bd5f authored by Georges Khaznadar's avatar Georges Khaznadar

Import Upstream version 1.5

parents
*~
db
external*
photos
debian
__pycache__/
DESTDIR =
SERVERDIR = $(DESTDIR)/var/lib/photodb
BINDIR = $(DESTDIR)/usr/bin
all:
clean:
find . -name "*~" | xargs rm -f
find . -name "__pycache__" | xargs rm -rf
install:
install -d $(SERVERDIR)
cp *.py cherryApp.conf nobody.jpg updateDb.sh haarcascade_frontalface_default.xml $(SERVERDIR)
cp -a static templates $(SERVERDIR)
# fix the cherrypy configuration
sed -i 's%tools.staticdir.root.*%tools.staticdir.root = "/var/lib/photodb"%' $(SERVERDIR)/cherryApp.conf
install -d $(BINDIR)
cp photodbServer $(BINDIR)
.PHONY: all clean install
#!/usr/bin/python3
"""
Reconnaissance faciale, recadrage et harmonisation des photos du trombinoscope
Jean-Bart
Dépendances logicielles:
python3-opencv, python3-numpy
"""
import cv2, sys, math, os, os.path
import numpy as np
from PIL import Image
from io import BytesIO
import base64
thisdir=os.path.dirname(__file__)
jpgPrefix=b'data:image/jpeg;base64,'
class FaceImage(object):
"""
a class to implement an image with face detection
"""
__CASCADE = cv2.CascadeClassifier(os.path.join(
thisdir,"haarcascade_frontalface_default.xml"))
def __init__(self, indata, size=(150,192)):
"""
the constructor
@param indata either a file name for an image or a bytes with
an URL-encoded image
@param size the size (width, height) of the cropped face to get
"""
self.photo=None # should become a cv2 image
self.size=size
self.cropRect={} # should become ("x":x, "y":y, "w":w, "h":h)
self.cropped=None # should become a cv2 image
self.ok=False # will become True when a face is detected
if type(indata) == str and os.path.exists(indata):
self.photo=cv2.imread(indata)
elif type(indata) == bytes and indata[:len(jpgPrefix)] == jpgPrefix:
photo=BytesIO(base64.b64decode(indata[len(jpgPrefix):]))
photo=np.array(Image.open(photo))
self.photo=cv2.cvtColor(photo, cv2.COLOR_BGR2RGB)
else:
raise Exception("Could not get a photo.")
self.crop()
return
def crop(self):
"""
Tries to find a face in self.photo, then crops it if possible
into self.cropped and puts the status into self.ok
"""
height, width = self.photo.shape[:2]
gray=cv2.cvtColor(self.photo, cv2.COLOR_BGR2GRAY)
faces = self.__CASCADE.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30),
flags = cv2.CASCADE_SCALE_IMAGE
)
if len(faces)==0 or len(faces)>1:
self.ok=False
self.cropped=self.photo
else:
self.ok=True
x, y, w, h = faces[0]
R=self.size[1]/self.size[0]
if h/w < R: # the face rectangle is not high enough
y=int((R-1)*h/2)
h=int(R*w) # so h/w is quite R
# int() casts are necessary since cv2 uses int32 which
# is not JSON serializable to communicate with Javascript
self.cropRect = {"x": int(x), "y": int(y), "w": int(w), "h": int(h)}
# calculate 'a' such as 25a * 32a equals the area 2 * w * h
a=math.sqrt(2*w*h/800)
# upper left coordinates
x1=round(x+w/2-12.5*a); y1=round(y+7/12*h-16*a)
x1=int(max(x1,0)); y1=int(max(y1,0))
# lower right coordinates
x2=round(x+w/2+12.5*a);y2=round(y+7/12*h+16*a)
x2=int(min(x2,width)); y2=int(min(y2,height))
crop_img=self.photo[y1:y2, x1:x2]
# try to normalize Hue, Saturation, Value
hsv=cv2.cvtColor(crop_img,cv2.COLOR_RGB2HSV)
h,s,v = cv2.split(hsv)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4,4))
h1=h # same hue
s1=s # same saturation
v1 = clahe.apply(v) # normalize the value
hsv1=cv2.merge((h1,s1,v1))
newimg=cv2.cvtColor(hsv1, cv2.COLOR_HSV2RGB)
self.cropped = cv2.resize(newimg, self.size)
return
@property
def toDataUrl(self):
"""
returns a DataUrl unicode string with self.cropped as an image
"""
data=cv2.imencode(".jpg",self.cropped)[1].tostring()
return (jpgPrefix+base64.b64encode(data)).decode("ascii")
def saveAs(self, path):
"""
saves self.cropped into a file
@param path a path to the file system
"""
cv2.imwrite(path, self.cropped)
return
if __name__=="__main__":
fi=FaceImage(sys.argv[1])
fi.saveAs(sys.argv[2])
[global]
server.socket_port = 8901
log.screen = False
[/]
tools.staticdir.root = "/home/georgesk/developpement/photodb/photodb-1.5"
[/static]
tools.staticdir.on = True
tools.staticdir.dir = "static"
#!/usr/bin/python3
import sys, csv, re, datetime
import sqlite3
encodingList=[
"utf-8",
"latin1",
"dos",
"cp437",
]
secondNamePattern=re.compile(r"^nom$", re.I)
firstNamePattern=re.compile(r"^pr[eé]nom$", re.I)
connection=None # connection to a sqlite3 database
def find_encoding(f, encodings=encodingList):
"""
finds the encoding of a text file
"""
for e in encodings:
try:
with open(f,encoding=e) as infile:
s=infile.read()
return e
except:
pass
return None
def getReader(csvfile):
"""
makes a DictReader from the file f, with the given encoding.
This Dictrader must have one field with name matched by secondNamePattern
and another one with name matched by firstNamePattern
@param csvfile an open string stream
@return a Dictreader, and the names of fields for the surname and
the given name
"""
reader=None
for delimiter in (";", ",", "\t",):
csvfile.seek(0)
reader = csv.DictReader(csvfile, delimiter=delimiter)
fields2=[f for f in reader.fieldnames if secondNamePattern.match(f)]
fields1=[f for f in reader.fieldnames if firstNamePattern.match(f)]
if (len(fields2), len(fields1)) == (1, 1):
return reader, fields2[0], fields1[0]
return reader, "", ""
def addToDb(row, field2, field1, verbose=False):
"""
adds surname and given name records to the database.
"""
date=datetime.datetime.utcnow().isoformat(sep=' ', timespec='seconds')
c = connection.cursor()
surname=row[field2]
givenname=row[field1]
c.execute("select * from person where surname=:surname and givenname=:givenname",
{"surname": surname, "givenname": givenname})
if c.fetchone()==None:
## the key surname + givenname does not yet exist !
print("+",end="")
sys.stdout.flush()
c.execute("INSERT INTO person (surname, givenname, date) VALUES (?,?,?)",
(surname, givenname, date))
connection.commit()
return 1
else:
if verbose:
print("-",end="")
sys.stdout.flush()
return 0
if __name__=="__main__":
infile=sys.argv[1]
try:
outfile=sys.argv[2]
except:
outfile="db/names.db"
connection = sqlite3.connect(outfile)
c = connection.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS person
(surname text, givenname text, photo text, date text)''')
connection.commit()
encoding=find_encoding(infile)
written=0
with open(infile, encoding=encoding) as csvfile:
reader, field2, field1 = getReader(csvfile)
for r in reader:
written=written + addToDb(r, field2, field1, verbose=True)
print ("\n{} ==>{}\nwritten {} records".format(infile,outfile,written))
File added
This diff is collapsed.
nobody.jpg

1.69 KB

#!/bin/sh
user=$(id -un)
if [ "$user" = "www-data" ]; then
cd /var/lib/photodb
exec python3 webretouche.py
else
sudo su www-data --shell /bin/sh --command "$0"
fi
#!/usr/bin/python3
import sqlite3, sys, os
from subprocess import call
def unquote(s):
if len(s)<2: return s
elif s[0]=='"' and s[-1]=='"': return s[1:-1]
elif s[0]=="'" and s[-1]=="'": return s[1:-1]
else: return s
def protege(s):
return s.replace(" ","-").replace("'","-")
def sansAccent(s):
accent={'a':'àâ','e':"éèêë","i":"îï","o":"ô","u":"ù","c":"ç",'A':'ÀÂ',
'E':"ÉÈÊË","I":"ÎÏ","O":"Ô","U":"Ù","C":"Ç"}
r=""
for c in s:
ok=False
for d in accent.keys():
if c in accent[d]:
r+=d
ok=True
if not ok:
r+=c
return r
def usage():
print("""Usage : {} <nom de répertoire>
Exporte des photos au format demandé par pronote dans un répertoire
""".format(sys.argv[0]))
return
if __name__=="__main__":
outdir=None # répertoire où envoyer les résultats
if len(sys.argv)<2:
usage()
sys.exit(1)
else:
outdir=sys.argv[1]
os.makedirs(outdir, mode=0o755, exist_ok=True)
conn=sqlite3.connect("/var/lib/photodb/db/names.db")
c=conn.cursor()
c.execute("select * from person")
for p in c:
nom=p[0]
prenom=p[1]
if p[2] and nom and prenom:
fichier=p[2].replace("photos/","")
nouveauFichier=protege(sansAccent(prenom)).lower()+"."+protege(sansAccent(nom)).lower()
cmd="cp /var/lib/photodb/photos/%s %s/%s.jpg" %(fichier, outdir, nouveauFichier)
call(cmd, shell=True)
/usr/share/javascript/jquery-ui
\ No newline at end of file
/usr/share/javascript/jquery-ui-themes
\ No newline at end of file
/usr/share/javascript/jquery/jquery.js
\ No newline at end of file
This diff is collapsed.
#webcam{
margin: 2em auto 0;
width: 320px;
height: 240px;
padding: 2em;
background: white;
box-shadow: 0 1px 10px #444444;
transform: rotateY(180deg);
-webkit-transform:rotateY(180deg); /* Safari and Chrome */
-moz-transform:rotateY(180deg); /* Firefox */
z-index:1;
}
#svgContainer{
margin: 2em auto 0;
width: 320px;
height: 240px;
padding: 2em;
position:absolute;
z-index:2;
}
.ui-menu-item {
font-size: 10px;
}
#theform {
float: right;
width:350px;
}
#theform h1,h2,h3 {
text-align: center;
}
#otherimage {
height: 2em;
vertical-align: middle;
padding: 0 1em;
}
#otherimage:hover {
height: 150px;
vertical-align: middle;
padding: 0 1em;
}
#message {
padding: 0 0.5em;
border-radius: 0.5em;
}
#message.orange {
background: orange;
color: white;
}
#message.red {
background: red;
color: white;
}
#message.green {
background: green;
color: black;
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<title>Saisie de portrait</title>
<script src="/static/jquery.js" type="text/javascript"></script>
<link rel="stylesheet" href="/static/jquery-ui-themes/smoothness/jquery-ui.css"/>
<script src="/static/jquery-ui/jquery-ui.js" type="text/javascript"></script>
<script type="text/javascript" src="/static/portrait.js"></script>
<link rel="stylesheet" href="/static/portrait.css" type="text/css"/>
</head>
<body>
<div id="maincontent">
<form id="theform" action="#" method="post" onsubmit="return envoyer();">
<h3>Portrait</h3>
<fieldset>
<legend>Identité</legend>
<table>
<tr>
<th>Nom :</th>
<td><input type="text" name="nom" id="nom"/></td>
</tr>
<tr>
<th>Prénom :</th>
<td><input type="text" name="prenom" id="prenom"/></td>
</tr>
<tr>
<td colspan="2">
<img id="otherimage" src="" alt=""/>
<span id="message"></span>
</td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="Enregistrer" /></td>
</tr>
</table>
</fieldset>
</form>
<div id="videoLive">
<svg id="svgContainer">
<rect id="face" x="-100" y="-100" width="0" height="0"
style="fill: white; fill-opacity: 0;
stroke: blue; stroke-width: 2; stroke-opacity: 1"/>
<rect id="mask-top" x="0" y="0" width="320" height="10"
style="fill:white; fill-opacity:0.5;"/>
<rect id="mask-bottom" x="0" y="230" width="320" height="10"
style="fill:white; fill-opacity:0.5;"/>
<rect id="mask-left" x="0" y="10" width="75" height="220"
style="fill:white; fill-opacity:0.5;"/>
<rect id="mask-right" x="245" y="10" width="75" height="220"
style="fill:white; fill-opacity:0.5;"/>
<rect id="frame" x="75" y="10" width="170" height="220"
style="fill:white; fill-opacity:0;
stroke:pink; stroke-width:5; stroke-opacity:0.5" />
</svg>
<video id="webcam" autoplay ></video>
</div>
</div>
<div id="dialog"></div>
</body>
</html>
<!--
Local Variables:
mode: nxml
End:
-->
function onFailure(err) {
alert("Erreur : " + err.name + ", " + err.message);
}
var c = document.createElement("canvas"); // an invisible canvas for computations
var message;
var otherimage;
var video;
var face;
jQuery(document).ready(function () {
// init global variables
message=$("#message");
otherimage=$("#otherimage");
video = document.querySelector('#webcam');
face=$($("#svgContainer")[0].children[0])
// make overlaied objects
var w=$("#webcam");
$("#svgContainer").offset(w.offset());
// start the video
var constraints={video: {width:320, height:240}};
navigator.mediaDevices.getUserMedia(constraints).then(
function (localMediaStream) {
video.srcObject = localMediaStream;
video.onloadedmetadata = function(e) {video.play();};
}).catch(
function(err) {
alert("Erreur : " + err.name + ", " + err.message);
});
// start the fast interactive loop
setTimeout(findFace,0);
$("#nom").autocomplete({
minLength: 3,
source: function(term, callback) {
$.getJSON("/chercheNom", term, callback);
},
// effacer le prénom pendant qu'on bricole le nom !
// et aussi : remttre en saisie de vidéo
search: function( event, ui ) {$("#prenom").val("")},
change: function( event, ui ) {$("#prenom").val("");},
});
$("#prenom").autocomplete({
source: function(term, callback) {
$.getJSON("/cherchePrenom", {nom: $("#nom").val(), prenom: $("#prenom").val()}, callback);
},
});
});
/**
* tries to find faces twice a second
**/
function findFace(){
c.width=video.videoWidth;
c.height = video.videoHeight;
var ctx=c.getContext('2d');
ctx.drawImage(video,0,0,320,240);
var photodata=c.toDataURL("image/jpeg");
$.post("/encadre",{
photo: photodata,
nom: $("#nom").val(),
prenom: $("#prenom").val(),
}).done(function(data){
if (data.status){
face.attr({
x: 320-data.rect.x-data.rect.w, // because of the symmetry
y: data.rect.y,
width: data.rect.w,
height: data.rect.h
});
} else {
face.attr({x: "-100", y: "-100", width: "0", height: "0"});
}
if (data.message){
message.text(data.message);
message.attr("class",data.cssclass)
message.show();
} else {
message.hide();
}
if (data.oldimage){
otherimage.attr({src: data.oldimage});
otherimage.show();
} else {
otherimage.hide();
}
});
setTimeout(findFace,333);
}
function envoyer(){
var nom=$("#nom").val();
var prenom=$("#prenom").val();
c.width=video.videoWidth;
c.height = video.videoHeight;
var ctx=c.getContext('2d');
ctx.drawImage(video,0,0,320,240);
var photodata=c.toDataURL("image/jpeg");
$.ajax("/envoi",{
type: "POST",
data:{
prenom: prenom,
nom: nom,
photo: photodata,
},
}).done(
function(data){
$("#dialog").html(
"<img align='right' src='"+data["base64"]+"'/>" +
data.message
);
$("#dialog").dialog({
width: 500,
buttons: [
{
text: "OK",
icons: {
primary: "ui-icon-heart"
},
click: function() {
$( this ).dialog( "close" );
}
},
]
});
}
)
return false; // never complete the submit action
}
<html>
<head>
<script type="text/javascript" src="/static/jquery.js"></script>
<script type="text/javascript" src="/static/la-joconde.js"></script>
<script type="text/javascript" src="/static/test.js"></script>
</head>
<body>
<h1>Hello World! Here is the face normalizing service</h1>
<h2>Original image</h2>
<img id="img1"/>
<h2>Reworked image</h2>
<img id="img2" />
</body>
</html>
window.onload = function(){
var img1=document.querySelector("#img1");
var img2=document.querySelector("#img2");
img1.src = la_joconde;
$.post("retouche",{
data: la_joconde,
}).done(
function(data){
if (data.status=="OK"){
img2.src=data.imgdata;
}
}
);
};
#!/usr/bin/python3
"""
Publications de paquets de 36 photos par page,
les photos sont prêtes à être imprimées avec un texte sous chacune
"""
from io import BytesIO, StringIO
import zipfile
import xml.dom.minidom
import os
import hashlib
def hashImageFileName(f):
"""
prend un nom de fichier qui se termine par .jpg et renvoie un nom de fichier
que LibreOffice peut prendre comme fichier d'image
"""
root, ext = os.path.splitext(f)
root=root.encode("utf-8")
return "Pictures/000000000000096000000"+hashlib.md5(root).hexdigest()[:18].upper()+ext
def pageGen(template="templates/modele0.odt", data=[], title="Joli titre"):
"""
Crée un fichier temporaire au format ODT
@param template un fichier ODT comportant 36 places de tableau
@param data une liste contenant au plus 36 photos et phrases
@param title un titre pour la page
@return un BytesIO contenant une page au format ODT
"""
modele=zipfile.ZipFile(template)
contents=modele.open("content.xml")
zipIO=BytesIO()
result=zipfile.ZipFile(zipIO,"x")
for zinf in modele.infolist():
f=zinf.filename
if f!="content.xml":
result.writestr(f,modele.read(zinf))
page=xml.dom.minidom.parse(contents)
print(page.toprettyxml(indent=" "))
cells=page.getElementsByTagName("table:table-cell")
title0=cells[0].getElementsByTagName("text:p")[0]
title0.firstChild.replaceWholeText(title)
i=1
for photo, texte in data:
# insertion de la photo
image=cells[i].getElementsByTagName("draw:image")[0]
target=hashImageFileName(photo)
image.setAttribute("xlink:href", target)
with open(photo,"rb") as infile:
result.writestr(target, infile.read())
# insertion du texte
t=cells[i].getElementsByTagName("text:s")[0].nextSibling
t.replaceWholeText(texte)
i+=1
# limitation à 36 cases de tableau !
if i > 36:
break
newcontents=StringIO()
page.writexml(newcontents)
newcontents.seek(0)