Minimal face API using OpenCV

In this example, we will see how to create a web face API using OpenCV and Flask. The minimal_face_api project codes the web server application. The main.py script is responsible for parsing the requests and building the response to the client. The code of this script is as follows:

# Import required packages:
from flask import Flask, request, jsonify
import urllib.request
from face_processing import FaceProcessing

# Initialize application and FaceProcessing():
app = Flask(__name__)
fc = FaceProcessing()


@app.errorhandler(400)
def bad_request(e):
# return also the code error
return jsonify({"status": "not ok", "message": "this server could not understand your request"}), 400


@app.errorhandler(404)
def not_found(e):
# return also the code error
return jsonify({"status": "not found", "message": "route not found"}), 404


@app.errorhandler(500)
def not_found(e):
# return also the code error
return jsonify({"status": "internal error", "message": "internal error occurred in server"}), 500


@app.route('/detect', methods=['GET', 'POST', 'PUT'])
def detect_human_faces():
if request.method == 'GET':
if request.args.get('url'):
with urllib.request.urlopen(request.args.get('url')) as url:
return jsonify({"status": "ok", "result": fc.face_detection(url.read())}), 200
else:
return jsonify({"status": "bad request", "message": "Parameter url is not present"}), 400
elif request.method == 'POST':
if request.files.get("image"):
return jsonify({"status": "ok", "result": fc.face_detection(request.files["image"].read())}), 200
else:
return jsonify({"status": "bad request", "message": "Parameter image is not present"}), 400
else:
return jsonify({"status": "failure", "message": "PUT method not supported for API"}), 405


if __name__ == "__main__":
# Add parameter host='0.0.0.0' to run on your machines IP address:
app.run(host='0.0.0.0')

As you can see, we make use of the jsonify() function to create the JSON representation of the given arguments, with an application/json MIME type. JSON can be considered the de facto standard for information exchange and, in this example, we will return a JSON response, rather than return an image as we have performed in the previous example. As you can also see, this API supports both GET and POST requests. Additionally, in the main.py script, we also register error handlers by decorating functions with errorhandler(). Remember also to set the error code when returning the response to the client.

The image processing is performed in the face_processing.py script, where the FaceProcessing() class is coded:

# Import required packages:
import cv2
import numpy as np
import os


class FaceProcessing(object):
def __init__(self):
self.file = os.path.join(os.path.join(os.path.dirname(__file__), "data"), "haarcascade_frontalface_alt.xml")
self.face_cascade = cv2.CascadeClassifier(self.file)

def face_detection(self, image):
# Convert image to OpenCV format:
image_array = np.asarray(bytearray(image), dtype=np.uint8)
img_opencv = cv2.imdecode(image_array, -1)
output = []
# Detect faces and build output:
gray = cv2.cvtColor(img_opencv, cv2.COLOR_BGR2GRAY)
faces = self.face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(25, 25))
for face in faces:
# face.tolist(): returns a copy of the array data as a Python list
x, y, w, h = face.tolist()
face = {"box": [x, y, x + w, y + h]}
output.append(face)
# Return output:
return output

The face_detection() method performs face detection by using the OpenCV detectMultiScale() function. For every detected face, we will get its coordinates, (x, y, w, h), and build the box by encoding the detection in a proper format:

face = {"box": [x, y, x + w, y + h]}

Finally, we add the encoded face detection to the output:

output.append(face)

When all the detected faces are added to the output, we will return it.

To make use of this API, we can perform a GET request from the browser in the same way as we performed in the previous examples. Additionally, as our API also supports POST requests, we have included two scripts to test the functionality of this API. These scripts perform both GET and POST requests to see how you can interact with the aforementioned face API. More specifically, demo_request.py performs several requests to the face API in order to obtain different responses and, also, to see how error handling works. 

In this script, we first perform a GET request using an incorrect URL:

# Import required packages:
import requests

FACE_DETECTION_REST_API_URL = "http://localhost:5000/detect"
FACE_DETECTION_REST_API_URL_WRONG = "http://localhost:5000/process"
IMAGE_PATH = "test_face_processing.jpg"
URL_IMAGE = "https://raw.githubusercontent.com/opencv/opencv/master/samples/data/lena.jpg"

# Submit the GET request:
r = requests.get(FACE_DETECTION_REST_API_URL_WRONG)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))

In this case, we get the following:

status code: 404
headers: {'Content-Type': 'application/json', 'Content-Length': '51', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:25 GMT'}
content: {'message': 'route not found', 'status': 'not found'}

The obtained status code (404) means that the client could communicate with the server, but the server could not find what was requested. This is because the URL of the request (http://localhost:5000/process) is incorrect.  

The second request we perform is a correct GET request:

# Submit the GET request:
payload = {'url': URL_IMAGE}
r = requests.get(FACE_DETECTION_REST_API_URL, params=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))

In this case, we get the following:

status code: 200
headers: {'Content-Type': 'application/json', 'Content-Length': '53', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:31 GMT'}
content: {'result': [{'box': [213, 200, 391, 378]}], 'status': 'ok'}

The status code (200) indicates that the request has been performed successfully. Additionally, you can also see that one face has been detected corresponding to Lenna's face. 

The third request we perform is also a GET request, but the payload is missing:

# Submit the GET request:
r = requests.get(FACE_DETECTION_REST_API_URL)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))

In this case, the response we get is as follows:

status code: 400
headers: {'Content-Type': 'application/json', 'Content-Length': '66', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:32 GMT'}
content: {'message': 'Parameter url is not present', 'status': 'bad request'}

The status code (400) means a bad request. As you can see, the url parameter is missing.

The fourth request we perform is a POST request with the correct payload:

# Load the image and construct the payload:
image = open(IMAGE_PATH, "rb").read()
payload = {"image": image}

# Submit the POST request:
r = requests.post(FACE_DETECTION_REST_API_URL, files=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))

We get the following response:

status code: 200
headers: {'Content-Type': 'application/json', 'Content-Length': '449', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:34 GMT'}
content: {'result': [{'box': [151, 29, 193, 71]}, {'box': [77, 38, 115, 76]}, {'box': [448, 37, 490, 79]}, {'box': [81, 172, 127, 218]}, {'box': [536, 47, 574, 85]}, {'box': [288, 173, 331, 216]}, {'box': [509, 170, 553, 214]}, {'box': [357, 48, 399, 90]}, {'box': [182, 179, 219, 216]}, {'box': [251, 38, 293, 80]}, {'box': [400, 174, 444, 218]}, {'box': [390, 87, 430, 127]}, {'box': [54, 89, 97, 132]}, {'box': [499, 91, 542, 134]}, {'box': [159, 95, 198, 134]}, {'box': [310, 115, 344, 149]}, {'box': [225, 116, 265, 156]}], 'status': 'ok'}

As you can see, many faces are detected. This is because test_face_processing.jpg contains a lot of faces. 

The final request is PUT request:

# Submit the PUT request:
r = requests.put(FACE_DETECTION_REST_API_URL, files=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))

We get the following output:

status code: 405
headers: {'Content-Type': 'application/json', 'Content-Length': '66', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:35 GMT'}
content: {'message': 'PUT method not supported for API', 'status': 'failure'}

As you can see, the PUT method is not supported. This face API only supports GET and POST methods.

As you could see in the previous responses, when the request was performed successfully, we got the detected faces as JSON data. In order to see how to parse the response and use it to draw the detected faces, we can code the script demo_request_drawing.py as follows:

# Import required packages:
import cv2
import numpy as np
import requests
from matplotlib import pyplot as plt


def show_img_with_matplotlib(color_img, title, pos):
"""Shows an image using matplotlib capabilities"""

img_RGB = color_img[:, :, ::-1]

ax = plt.subplot(1, 1, pos)
plt.imshow(img_RGB)
plt.title(title)
plt.axis('off')


FACE_DETECTION_REST_API_URL = "http://localhost:5000/detect"
IMAGE_PATH = "test_face_processing.jpg"

# Load the image and construct the payload:
image = open(IMAGE_PATH, "rb").read()
payload = {"image": image}

# Submit the POST request:
r = requests.post(FACE_DETECTION_REST_API_URL, files=payload)

# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))

# Get JSON data from the response and get 'result':
json_data = r.json()
result = json_data['result']

# Convert the loaded image to the OpenCV format:
image_array = np.asarray(bytearray(image), dtype=np.uint8)
img_opencv = cv2.imdecode(image_array, -1)

# Draw faces in the OpenCV image:
for face in result:
left, top, right, bottom = face['box']
# To draw a rectangle, you need top-left corner and bottom-right corner of rectangle:
cv2.rectangle(img_opencv, (left, top), (right, bottom), (0, 255, 255), 2)
# Draw top-left corner and bottom-right corner (checking):
cv2.circle(img_opencv, (left, top), 5, (0, 0, 255), -1)
cv2.circle(img_opencv, (right, bottom), 5, (255, 0, 0), -1)

# Create the dimensions of the figure and set title:
fig = plt.figure(figsize=(8, 8))
plt.suptitle("Using face detection API", fontsize=14, fontweight='bold')
fig.patch.set_facecolor('silver')

# Show the output image
show_img_with_matplotlib(img_opencv, "face detection", 1)

# Show the Figure:
plt.show()

As it can be seen above, we first load the image and construct the payload. Then, we perform a POST request. Afterwards, we get the JSON data from the response and get the result:

# Get JSON data from the response and get 'result':
json_data = r.json()
result = json_data['result']

At this point, we can draw the detected faces, iterating over all the detected faces, as follows:

# Draw faces in the OpenCV image:
for face in result:
left, top, right, bottom = face['box']
# To draw a rectangle, you need top-left corner and bottom-right corner of rectangle:
cv2.rectangle(img_opencv, (left, top), (right, bottom), (0, 255, 255), 2)
# Draw top-left corner and bottom-right corner (checking):
cv2.circle(img_opencv, (left, top), 5, (0, 0, 255), -1)
cv2.circle(img_opencv, (right, bottom), 5, (255, 0, 0), -1)

For each detected face we draw a rectangle and also top-left and bottom-right points. The output of this script can be seen in the next screenshot:

Faces detected with API using OpenCV
As shown in the previous screenshot, all the faces have been detected.