Skip to content

Commit

Permalink
v0.1.3
Browse files Browse the repository at this point in the history
Enhancements
- util.random_string more human readable
- Create method for wrap the response data and construct the response

Issue #8 Manage face recognition among multiple faces in the same image
- Allow the possibility to receive an image with more than one face
- Apply face recognition logic among every faces found
- Apply machine learning technique to the faces encoded
- Return [{person: distance}] as data into response

Issue #12 [Owasp] Add security headers
- util.secure_request is now delegated to set the security headers into the response
- @app.after_request wrapper used for secure every response request
  • Loading branch information
alessiosavi committed May 23, 2019
1 parent a9f8bb3 commit 4b539ab
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 37 deletions.
4 changes: 1 addition & 3 deletions api/Api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ def predict_image(img_path, clf, PREDICTION_PATH):
log.debug("predict_image | Generated a random name: {}".format(img_name))
log.debug("predict_image | Visualizing face recognition ...")
print_prediction_on_image(img_path, prediction["predictions"], PREDICTION_PATH, img_name)
return Response(status="OK", description=img_name, data={"name": prediction["predictions"][0][0],
"distance": prediction[
"score"]}).__dict__
return Response(status="OK", description=img_name, data=prediction).__dict__

# Manage error
elif prediction is None:
Expand Down
2 changes: 1 addition & 1 deletion conf/test.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"PyRecognizer": {
"Version": "0.1.2",
"Version": "0.1.3",
"temp_upload_training": "uploads/training/",
"temp_upload_predict": "uploads/predict/",
"temp_upload": "uploads/upload"
Expand Down
41 changes: 23 additions & 18 deletions datastructure/Classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ def tuning(self, X, Y, timestamp):
power_range = [1, 2]
nn_root = int(round(sqrt(len(X_train))))
parameter_space = {
# 'n_neighbors': list(range(1,nn_root)),
'n_neighbors': [nn_root],
'metric': metrics_range,
'weights': weights_range,
Expand Down Expand Up @@ -233,7 +232,6 @@ def dump_model(self, timestamp, classifier, params=None, path=None):
classifier_file = os.path.join(classifier_folder, "model")

log.debug("dump_model | Dumping model ... | Path: {} | Model folder: {}".format(path, timestamp))
# TODO: Save every model in a different folder
if not os.path.exists(classifier_folder):
os.makedirs(classifier_folder)

Expand Down Expand Up @@ -266,8 +264,6 @@ def init_peoples_list(self, peoples_path=None):

self.peoples_list = list(filter(None.__ne__, self.peoples_list)) # Remove None

# TODO: Add method for dump datastructure in order to don't wait to load same data for test

def init_peoples_list_core(self, people_name):
"""
Delegated core method for parallelize operation
Expand Down Expand Up @@ -318,11 +314,12 @@ def predict(self, X_img_path, distance_threshold=0.54):
"""

if self.classifier is None:
log.error("predict | Be sure that you have loaded/trained a nerual model")
log.error("predict | Be sure that you have loaded/trained the nerual network model")
return None

# Load image file and find face locations
# Load image data in a numpy array
try:
log.debug("predict | Loading image {}".format(X_img_path))
# TODO: Necessary cause at this point we are not sure what file type is this ...
X_img = face_recognition.load_image_file(X_img_path)
except OSError:
Expand All @@ -337,24 +334,32 @@ def predict(self, X_img_path, distance_threshold=0.54):
if len(X_face_locations) == 0:
log.warning("predict | Seems that no faces was found :( ")
return []
log.debug("predict | Found more than one face, encoding the faces ...")

# Find encodings for faces in the test iamge
faces_encodings = face_recognition.face_encodings(X_img, known_face_locations=X_face_locations)
log.debug("predict | Face encoded! Let's ask to the neural network ...")
log.debug("predict | Encoding faces ...")
faces_encodings = face_recognition.face_encodings(X_img, known_face_locations=X_face_locations, num_jitters=2)
log.debug("predict | Face encoded! | Let's ask to the neural network ...")
# Use the KNN model to find the best matches for the test face
closest_distances = self.classifier.kneighbors(faces_encodings)
log.debug("predict | Closest distances: {}".format(closest_distances))
min_distance = min(closest_distances[0][0])
log.debug("predict | Min: {}".format(min_distance))
log.debug("predict | Closest distances: [{}]".format(len(closest_distances)))
# At least we need to recognize 1 face
scores = []
for i in range(len(closest_distances[0])):
scores.append(min(closest_distances[0][i]))
log.debug("predict | *****MIN****| {}".format(min(closest_distances[0][i])))


predictions = []
if min_distance < distance_threshold:
for pred, loc in zip(self.classifier.predict(faces_encodings), X_face_locations):
log.debug("predict_folder | Pred: {} | Loc: {}".format(pred, loc))
predictions.append((pred, loc))
log.debug("predict_folder | Prediction: {}".format(predictions))
if len(scores) > 0:
for pred, loc, score in zip(self.classifier.predict(faces_encodings), X_face_locations, scores):
if score <= distance_threshold:
log.debug("predict | Pred: {} | Loc: {} | Score: {}".format(pred, loc, score))
predictions.append((pred, loc))
else:
log.warning("predict | Person {} does not outbounds treshold {}>{}".format(pred,score,distance_threshold))
log.debug("predict | Prediction: {}".format(predictions))
else:
log.debug("predict | Face not recognized :/")
predictions = -1

return {"predictions": predictions, "score": min_distance}
return {"predictions": predictions, "scores": scores}
33 changes: 32 additions & 1 deletion datastructure/Response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Define the standard response to return to the client
"""

import logging
from datetime import datetime


Expand All @@ -16,5 +17,35 @@ def __init__(self, status="KO", description=None, error=None, data=None):
self.status = status
self.description = description
self.error = error
self.data = data
self.data = self.parse_data(data)
self.date = str(datetime.now())

def parse_data(self, data):
"""
:param data:
:return:
"""
log = logging.getLogger()
log.debug("parse_data | Parsing {}".format(data))

predictions = []
scores = []
t = {}
if data is not None:
log.debug("parse_data | Data not None ...")
if isinstance(data, dict):
log.debug("parse_data | Data is a dict")
if data["predictions"] and data["scores"]:
log.debug("parse_data | Predictions and scores provided")
if isinstance(data["predictions"], list) and isinstance(data["scores"], list):
predictions = data["predictions"]
scores = data["scores"]
if len(predictions) == len(scores):
log.debug("parse_data | Predictions and scores same lenght")
for i in range(len(predictions)):
t[predictions[i][0]] = scores[i]
log.debug("parse_data | Dict initalized -> {}".format(t))
return t

self.date = data
22 changes: 11 additions & 11 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from utils.util import init_main_data, random_string, secure_request

# ===== LOAD CONFIGURATION FILE =====
# TODO: Add argument parser for manage configuration file
# TODO: Add argument/environment var parser for manage configuration file
CONFIG_FILE = "conf/test.json"

CFG, log, TMP_UPLOAD_PREDICTION, TMP_UPLOAD_TRAINING, TMP_UPLOAD = init_main_data(CONFIG_FILE)
Expand Down Expand Up @@ -46,9 +46,6 @@
clf.model_path = CFG["classifier"]["model_path"]
clf.load_classifier_from_file(CFG["classifier"]["timestamp"])

# TODO Add check on extension
allowed_ext = ["jpg", "jpeg", "png"]


@app.route('/', methods=['GET'])
def home():
Expand Down Expand Up @@ -119,12 +116,10 @@ def tune_http():
return jsonify(tune_network(TMP_UPLOAD_TRAINING, file, clf))


# TODO: Create API for init dataset only

@app.route('/uploads/<filename>')
def uploaded_file(filename):
"""
Expose images only to the one that know the image name
Expose images only to the one that know the image name in a secure method
:param filename:
:return:
"""
Expand All @@ -134,7 +129,7 @@ def uploaded_file(filename):
@app.before_request
def csrf_protect():
"""
Validate csrf token against the one in session
:return:
"""
if "dashboard" not in str(request.url_rule):
Expand All @@ -144,20 +139,25 @@ def csrf_protect():
abort(403)


# secure_request(request)
@app.after_request
def secure_headers(response):
"""
Apply securiy headers to the response call
:return:
"""
return secure_request(response)


def generate_csrf_token():
"""
Generate a randome string and set the data into session
:return:
"""
if '_csrf_token' not in session:
session['_csrf_token'] = random_string()
return session['_csrf_token']



app.jinja_env.globals['csrf_token'] = generate_csrf_token

if __name__ == '__main__':
Expand Down
12 changes: 9 additions & 3 deletions utils/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ def init_main_data(config_file):

log = load_logger(CFG["logging"]["level"], CFG["logging"]["path"], CFG["logging"]["prefix"])

# TODO: Verify the presence -> create directory
# NOTE: create a directory every time that you need to use this folder
TMP_UPLOAD_PREDICTION = CFG["PyRecognizer"]["temp_upload_predict"]
TMP_UPLOAD_TRAINING = CFG["PyRecognizer"]["temp_upload_training"]
Expand Down Expand Up @@ -121,7 +120,10 @@ def random_string(string_length=10):
:return:
"""
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(string_length))
data = ""
for character in range(string_length):
data += random.choice(letters)
return data


def zip_data(file_to_zip, path):
Expand Down Expand Up @@ -241,7 +243,11 @@ def secure_request(request):
:return:
"""
request.headers['Content-Security-Policy'] = "default-src 'self'"
request.headers['Feature-Policy'] = "geolocation 'none'; microphone 'none'; camera 'self'"
request.headers['Referrer-Policy'] = 'no-referrer'
request.headers['Strict-Transport-Security'] = "max-age=31536000; includeSubDomains"
request.headers['X-Content-Type-Options'] = 'nosniff'
request.headers['X-Content-Type-Options'] = 'nosniff'
request.headers['X-Permitted-Cross-Domain-Policies'] = 'none'
request.headers['X-XSS-Protection'] = '1; mode=block'

return request

0 comments on commit 4b539ab

Please sign in to comment.