...
 
Commits (2)
*~
db
db/
external*
photos
debian
photos/
debian/
__pycache__/
No preview for this file type
#!/usr/bin/env python3
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "photodb.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)
"""
Django settings for photodb project.
Generated by 'django-admin startproject' using Django 1.11.13.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'a0gse8(@w8*1gfzzkgvfjg925s1#kv(f#2mi-_mz$yf_!c76a7'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1',]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'pose',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'photodb.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'photodb.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db', 'names.db'),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 7,
},
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
"""photodb URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'', include('pose.urls')),
]
"""
WSGI config for photodb project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "photodb.settings")
application = get_wsgi_application()
from django.contrib import admin
from .models import Person
admin.site.register(Person)
from django.apps import AppConfig
class PoseConfig(AppConfig):
name = 'pose'
#!/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])
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-20 09:46
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Person',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('firstName', models.CharField(max_length=30)),
('lastName', models.CharField(max_length=30)),
('photo', models.ImageField(blank=True, null=True, upload_to='')),
('modifDate', models.DateTimeField(blank=True, null=True)),
('level', models.IntegerField(blank=True, choices=[(4, 'Seconde'), (5, 'Première'), (6, 'Terminale'), (7, 'Supérieur1'), (8, 'Supérieur2')], null=True)),
('className', models.CharField(blank=True, max_length=10, null=True)),
],
),
migrations.AlterUniqueTogether(
name='person',
unique_together=set([('firstName', 'lastName')]),
),
]
from django.db import models
LEVELS = (
(4, 'Seconde'),
(5, 'Première'),
(6, 'Terminale'),
(7, 'Supérieur1'),
(8, 'Supérieur2'),
)
class Person(models.Model):
firstName = models.CharField(max_length=30)
lastName = models.CharField(max_length=30)
photo = models.ImageField(blank=True, null=True)
modifDate = models.DateTimeField(blank=True, null=True)
level = models.IntegerField(choices=LEVELS, blank=True, null=True)
className = models.CharField(max_length=10, blank=True, null=True)
class Meta:
unique_together = (("firstName", "lastName"),)
def __str__(self):
return "{f} {l}".format(f=self.firstName, l=self.lastName)
......@@ -64,6 +64,7 @@ function findFace(){
photo: photodata,
nom: $("#nom").val(),
prenom: $("#prenom").val(),
csrfmiddlewaretoken: $("#csrf").val(),
}).done(function(data){
if (data.status){
face.attr({
......@@ -106,6 +107,7 @@ function envoyer(){
prenom: prenom,
nom: nom,
photo: photodata,
csrfmiddlewaretoken: $("#csrf").val(),
},
}).done(
function(data){
......
{% load static %}
<!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"/>
<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>
<body>
<input type="hidden" id="csrf" value="{{csrf_token}}"/>
<div id="maincontent">
<form id="theform" action="#" method="post" onsubmit="return envoyer();">
<h3>Portrait</h3>
......
from django.test import TestCase
# Create your tests here.
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^encadre$', views.encadre, name='encadre'),
url(r'^envoi$', views.envoi, name='envoi'),
]
from django.shortcuts import render
from django.http import JsonResponse
from django.utils import timezone
from .models import Person
from .autoretouche import jpgPrefix, FaceImage
from photodb.settings import BASE_DIR
import re, os, base64, uuid
def json_response(F):
"""
decorator for any function which returns a json-serializable data
@param F a function with profile
(request, *args, **kwargs) -> json-serializable
return the result of the decorator
"""
def result(request, *args, **kwargs):
data=F(request, *args, **kwargs)
return JsonResponse(data)
return result
def index(request):
return render(request, 'pose/index.html', {})
@json_response
def encadre(request):
fi = FaceImage(request.POST['photo'].encode("utf-8"))
p=list(Person.objects.filter(
firstName=request.POST['prenom'],
lastName=request.POST['nom'],
))
message=""
oldimage=""
if not p:
message="Inconnu(e) dans la base"
cssclass="red"
else:
message="Trouvé(e) dans la base"
cssclass="green"
photo=p[0].photo
if photo:
photo=open(os.path.join(BASE_DIR,"photos",str(photo)),'rb').read()
photo=jpgPrefix+base64.b64encode(photo)
oldimage=photo.decode("ascii")
message="Trouvé(e) avec la photo"
cssclass="orange"
return {
"status": fi.ok,
"rect": fi.cropRect,
"message": message,
"oldimage": oldimage,
"cssclass": cssclass,
}
def protect(s):
"""
prepare a name to be compatible with every file system
@ a string with no spaces, no diacritics, etc.
"""
return re.sub(r'[^A-Za-z0-9_\-]','_',s)
def nommage(nom, prenom):
"""
return a unique file name based on two strings
@param nom surname
@param prenom given name
@return a unique filename
"""
result=protect(nom)+'_'+protect(prenom)
result=result[:20]+'_'+str(uuid.uuid1())+'.jpg'
return result
@json_response
def envoi(request):
"""
callback page to deal with first and second name, plus an image
request.POST should contain those keys:
nom, prenom, photo
@return dictionary with those keys: status and message; when
status is not "ko", two other keys are given: fname and base64
"""
keys=('nom','prenom','photo')
# give an error message when one parameter is missing or empty
missingKeys=[k for k in keys if k not in request.POST]
missingVals=[k for k in request.POST if not request.POST[k]]
if missingKeys + missingVals:
return {
"status": "ko",
"message": "appel erroné, paramètres incorrects: {l}".format(l=",".join(missingKeys + missingVals)),
}
fi = FaceImage(request.POST['photo'].encode("utf-8"))
#### default return components, when no face is detected ####
status="malretouche"
fname=""
base64=fi.toDataUrl
message="""<p>Le système détecte mal le visage à recadrer.</p>
<p>Veuillez refaire la photo.</p>
"""
if fi.ok:
# a face was detected, good!
fname=nommage(request.POST['nom'],request.POST['prenom'])
fi.saveAs(os.path.join(BASE_DIR,'photos',fname))
p=list(Person.objects.filter(
firstName=request.POST['prenom'],
lastName=request.POST['nom'],
))
if not p:
# the user does not exist so far: create a new entry
status="nouveau"
message="""
<p>Nouvel enregistrement créé pour {nom} {prenom}</p>
""".format(**request.POST)
p=Person(
firstName=request.POST['prenom'],
lastName=request.POST['nom'],
photo=fname,
modifDate=timezone.now()
)
p.save()
else:
p=p[0]
photo=p.photo
# the user already exists, make an update
status="ok"
message="""
<p>Enregistrement de la photo effectué pour {nom} {prenom}</p>
""".format(**request.POST)
if photo: # erase an earlier photo file
moremessage="<p>L'ancienne photo n'existait pas : erreur ?</p>"
try:
os.unlink(os.path.join(BASE_DIR,'photos',str(photo)))
moremessage="<p>L'ancienne photo a été effacée</p>"
except:
pass
message+=moremessage
# the update is made in either case, even if there was no photo
p.photo=fname
p.modifDate=timezone.now()
p.save()
# in any case, return status, fname, base64 message
return {
"statut": "ok",
"fname": fname,
"base64": fi.toDataUrl,
"message": message,
}
This diff is collapsed.