Watch Video Here
In this tutorial, we’ll learn to perform real-time multi-face detection followed by 3D face landmarks detection using the Mediapipe library in python on 2D images/videos, without using any dedicated depth sensor. After that, we will learn to build a facial expression recognizer that tells you if the person’s eyes or mouth are open or closed
Below you can see the facial expression recognizer in action, on a few sample images:
And then, in the end, we see how we can combine what we’ve learned to create animated Snapchat-like 2d filters and overlay them over the faces in images and videos. The filters will trigger in real-time for videos based on the facial expressions of the person. Below you can see results on a sample video.
Everything that we will build will work on the images, camera feed in real-time, and recorded videos as well, and the code is very neatly structured and is explained in the simplest manner possible.
This tutorial also has a video version that you can go and watch for a detailed explanation, although this blog post alone can also suffice.
This post can be split into 4 parts:
Part 1 (a): Introduction to Face Landmarks Detection
Part 1 (b): Mediapipe’s Face Landmarks Detection Implementation
Part 2: Face Landmarks Detection on images and videos
Part 3: Face Expression Recognition
Part 4: Snapchat Filter Controlled by Facial Expressions
Part 1 (a): Introduction to Face Landmarks Detection
Facial landmark detection/estimation is the process of detecting and tracking face key landmarks (that represent important regions of the face e.g, the center of the eye, and the tip of the nose, etc) in images and videos. It allows you to localize the face features and identify the shape and orientation of the face.
It also fits into the key point estimation category that I had explained in detail a few weeks ago in Real-Time 3D Pose Detection & Pose Classification with Mediapipe and Python post, make sure to check that one out too.
In this tutorial, we will learn to detect four hundred sixty-eight facial landmarks. Below are the results of the landmarks detector we will use.
It is a must-learn task for every vision practitioner as it is used as a pre-processing task in many vision applications like
- Face Animation & Reenactment
- Face Morphing & Replacement
- Lip Reading & Facial Expression Recognition
Some other types of keypoint estimation tasks are hand landmark detection, pose detection, etc.
I have already made tutorials (Hands Landmarks Detection, Pose Detection) on both of them.
Part 1 (b): Mediapipe’s Face Landmarks Detection Implementation
If Here’s a brief introduction to Mediapipe;
“Mediapipe is a cross-platform/open-source tool that allows you to run a variety of machine learning models in real-time. It’s designed primarily for facilitating the use of ML in streaming media & It was built by Google”
All the solutions provided by Mediapipe are state-of-the-art in terms of speed and accuracy and are used in a lot of well-known applications.
The facial landmarks detection solution provided by Mediapipe is capable of detecting 3D 468 facial landmarks from a 2D image/video and is pretty fast and highly accurate as well and even works fine for occluded faces in varying lighting conditions and with faces of various orientations, and sizes in real-time, even on low-end devices like mobile phones, and Raspberry Pi, etc.
The landmarks detector’s remarkable speed distinguishes it from the other solutions out there anThe landmarks detector’s remarkable speed distinguishes it from the other solutions out there and the reason which makes this solution so fast is that they are using a 2 step detection approach where they have combined a face detector with a comparatively less computationally expensive tracker
So that for the videos, the tracker can be used instead of invoking the face detector at every frame. Let’s dive further into more details
The machine learning pipeline of the Mediapipe’s solution contains two different models that work together:
- A face detector that operates on the full image and locates the faces in the image.
- A face landmarks detector that operates only on those face locations and predicts the 3D facial landmarks.
So the landmarks detector gets an accurately cropped face ROI which makes it capable of precisely working on scaled, rotated, and translated faces without needing data augmentation techniques.
In addition, the faces can also be located based on the face landmarks identified in the previous frame, so the face detector is only invoked as needed, that is in the very first frame or when the tracker loses track of any of the faces.
They have utilized transfer learning and used both synthetic rendered and annotated real-world data to get a model capable of predicting 3D landmark coordinates. Another approach could be to train a model to predict a 2D heatmap for each landmark but will increase the computational cost as there are so many points.
Alright now we have gone through the required basic theory and implementation details of the solution provided by Mediapipe, so without further ado, let’s get started with the code.
Download Code:
[optin-monster slug=”pcj5qsilaajmf3fnkrnm”]
Part 2: Face Landmarks Detection on images and videos
Import the Libraries
Let’s start by importing the required libraries.
import cv2
import itertools
import numpy as np
from time import time
import mediapipe as mp
import matplotlib.pyplot as plt
As mentioned Mediapipe’s face landmarks detection solution internally uses a face detector to get the required Regions of Interest (faces) from the image. So before going to the facial landmarks detection, let’s briefly discuss that face detector first, as Mediapipe also allows to separately use it.
Face Detection
The mediapipe’s face detection solution is based on BlazeFace face detector that uses a very lightweight and highly accurate feature extraction network, that is inspired and modified from MobileNetV1/V2 and used a detection method similar to Single Shot MultiBox Detector (SSD). It is capable of running at a speed of 200-1000+ FPS on flagship devices. For more info, you can check the resources here.
Initialize the Mediapipe Face Detection Model
To use the Mediapipe’s Face Detection solution, we will first have to initialize the face detection class using the syntax mp.solutions.face_detection
, and then we will have to call the function mp.solutions.face_detection.FaceDetection()
with the arguments explained below:
model_selection
– It is an integer index( i.e., 0 or 1 )
. When set to0
, a short-range model is selected that works best for faces within 2 meters from the camera, and when set to1
, a full-range model is selected that works best for faces within 5 meters. Its default value is0
.min_detection_confidence
– It is the minimum detection confidence between([0.0, 1.0])
required to consider the face-detection model’s prediction successful. Its default value is0.5
( i.e., 50% ) which means that all the detections with prediction confidence less than0.5
are ignored by default.
We will also have to initialize the drawing class using the syntax mp.solutions.drawing_utils
which is used to visualize the detection results on the images/frames.
# Initialize the mediapipe face detection class. mp_face_detection = mp.solutions.face_detection # Setup the face detection function. face_detection = mp_face_detection.FaceDetection(model_selection=0, min_detection_confidence=0.5) # Initialize the mediapipe drawing class. mp_drawing = mp.solutions.drawing_utils
Read an Image
Now we will use the function cv2.imread()
to read a sample image and then display the image using the matplotlib
library, after converting it into RGB
from BGR
format.
# Read an image from the specified path. sample_img = cv2.imread('media/sample.jpg') # Specify a size of the figure. plt.figure(figsize = [10, 10]) # Display the sample image, also convert BGR to RGB for display. plt.title("Sample Image");plt.axis('off');plt.imshow(sample_img[:,:,::-1]);plt.show()
Perform Face Detection
Now to perform the detection on the sample image, we will have to pass the image (in RGB
format) into the loaded model by using the function mp.solutions.face_detection.FaceDetection().process()
and we will get an object that will have an attribute detections
that contains a list of a bounding box and six key points for each face in the image. The six key points are on the:
- Right Eye
- Left Eye
- Nose Tip
- Mouth Center
- Right Ear Tragion
- Left Ear Tragion
After performing the detection, we will display the bounding box coordinates and only the first two key points of each detected face in the image, so that you get more intuition about the format of the output.
# Perform face detection after converting the image into RGB format. face_detection_results = face_detection.process(sample_img[:,:,::-1]) # Check if the face(s) in the image are found. if face_detection_results.detections: # Iterate over the found faces. for face_no, face in enumerate(face_detection_results.detections): # Display the face number upon which we are iterating upon. print(f'FACE NUMBER: {face_no+1}') print('---------------------------------') # Display the face confidence. print(f'FACE CONFIDENCE: {round(face.score[0], 2)}') # Get the face bounding box and face key points coordinates. face_data = face.location_data # Display the face bounding box coordinates. print(f'\nFACE BOUNDING BOX:\n{face_data.relative_bounding_box}') # Iterate two times as we only want to display first two key points of each detected face. for i in range(2): # Display the found normalized key points. print(f'{mp_face_detection.FaceKeyPoint(i).name}:') print(f'{face_data.relative_keypoints[mp_face_detection.FaceKeyPoint(i).value]}')
FACE NUMBER: 1
—————————–
FACE CONFIDENCE: 0.98
FACE BOUNDING BOX:
xmin: 0.39702364802360535
ymin: 0.2762746810913086
width: 0.16100731492042542
height: 0.24132275581359863
RIGHT_EYE:
x: 0.4368540048599243
y: 0.3198586106300354
LEFT_EYE:
x: 0.5112437605857849
y: 0.3565130829811096
Note: The bounding boxes are composed of xmin
and width
(both normalized to [0.0, 1.0]
by the image width) and ymin
and height
(both normalized to [0.0, 1.0]
by the image height). Each keypoint is composed of x
and y
, which are normalized to [0.0, 1.0]
by the image width and height respectively.
Now we will draw the detected bounding box(es) and the key points on a copy of the sample image using the function mp.solutions.drawing_utils.draw_detection()
from the class mp.solutions.drawing_utils
, we had initialized earlier and will display the resultant image using the matplotlib library.
# Create a copy of the sample image to draw the bounding box and key points. img_copy = sample_img[:,:,::-1].copy() # Check if the face(s) in the image are found. if face_detection_results.detections: # Iterate over the found faces. for face_no, face in enumerate(face_detection_results.detections): # Draw the face bounding box and key points on the copy of the sample image. mp_drawing.draw_detection(image=img_copy, detection=face, keypoint_drawing_spec=mp_drawing.DrawingSpec(color=(255, 0, 0), thickness=2, circle_radius=2)) # Specify a size of the figure. fig = plt.figure(figsize = [10, 10]) # Display the resultant image with the bounding box and key points drawn, # also convert BGR to RGB for display. plt.title("Resultant Image");plt.axis('off');plt.imshow(img_copy);plt.show()
Note: Although, the detector quite accurately detects the faces, but fails to precisely detect facial key points (landmarks) in some scenarios (e.g. for non-frontal, rotated, or occluded faces) so that is why we will need the Mediapipe’s face landmarks detection solution for creating the Snapchat filter that is our main goal.
Face Landmarks Detection
Now, let’s move to the facial landmarks detection, we will start by initializing the face landmarks detection model.
Initialize the Mediapipe Face Landmarks Detection Model
To initialize the Mediapipe’s face landmarks detection model, we will have to initialize the face mesh class using the syntax mp.solutions.face_mesh
and then we will have to call the function mp.solutions.face_mesh.FaceMesh()
with the arguments explained below:
static_image_mode
– It is a boolean value that is if set toFalse
, the solution treats the input images as a video stream. It will try to detect faces in the first input images, and upon a successful detection further localizes the face landmarks. In subsequent images, once allmax_num_faces
faces are detected and the corresponding face landmarks are localized, it simply tracks those landmarks without invoking another detection until it loses track of any of the faces. This reduces latency and is ideal for processing video frames. If set toTrue
, face detection runs on every input image, ideal for processing a batch of static, possibly unrelated, images. Its default value isFalse
.max_num_faces
– It is the maximum number of faces to detect. Its default value is1
.min_detection_confidence
– It is the minimum detection confidence([0.0, 1.0])
required to consider the face-detection model’s prediction correct. Its default value is0.5
which means that all the detections with prediction confidence less than 50% are ignored by default.min_tracking_confidence
– It is the minimum tracking confidence([0.0, 1.0])
from the landmark-tracking model for the face landmarks to be considered tracked successfully, or otherwise face detection will be invoked automatically on the next input image, so increasing its value increases the robustness, but also increases the latency. It is ignored ifstatic_image_mode
isTrue
, where face detection simply runs on every image. Its default value is0.5
.
After that, we will initialize the mp.solutions.drawing_styles
class that will allow us to get different provided drawing styles of the landmarks on the images/frames.
# Initialize the mediapipe face mesh class. mp_face_mesh = mp.solutions.face_mesh # Setup the face landmarks function for images. face_mesh_images = mp_face_mesh.FaceMesh(static_image_mode=True, max_num_faces=2, min_detection_confidence=0.5) # Setup the face landmarks function for videos. face_mesh_videos = mp_face_mesh.FaceMesh(static_image_mode=False, max_num_faces=1, min_detection_confidence=0.5,min_tracking_confidence=0.3) # Initialize the mediapipe drawing styles class. mp_drawing_styles = mp.solutions.drawing_styles
Perform Face Landmarks Detection
Now to perform the landmarks detection, we will pass the image (in RGB
format) to the face landmarks detection machine learning pipeline by using the function mp.solutions.face_mesh.FaceMesh().process()
and get a list of four hundred sixty-eight facial landmarks for each detected face in the image. Each landmark will have:
x
– It is the landmark x-coordinate normalized to [0.0, 1.0] by the image width.y
– It is the landmark y-coordinate normalized to [0.0, 1.0] by the image height.z
– It is the landmark z-coordinate normalized to roughly the same scale asx
. It represents the landmark depth with the center of the head being the origin, and the smaller the value is, the closer the landmark is to the camera.
We will display only two landmarks of each eye to get an intuition about the format of output, the ml pipeline outputs an object that has an attribute multi_face_landmarks
that contains the found landmarks coordinates of each face as an element of a list.
# Perform face landmarks detection after converting the image into RGB format. face_mesh_results = face_mesh_images.process(sample_img[:,:,::-1]) # Get the list of indexes of the left and right eye. LEFT_EYE_INDEXES = list(set(itertools.chain(*mp_face_mesh.FACEMESH_LEFT_EYE))) RIGHT_EYE_INDEXES = list(set(itertools.chain(*mp_face_mesh.FACEMESH_RIGHT_EYE))) # Check if facial landmarks are found. if face_mesh_results.multi_face_landmarks: # Iterate over the found faces. for face_no, face_landmarks in enumerate(face_mesh_results.multi_face_landmarks): # Display the face number upon which we are iterating upon. print(f'FACE NUMBER: {face_no+1}') print('-----------------------') # Display the face part name i.e., left eye whose landmarks we are gonna display. print(f'LEFT EYE LANDMARKS:\n') # Iterate over the first two landmarks indexes of the left eye. for LEFT_EYE_INDEX in LEFT_EYE_INDEXES[:2]: # Display the found normalized landmarks of the left eye. print(face_landmarks.landmark[LEFT_EYE_INDEX]) # Display the face part name i.e., right eye whose landmarks we are gonna display. print(f'RIGHT EYE LANDMARKS:\n') # Iterate over the first two landmarks indexes of the right eye. for RIGHT_EYE_INDEX in RIGHT_EYE_INDEXES[:2]: # Display the found normalized landmarks of the right eye. print(face_landmarks.landmark[RIGHT_EYE_INDEX])
FACE NUMBER: 1
—————————–
LEFT EYE LANDMARKS:
x: 0.49975821375846863
y: 0.3340317904949188
z: -0.0035526191350072622
x: 0.505615234375
y: 0.33464953303337097
z: -0.005253124982118607
RIGHT EYE LANDMARKS:
x: 0.4383838176727295
y: 0.2998684346675873
z: -0.0014895268250256777
x: 0.430422842502594
y: 0.30033284425735474
z: 0.006082724779844284
Note: The z-coordinate is just the relative distance of the landmark from the center of the head, and this distance increases and decreases depending upon the distance from the camera so that is why it represents the depth of each landmark point.
Now we will draw the detected landmarks on a copy of the sample image using the function mp.solutions.drawing_utils.draw_landmarks()
from the class mp.solutions.drawing_utils
, we had initialized earlier and will display the resultant image. The function mp.solutions.drawing_utils.draw_landmarks()
can take the following arguments.
image
– It is the image in RGB format on which the landmarks are to be drawn.landmark_list
– It is the normalized landmark list that is to be drawn on the image.connections
– It is the list of landmark index tuples that specifies how landmarks to be connected in the drawing. The provided options are;mp_face_mesh.FACEMESH_FACE_OVAL, mp_face_mesh.FACEMESH_LEFT_EYE, mp_face_mesh.FACEMESH_LEFT_EYEBROW, mp_face_mesh.FACEMESH_LIPS, mp_face_mesh.FACEMESH_RIGHT_EYE, mp_face_mesh.FACEMESH_RIGHT_EYEBROW, mp_face_mesh.FACEMESH_TESSELATION, mp_face_mesh.FACEMESH_CONTOURS
.landmark_drawing_spec
– It specifies the landmarks’ drawing settings such as color, line thickness, and circle radius. It can be set equal to themp.solutions.drawing_utils.DrawingSpec(color, thickness, circle_radius))
object.connection_drawing_spec
– It specifies the connections’ drawing settings such as color and line thickness. It can be either amp.solutions.drawing_utils.DrawingSpec
object or a function from the classmp.solutions.drawing_styles
, the currently provided options for face mesh are;get_default_face_mesh_contours_style()
,get_default_face_mesh_tesselation_style()
.
# Create a copy of the sample image in RGB format to draw the found facial landmarks on. img_copy = sample_img[:,:,::-1].copy() # Check if facial landmarks are found. if face_mesh_results.multi_face_landmarks: # Iterate over the found faces. for face_landmarks in face_mesh_results.multi_face_landmarks: # Draw the facial landmarks on the copy of the sample image with the # face mesh tesselation connections using default face mesh tesselation style. mp_drawing.draw_landmarks(image=img_copy, landmark_list=face_landmarks,connections=mp_face_mesh.FACEMESH_TESSELATION, landmark_drawing_spec=None, connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style()) # Draw the facial landmarks on the copy of the sample image with the # face mesh contours connections using default face mesh contours style. mp_drawing.draw_landmarks(image=img_copy, landmark_list=face_landmarks,connections=mp_face_mesh.FACEMESH_CONTOURS, landmark_drawing_spec=None, connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_contours_style()) # Specify a size of the figure. fig = plt.figure(figsize = [10, 10]) # Display the resultant image with the face mesh drawn. plt.title("Resultant Image");plt.axis('off');plt.imshow(img_copy);plt.show()
Create a Face Landmarks Detection Function
Now we will put all this together to create a function detectFacialLandmarks()
that will perform face landmarks detection on an image and will visualize the resultant image along with the original image or return the resultant image along with the output of the model depending upon the passed arguments.
def detectFacialLandmarks(image, face_mesh, display = True): ''' This function performs facial landmarks detection on an image. Args: image: The input image of person(s) whose facial landmarks needs to be detected. face_mesh: The face landmarks detection function required to perform the landmarks detection. display: A boolean value that is if set to true the function displays the original input image, and the output image with the face landmarks drawn and returns nothing. Returns: output_image: A copy of input image with face landmarks drawn. results: The output of the facial landmarks detection on the input image. ''' # Perform the facial landmarks detection on the image, after converting it into RGB format. results = face_mesh.process(image[:,:,::-1]) # Create a copy of the input image to draw facial landmarks. output_image = image[:,:,::-1].copy() # Check if facial landmarks in the image are found. if results.multi_face_landmarks: # Iterate over the found faces. for face_landmarks in results.multi_face_landmarks: # Draw the facial landmarks on the output image with the face mesh tesselation # connections using default face mesh tesselation style. mp_drawing.draw_landmarks(image=output_image, landmark_list=face_landmarks, connections=mp_face_mesh.FACEMESH_TESSELATION, landmark_drawing_spec=None, connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style()) # Draw the facial landmarks on the output image with the face mesh contours # connections using default face mesh contours style. mp_drawing.draw_landmarks(image=output_image, landmark_list=face_landmarks, connections=mp_face_mesh.FACEMESH_CONTOURS, landmark_drawing_spec=None, connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_contours_style()) # Check if the original input image and the output image are specified to be displayed. if display: # Display the original input image and the output image. plt.figure(figsize=[15,15]) plt.subplot(121);plt.imshow(image[:,:,::-1]);plt.title("Original Image");plt.axis('off'); plt.subplot(122);plt.imshow(output_image);plt.title("Output");plt.axis('off'); # Otherwise else: # Return the output image in BGR format and results of facial landmarks detection. return np.ascontiguousarray(output_image[:,:,::-1], dtype=np.uint8), results
Now we will utilize the function detectFacialLandmarks()
created above to perform face landmarks detection on a few sample images and display the results.
# Read a sample image and perform facial landmarks detection on it. image = cv2.imread('media/sample1.jpg') detectFacialLandmarks(image, face_mesh_images, display=True)
# Read another sample image and perform facial landmarks detection on it. image = cv2.imread('media/sample2.jpg') detectFacialLandmarks(image, face_mesh_images, display=True)
# Read another sample image and perform facial landmarks detection on it. image = cv2.imread('media/sample3.jpg') detectFacialLandmarks(image, face_mesh_images, display=True)
Face Landmarks Detection on Real-Time Webcam Feed
The results on the images were remarkable, but now we will try the function on a real-time webcam feed. We will also calculate and display the number of frames being updated in one second to get an idea of whether this solution can work in real-time on a CPU or not.
# Initialize the VideoCapture object to read from the webcam. camera_video = cv2.VideoCapture(0) camera_video.set(3,1280) camera_video.set(4,960) # Create named window for resizing purposes. cv2.namedWindow('Face Landmarks Detection', cv2.WINDOW_NORMAL) # Initialize a variable to store the time of the previous frame. time1 = 0 # Iterate until the webcam is accessed successfully. while camera_video.isOpened(): # Read a frame. ok, frame = camera_video.read() # Check if frame is not read properly then continue to the next iteration to # read the next frame. if not ok: continue # Flip the frame horizontally for natural (selfie-view) visualization. frame = cv2.flip(frame, 1) # Perform Face landmarks detection. frame, _ = detectFacialLandmarks(frame, face_mesh_videos, display=False) # Set the time for this frame to the current time. time2 = time() # Check if the difference between the previous and this frame time > 0 to avoid # division by zero. if (time2 - time1) > 0: # Calculate the number of frames per second. frames_per_second = 1.0 / (time2 - time1) # Write the calculated number of frames per second on the frame. cv2.putText(frame, 'FPS: {}'.format(int(frames_per_second)), (10, 30), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 3) # Update the previous frame time to this frame time. # As this frame will become previous frame in next iteration. time1 = time2 # Display the frame. cv2.imshow('Face Landmarks Detection', frame) # Wait for 1ms. If a key is pressed, retreive the ASCII code of the key. k = cv2.waitKey(1) & 0xFF # Check if 'ESC' is pressed and break the loop. if(k == 27): break # Release the VideoCapture Object and close the windows. camera_video.release() cv2.destroyAllWindows()
Output
Impressive! the solution is fast as well as accurate.
Face Expression Recognition
Now that we have the detected landmarks, we will use them to recognize the facial expressions of people in the images/videos using the classical techniques. Our recognizor will be capable of identifying the following facial expressions:
- Eyes Opened or Closed 😳 (can be used to check drowsiness, wink or shock expression)
- Mouth Opened or Closed 😱 (can be used to check yawning)
For the sake of simplicity, we are only limiting this to two expressions. But if you want, you can easily extend this application to make it capable of identifying more facial expressions just by adding more conditional statements or maybe merging these two conditions. Like for example, eyes and mouth both wide open can represent surprise expression.
Create a Function to Calculate Size of a Face Part
First, we will create a function getSize()
that will utilize detected landmarks to calculate the size of a face part. All we will need is to figure out a way to isolate the landmarks of the face part and luckily that can easily be done using the frozenset objects (attributes of the mp.solutions.face_mesh
class), which contain the required indexes.
mp_face_mesh.FACEMESH_FACE_OVAL
contains indexes of face outline.mp_face_mesh.FACEMESH_LIPS
contains indexes of lips.mp_face_mesh.FACEMESH_LEFT_EYE
contains indexes of left eye.mp_face_mesh.FACEMESH_RIGHT_EYE
contains indexes of right eye.mp_face_mesh.FACEMESH_LEFT_EYEBROW
contains indexes of left eyebrow.mp_face_mesh.FACEMESH_RIGHT_EYEBROW
contains indexes of right eyebrow.
After retrieving the landmarks of the face part, we will simply pass it to the function cv2.boundingRect()
to get the width and height of the face part. The function cv2.boundingRect(landmarks)
returns the coordinates (x1
, y1
, width
, height
) of a bounding box enclosing the object (face part), given the landmarks but we will only need the height
and width
of the bounding box.
def getSize(image, face_landmarks, INDEXES): ''' This function calculate the height and width of a face part utilizing its landmarks. Args: image: The image of person(s) whose face part size is to be calculated. face_landmarks: The detected face landmarks of the person whose face part size is to be calculated. INDEXES: The indexes of the face part landmarks, whose size is to be calculated. Returns: width: The calculated width of the face part of the face whose landmarks were passed. height: The calculated height of the face part of the face whose landmarks were passed. landmarks: An array of landmarks of the face part whose size is calculated. ''' # Retrieve the height and width of the image. image_height, image_width, _ = image.shape # Convert the indexes of the landmarks of the face part into a list. INDEXES_LIST = list(itertools.chain(*INDEXES)) # Initialize a list to store the landmarks of the face part. landmarks = [] # Iterate over the indexes of the landmarks of the face part. for INDEX in INDEXES_LIST: # Append the landmark into the list. landmarks.append([int(face_landmarks.landmark[INDEX].x * image_width), int(face_landmarks.landmark[INDEX].y * image_height)]) # Calculate the width and height of the face part. _, _, width, height = cv2.boundingRect(np.array(landmarks)) # Convert the list of landmarks of the face part into a numpy array. landmarks = np.array(landmarks) # Retrurn the calculated width height and the landmarks of the face part. return width, height, landmarks
Now we will create a function isOpen()
that will utilize the getSize()
function we had created above to check whether a face part (e.g. mouth or an eye) of a person is opened or closed.
Hint: The height of an opened mouth or eye will be greater than the height of a closed mouth or eye.
def isOpen(image, face_mesh_results, face_part, threshold=5, display=True): ''' This function checks whether the an eye or mouth of the person(s) is open, utilizing its facial landmarks. Args: image: The image of person(s) whose an eye or mouth is to be checked. face_mesh_results: The output of the facial landmarks detection on the image. face_part: The name of the face part that is required to check. threshold: The threshold value used to check the isOpen condition. display: A boolean value that is if set to true the function displays the output image and returns nothing. Returns: output_image: The image of the person with the face part is opened or not status written. status: A dictionary containing isOpen statuses of the face part of all the detected faces. ''' # Retrieve the height and width of the image. image_height, image_width, _ = image.shape # Create a copy of the input image to write the isOpen status. output_image = image.copy() # Create a dictionary to store the isOpen status of the face part of all the detected faces. status={} # Check if the face part is mouth. if face_part == 'MOUTH': # Get the indexes of the mouth. INDEXES = mp_face_mesh.FACEMESH_LIPS # Specify the location to write the is mouth open status. loc = (10, image_height - image_height//40) # Initialize a increment that will be added to the status writing location, # so that the statuses of two faces donot overlap. increment=-30 # Check if the face part is left eye. elif face_part == 'LEFT EYE': # Get the indexes of the left eye. INDEXES = mp_face_mesh.FACEMESH_LEFT_EYE # Specify the location to write the is left eye open status. loc = (10, 30) # Initialize a increment that will be added to the status writing location, # so that the statuses of two faces donot overlap. increment=30 # Check if the face part is right eye. elif face_part == 'RIGHT EYE': # Get the indexes of the right eye. INDEXES = mp_face_mesh.FACEMESH_RIGHT_EYE # Specify the location to write the is right eye open status. loc = (image_width-300, 30) # Initialize a increment that will be added to the status writing location, # so that the statuses of two faces donot overlap. increment=30 # Otherwise return nothing. else: return # Iterate over the found faces. for face_no, face_landmarks in enumerate(face_mesh_results.multi_face_landmarks): # Get the height of the face part. _, height, _ = getSize(image, face_landmarks, INDEXES) # Get the height of the whole face. _, face_height, _ = getSize(image, face_landmarks, mp_face_mesh.FACEMESH_FACE_OVAL) # Check if the face part is open. if (height/face_height)*100 > threshold: # Set status of the face part to open. status[face_no] = 'OPEN' # Set color which will be used to write the status to green. color=(0,255,0) # Otherwise. else: # Set status of the face part to close. status[face_no] = 'CLOSE' # Set color which will be used to write the status to red. color=(0,0,255) # Write the face part isOpen status on the output image at the appropriate location. cv2.putText(output_image, f'FACE {face_no+1} {face_part} {status[face_no]}.', (loc[0],loc[1]+(face_no*increment)), cv2.FONT_HERSHEY_PLAIN, 1.4, color, 2) # Check if the output image is specified to be displayed. if display: # Display the output image. plt.figure(figsize=[10,10]) plt.imshow(output_image[:,:,::-1]);plt.title("Output Image");plt.axis('off'); # Otherwise else: # Return the output image and the isOpen statuses of the face part of each detected face. return output_image, status
Now we will utilize the function isOpen()
created above to check the mouth and eyes status on a few sample images and display the results.
# Read another sample image and perform facial expression recognition on it. image = cv2.imread('media/sample1.jpg') image = cv2.flip(image, 1) _, face_mesh_results = detectFacialLandmarks(image, face_mesh_images, display=False) if face_mesh_results.multi_face_landmarks: output_image, _ = isOpen(image, face_mesh_results, 'MOUTH', threshold=15, display=False) output_image, _ = isOpen(output_image, face_mesh_results, 'LEFT EYE', threshold=5, display=False) isOpen(output_image, face_mesh_results, 'RIGHT EYE', threshold=5)
# Read another sample image and perform facial expression recognition on it. image = cv2.imread('media/sample2.jpg') image = cv2.flip(image, 1) _, face_mesh_results = detectFacialLandmarks(image, face_mesh_images, display=False) if face_mesh_results.multi_face_landmarks: output_image, _ = isOpen(image, face_mesh_results, 'MOUTH', threshold=15, display=False) output_image, _ = isOpen(output_image, face_mesh_results, 'LEFT EYE', threshold=5, display=False) isOpen(output_image, face_mesh_results, 'RIGHT EYE', threshold=5)
# Read another sample image and perform facial expression recognition on it. image = cv2.imread('media/sample3.jpg') image = cv2.flip(image, 1) _, face_mesh_results = detectFacialLandmarks(image, face_mesh_images, display=False) if face_mesh_results.multi_face_landmarks: output_image, _ = isOpen(image, face_mesh_results, 'MOUTH', threshold=15, display=False) output_image, _ = isOpen(output_image, face_mesh_results, 'LEFT EYE', threshold=5, display=False) isOpen(output_image, face_mesh_results, 'RIGHT EYE', threshold=5)
As expected, the results are fascinating!
Snapchat Filter Controlled by Facial Expressions
Now that we have the face expression recognizer, let’s start building a Snapchat filter on top of it, that will be triggered based on the facial expressions of the person in real-time.
Currently, our face expression recognizer can check whether the eyes and mouth are open 😯
or not 😌
so to get the most out of it, we can overlay scalable eyes 👀
images on top of the eyes of the user when his eyes are open and a video of fire 🔥
coming out of the mouth of the user when the mouth is open.
Create a Function to Overlay the Image Filters
Now we will create a function overlay()
that will apply the filters on top of the eyes and mouth of a person in images/videos utilizing the facial landmarks to locate the face parts and will also resize the filter images according to the size of the face part on which the filter images will be overlayed.
def overlay(image, filter_img, face_landmarks, face_part, INDEXES, display=True): ''' This function will overlay a filter image over a face part of a person in the image/frame. Args: image: The image of a person on which the filter image will be overlayed. filter_img: The filter image that is needed to be overlayed on the image of the person. face_landmarks: The facial landmarks of the person in the image. face_part: The name of the face part on which the filter image will be overlayed. INDEXES: The indexes of landmarks of the face part. display: A boolean value that is if set to true the function displays the annotated image and returns nothing. Returns: annotated_image: The image with the overlayed filter on the top of the specified face part. ''' # Create a copy of the image to overlay filter image on. annotated_image = image.copy() # Errors can come when it resizes the filter image to a too small or a too large size . # So use a try block to avoid application crashing. try: # Get the width and height of filter image. filter_img_height, filter_img_width, _ = filter_img.shape # Get the height of the face part on which we will overlay the filter image. _, face_part_height, landmarks = getSize(image, face_landmarks, INDEXES) # Specify the height to which the filter image is required to be resized. required_height = int(face_part_height*2.5) # Resize the filter image to the required height, while keeping the aspect ratio constant. resized_filter_img = cv2.resize(filter_img, (int(filter_img_width* (required_height/filter_img_height)), required_height)) # Get the new width and height of filter image. filter_img_height, filter_img_width, _ = resized_filter_img.shape # Convert the image to grayscale and apply the threshold to get the mask image. _, filter_img_mask = cv2.threshold(cv2.cvtColor(resized_filter_img, cv2.COLOR_BGR2GRAY), 25, 255, cv2.THRESH_BINARY_INV) # Calculate the center of the face part. center = landmarks.mean(axis=0).astype("int") # Check if the face part is mouth. if face_part == 'MOUTH': # Calculate the location where the smoke filter will be placed. location = (int(center[0] - filter_img_width / 3), int(center[1])) # Otherwise if the face part is an eye. else: # Calculate the location where the eye filter image will be placed. location = (int(center[0]-filter_img_width/2), int(center[1]-filter_img_height/2)) # Retrieve the region of interest from the image where the filter image will be placed. ROI = image[location[1]: location[1] + filter_img_height, location[0]: location[0] + filter_img_width] # Perform Bitwise-AND operation. This will set the pixel values of the region where, # filter image will be placed to zero. resultant_image = cv2.bitwise_and(ROI, ROI, mask=filter_img_mask) # Add the resultant image and the resized filter image. # This will update the pixel values of the resultant image at the indexes where # pixel values are zero, to the pixel values of the filter image. resultant_image = cv2.add(resultant_image, resized_filter_img) # Update the image's region of interest with resultant image. annotated_image[location[1]: location[1] + filter_img_height, location[0]: location[0] + filter_img_width] = resultant_image # Catch and handle the error(s). except Exception as e: pass # Check if the annotated image is specified to be displayed. if display: # Display the annotated image. plt.figure(figsize=[10,10]) plt.imshow(annotated_image[:,:,::-1]);plt.title("Output Image");plt.axis('off'); # Otherwise else: # Return the annotated image. return annotated_image
Snapchat Filter on Real-Time Webcam Feed
Now we will utilize the function overlay()
created above to apply filters based on the facial expressions, that we will recognize utilizing the function isOpen()
on a real-time webcam feed.
# Initialize the VideoCapture object to read from the webcam. camera_video = cv2.VideoCapture(2) camera_video.set(3,1280) camera_video.set(4,960) # Create named window for resizing purposes. cv2.namedWindow('Face Filter', cv2.WINDOW_NORMAL) # Read the left and right eyes images. left_eye = cv2.imread('media/left_eye.png') right_eye = cv2.imread('media/right_eye.png') # Initialize the VideoCapture object to read from the smoke animation video stored in the disk. smoke_animation = cv2.VideoCapture('media/smoke_animation.mp4') # Set the smoke animation video frame counter to zero. smoke_frame_counter = 0 # Iterate until the webcam is accessed successfully. while camera_video.isOpened(): # Read a frame. ok, frame = camera_video.read() # Check if frame is not read properly then continue to the next iteration to read # the next frame. if not ok: continue # Read a frame from smoke animation video _, smoke_frame = smoke_animation.read() # Increment the smoke animation video frame counter. smoke_frame_counter += 1 # Check if the current frame is the last frame of the smoke animation video. if smoke_frame_counter == smoke_animation.get(cv2.CAP_PROP_FRAME_COUNT): # Set the current frame position to first frame to restart the video. smoke_animation.set(cv2.CAP_PROP_POS_FRAMES, 0) # Set the smoke animation video frame counter to zero. smoke_frame_counter = 0 # Flip the frame horizontally for natural (selfie-view) visualization. frame = cv2.flip(frame, 1) # Perform Face landmarks detection. _, face_mesh_results = detectFacialLandmarks(frame, face_mesh_videos, display=False) # Check if facial landmarks are found. if face_mesh_results.multi_face_landmarks: # Get the mouth isOpen status of the person in the frame. _, mouth_status = isOpen(frame, face_mesh_results, 'MOUTH', threshold=15, display=False) # Get the left eye isOpen status of the person in the frame. _, left_eye_status = isOpen(frame, face_mesh_results, 'LEFT EYE', threshold=4.5 , display=False) # Get the right eye isOpen status of the person in the frame. _, right_eye_status = isOpen(frame, face_mesh_results, 'RIGHT EYE', threshold=4.5, display=False) # Iterate over the found faces. for face_num, face_landmarks in enumerate(face_mesh_results.multi_face_landmarks): # Check if the left eye of the face is open. if left_eye_status[face_num] == 'OPEN': # Overlay the left eye image on the frame at the appropriate location. frame = overlay(frame, left_eye, face_landmarks, 'LEFT EYE', mp_face_mesh.FACEMESH_LEFT_EYE, display=False) # Check if the right eye of the face is open. if right_eye_status[face_num] == 'OPEN': # Overlay the right eye image on the frame at the appropriate location. frame = overlay(frame, right_eye, face_landmarks, 'RIGHT EYE', mp_face_mesh.FACEMESH_RIGHT_EYE, display=False) # Check if the mouth of the face is open. if mouth_status[face_num] == 'OPEN': # Overlay the smoke animation on the frame at the appropriate location. frame = overlay(frame, smoke_frame, face_landmarks, 'MOUTH', mp_face_mesh.FACEMESH_LIPS, display=False) # Display the frame. cv2.imshow('Face Filter', frame) # Wait for 1ms. If a key is pressed, retreive the ASCII code of the key. k = cv2.waitKey(1) & 0xFF # Check if 'ESC' is pressed and break the loop. if(k == 27): break # Release the VideoCapture Object and close the windows. camera_video.release() cv2.destroyAllWindows()
Output
Cool! I am impressed by the results now if you want, you can extend the application and add more filters like glasses, nose, and ears, etc. and use some other facial expressions to trigger those filters.
Join My Course Computer Vision For Building Cutting Edge Applications Course
The only course out there that goes beyond basic AI Applications and teaches you how to create next-level apps that utilize physics, deep learning, classical image processing, hand and body gestures. Don’t miss your chance to level up and take your career to new heights
You’ll Learn about:
- Creating GUI interfaces for python AI scripts.
- Creating .exe DL applications
- Using a Physics library in Python & integrating it with AI
- Advance Image Processing Skills
- Advance Gesture Recognition with Mediapipe
- Task Automation with AI & CV
- Training an SVM machine Learning Model.
- Creating & Cleaning an ML dataset from scratch.
- Training DL models & how to use CNN’s & LSTMS.
- Creating 10 Advance AI/CV Applications
- & More
Whether you’re a seasoned AI professional or someone just looking to start out in AI, this is the course that will teach you, how to Architect & Build complex, real world and thrilling AI applications
Summary:
Today, in this tutorial, we learned about a very common computer vision task called Face landmarks detection. First, we covered what exactly it is, along with its applications, and then we moved to the implementation details of the solution provided by Mediapipe and how it uses a 2-step (detection + tracking) pipeline to speed up the process.
After that, we performed multi-face detection and 3D face landmarks detection using Mediapipe’s solutions on images and real-time webcam feed.
Then we learned to recognize the facial expressions in the images/videos utilizing the face landmarks and after that, we learned to apply face filters, which were dynamically controlled by the facial expressions in the images/videos.
Alright here are a few limitations of our application that you should know about, the face expression recognizer we created is really basic to recognize dedicated expressions like shock, surprise. For that, you should train a DL model on top of these landmarks.
Another current limitation is that the face filters are not currently being rotated with the rotations of the faces in the images/videos. This can be overcome simply by calculating the face angle and rotating the filter images with the face angle. I am planning to cover this and a lot more in my upcoming course mentioned above.
You can reach out to me personally for a 1 on 1 consultation session in AI/computer vision regarding your project. Our talented team of vision engineers will help you every step of the way. Get on a call with me directly here.
Ready to seriously dive into State of the Art AI & Computer Vision?
Then Sign up for these premium Courses by Bleed AI
great work
Thanks Mahwiz, 🙂
Excellent Work
Thanks Jibran 🙂
How to i replace the Eye image with a 3d file can you tell please?
For rendering 3d Accessories you’ll use a 3d lib in python like OpenGL but in my exp python is not a great language to render 3d objects, for that Unity is Great.
Hi,
I am currently working on a similar project.
i just want to observe the eye and mouth state then show the status in text on the screen. Can I do it realtime?
Hi yes, you can monitor open/close status using this code in real-time.
great work. İm trying to create and project for both facial recognition and activity recognition. im using dlib and ~/face_recognition libs but is there any shorcut in mediapipe that can detect a difference bw me and the others… i mean maybe space between eyes or mount and eyes etc. İs there any ? i want it to work only when i said so, not someone else..
Thanks, for this, you’ll have to use a facial recognition approach, any other hacky solution is prone to fail
Great work. it’s very useful but i have a little problem. The function getSize() works fine, except for the cv2.boundingRect() that always give me 1 as result of the height. I’m trying to understand why, and some answers can be a good help.
Tnx for your work 🙂
Thank you gatre. I have checked the issue you have mentioned but the cv2.boundingRect() function is returning the correct height of the face parts for me. I think you have mistakenly edited the jupyter notebook which is causing this issue.