<jupyter_start><jupyter_text>**Objective and problem statement**>  **The coronavirus: why do we need stricter measures?**The coronavirus was declared a pandemic by WHO on March 11th, 2020, and by that time, we already had 118,000 confirmed cases and had spread to 114 countries around the globe. Since then, due to a variety of reasons some of which include strict measures not being put into place and government negligence, we have seen these figures blown out of proportions.  Currently, at the time of writing this, we have 13.7 million confirmed cases and near 586,000 deaths worldwide. While some factors can be eluded as being not being under our control as individuals, we cannot say that face masks do not have a big role to play in controlling this pandemic.>  - **So, why are masks effective?** (***and why is it not a personal choice to wear masks in public places?***)....Upon wearing the face mask, we as the source of infection reduce the number of droplets ejected by 99 percent according to confirmed studies. It also reduces the intake of number of droplets by an uninfected individual, though not as effective. Yet, if we reduce the number of people getting infected, it also reduces the effective reproduction rate, and hence having an impact of exponential margins.Nearly half of the infected people do not show symptoms as per recent studies, which can take up to a period of 14 days to appear in an infected individual. Hence, it is really necessary for wearing masks by people in public places, and should really be made mandatory rather than being based on individual decisions, as a significant portion of people with infection lack coronavirus symptoms.We cannot wait for a controlled trial of a vaccination to be successful for us to be taking all the preventive steps to protect ourselves and others, while contributing to stop the flow of this disease. A successful vaccination won't come till at least the mid of next year, and we cannot ascertain that it would immediately be made publicly available even then by every nation. It can be safely stated that wearing masks is not a personal choice as it can put us and people around us at risk of infection.<jupyter_code># Training data for the classification model (Mask vs Without Mask Data)
!gdown --id 1nmLc8_Xgj7jFBffjKqsMXYXWZpiTzgut
!unzip /content/Dataset.zip
# Testing images for mask detection
!gdown --id 1H3anHpuKHOJBfOU_AUZ4vci9IsCeA8Zp
!unzip /content/CrowdMaskDetection.zip 
!pip install fdet==0.2.1
!pip install youtube-dl
!pip install -U kora
# Downloaing testing video (Change the link for testing more videos)
!wget https://media.istockphoto.com/videos/crowd-people-using-mask-video-id1213759078<jupyter_output><empty_output><jupyter_text>**Importing Libraries**<jupyter_code>import cv2
import tensorflow as tf
import numpy as np
from glob import glob
import matplotlib.pyplot as plt
from fdet import io, MTCNN, RetinaFace
from fdet.utils.io import VideoHandle
from sklearn.metrics import classification_report
from google.colab.patches import cv2_imshow
from sklearn.model_selection import train_test_split
from kora.drive import upload_public
from IPython.display import HTML<jupyter_output><empty_output><jupyter_text>**Constants**<jupyter_code>batch_size = 32
epoch = 2
num_classes = 2
step_per_epoch = len(glob('/content/Dataset/*/*'))//batch_size

image_h, image_w = 128, 128
classes = sorted(['with_mask','without_mask'])<jupyter_output><empty_output><jupyter_text>**Data Collection and Preprocessing**There were issues circumventing the data collection process, primarily the unavailability of data specific to detecting face masks as detection problem. The images did not have localized bounding boxes, and did not have category labels for the masked and without mask faces of people in the image. Due to this reason, I had to use the approach for face detection and followed by the classification of faces into mask and without mask categories.This section can be divided into two parts, with the first part focusing solely on the part of data collection for training examples, followed by the part where I discuss the collection of testing image and video examples. This was done in separate stages to test the proposed approach in a real world crowded setting of people, the use-case for which my algorithm of mask detection is proposed, which would not have been ideal in a setting wherein the training and testing data came from the same source. **1. Data Collection for Training examples**As discussed earlier, there were various issues which occurred during the collection process for training examples, due to the unavailability of datasets specific to face mask detection. A solution to the same would have been using facial landmarks to detect important indicators for faces, but this idea was not used in our implementation. Finally, it was decided that my best option would be to use a data classified into two categories of images: images of people wearing masks and the ones without mask. It is important to note that the people wearing masks on their face in an incorrect manner would be classified into the "without mask" category. Data collection was done using a variety of sources from interfaces such as Kaggle API and Bing Search. As an additional source, the Real World Masked Face Detection (RMFD) dataset was used to facilitate training of our mask detection algorithm using the classification based approach of detecting masks followed by predicting whether the detected face contains a mask or not.The total number of images in the training set were divided into the two categories as follows:1. With mask: 8072 images2. Without mask: 8086 images **2. Data Collection for Inference**The images and video sequence collected for the purpose of testing our model had to come from a crowded setting to provide a realistic test scenario for my proposed model for mask detection. This task was accomplished using using Google search for images and available YouTube videos for the purpose of scraping the web for realistic examples suited to inference in a real world surveillance setting.  **Reading Image**- We need to apply transformations on the input image before defining inputs for the model architecture.- Transformations are done so as to ensure that the inputs to the network are of the right size and respective values sit in a similar range.- Input image was resized to (128,128) for (height, width) and values in the image tensor were scaled to the range of (-1,1).- Entire dataset was split into training and test set for cross validation. Split ratio was chose such that 90 percent of the dataset is part of the training set and 10 percent for test set.<jupyter_code>def read_img(image_path):
  img = tf.io.read_file(image_path)
  img = tf.image.decode_image(img, channels=3)
  img.set_shape([None,None,3])
  img = tf.image.resize(img, [image_w, image_h])
  img  = img/127.5-1
  return img

label_map = {v:i for i, v in enumerate(classes)}

images = glob('/content/Dataset/*/*')
np.random.shuffle(images)

labels = [label_map[x.split('/')[-2]] for x in images]

(train_images, test_images, train_labels, test_labels) = train_test_split(images, labels,
	test_size=0.10, stratify=labels, random_state=42)

#reading image and label
def load_data(image_path, label):
  image = read_img(image_path)
  return image, label<jupyter_output><empty_output><jupyter_text>**Data Generator**- Using a data generator, we can batch individual samples into a unified set.- Data generator spawns processes for loading data to input while training the model.- This function also provides parallel loading for batches in the training data using shared memory and separate process.<jupyter_code>def data_generator(features,labels):
  dataset = tf.data.Dataset.from_tensor_slices((features,labels))
  dataset = dataset.shuffle(buffer_size=100)
  autotune = tf.data.experimental.AUTOTUNE
  dataset = dataset.map(load_data, num_parallel_calls=autotune)
  dataset = dataset.batch(batch_size=batch_size)
  dataset = dataset.repeat()
  dataset = dataset.prefetch(autotune)
  return dataset<jupyter_output><empty_output><jupyter_text>**Visualization of Classification Training Data**<jupyter_code>def show_img(dataset):
  plt.figure(figsize=(15,15))
  for i in range(8):
    for val in dataset.take(1):
      img  = (val[0][i]+1)*127.5
      plt.subplot(4,2,i+1)
      plt.imshow(tf.cast(img,tf.uint8))
      plt.title(val[1][i].numpy())
      plt.subplots_adjust(hspace=1)
  plt.show()

train_dataset = data_generator(train_images,train_labels)

show_img(train_dataset)<jupyter_output><empty_output><jupyter_text>**Proposed model for Face Mask Detection**As discussed earlier, the mask detection model can be said to be a combination of classification and face detection model.For the purpose of classification, we use transfer learning with an Xception model trained on the ImageNet dataset with a modified final fully connected layer. While using the face detection model, several different approaches were tried upon based on existing literature, and the one which worked the best was a RetinaNet Face pre-trained model which gave the highest measures of recall while experimenting on different use-cases and testing images of people in a crowded setting.The models and implementation details for them have been discussed in an objective manner as part of this section, and while providing an insight on the approach used (and why it was chosen in the first place), we delve into our final mask detection model which was built using a combination of the classification and face detection models as were briefly described above.  **Model for Classification**: **Xception**A classification problem is, using available training data with defined features and class labels, build a function which can, with high levels of certainty categorize a new unseen set of data into one of the classes. Having only a limited amount of data available for training the classifier, I was inclined to use transfer learning for the purpose of our task of classifying an input image into the categories of whether the subject is wearing a mask or not. The transfer learning technique is probably one of the most revolutionary ideas to have come out in the past few years, and can be thought of reusing a pre-trained model, which is trained on another set of input images, which in our case would be the use of Xception model trained on the ImageNet database. ***A short introduction to the Xception model***:It is called an "extreme version of its predecessor Inception V3" for the reason that an efficient use of model parameters by using depth-wise separable convolutions gives better performance on the ImageNet datab. While keeping the number of parameters same in the Inception V3 and Xception model, we ensure that the capacity of the model remains the same but yielding better performance on an almost unparalleled scale, than was achieved using Inception V3 model.Some properties of the Xception model chosen in our case are discussed below:1. Shape invariance: similar results irrespective of the dimensions of input images (by using Global Avg pooling instaed of flatten) .2. Removal of final fully connected layer: Prior layers to the final fully connected layer were trained using the weights of Xception trained on the ImageNet database.<jupyter_code>base_model = tf.keras.applications.Xception(include_top=False,
                                       input_shape=(None, None, 3),
                                       weights='imagenet')
base_model.trainable = False
layer = tf.keras.layers.GlobalAveragePooling2D()(base_model.output)
layer = tf.keras.layers.Dense(1024, activation='relu')(layer)
layer = tf.keras.layers.Dropout(0.5)(layer)
output = tf.keras.layers.Dense(num_classes, activation='softmax')(layer)
model = tf.keras.models.Model(base_model.inputs,output)
model.summary()<jupyter_output>Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/xception/xception_weights_tf_dim_ordering_tf_kernels_notop.h5
83689472/83683744 [==============================] - 1s 0us/step
Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, None, None,  0                                            
__________________________________________________________________________________________________
block1_conv1 (Conv2D)           (None, None, None, 3 864         input_1[0][0]                    
__________________________________________________________________________________________________
block1_conv1_bn (BatchNormaliza (None, None, None, 3 128         block1_conv1[0][0[...]<jupyter_text>**Model fitting**Implementation details for classification architecture:- ***Batch Size***: 32 - ***Epochs***: 2 - ***Learning rate***: 1e-4 (with decay of 1e-4 / epoch)- ***Gradient Descent Optimizer***: Adam- ***Loss function***: Sparse categorical cross entropy- ***Criterion for evaluation*** (metric): F1-score<jupyter_code>#We can use learning rate scheduler here like Cyclical Learning Rate(available in Tf Addons)
model.compile(optimizer=tf.keras.optimizers.Adam(1e-4, decay=1e-4 / epoch),
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

callbacks = [
    tf.keras.callbacks.ModelCheckpoint('mask_classification_model.h5', 
                                       save_best_only=True, 
                                       save_weights_only=True,
                                       monitor='loss')]

model.fit(train_dataset,
          batch_size=batch_size,
          epochs=epoch,steps_per_epoch=step_per_epoch,
          callbacks=callbacks)<jupyter_output>Epoch 1/2
504/504 [==============================] - 28s 56ms/step - loss: 0.0410 - accuracy: 0.9866
Epoch 2/2
504/504 [==============================] - 29s 59ms/step - loss: 0.0158 - accuracy: 0.9954<jupyter_text>**Testing the classification model**<jupyter_code>plt.figure(figsize=(15,15))
for i in range(8):
  for val in train_dataset.take(1):
    plt.subplot(4,2,i+1)
    img  = (val[0][i]+1)*127.5
    plt.imshow(tf.cast(img,tf.uint8))
    y_pred = model.predict(np.expand_dims(val[0][i],axis=0))
    y_pred = np.argmax(y_pred,axis=1)
    plt.title('Ground Truth{}, Predicted{}'.format(val[1][i],y_pred))
    plt.subplots_adjust(wspace=1, hspace=1)
plt.show()<jupyter_output><empty_output><jupyter_text>**Evaluation/Performance Metric**<jupyter_code>pred_labels = []
for image_path in (test_images):
  image = read_img(image_path)
  y_pred = model.predict(np.expand_dims(image,axis=0))
  y_pred = np.argmax(y_pred,axis=1)
  pred_labels.append(y_pred)

print(classification_report(test_labels, pred_labels))<jupyter_output>precision    recall  f1-score   support

           0       0.99      0.99      0.99       807
           1       0.99      0.99      0.99       809

    accuracy                           0.99      1616
   macro avg       0.99      0.99      0.99      1616
weighted avg       0.99      0.99      0.99      1616<jupyter_text>**Face Detection: RetinaNet Face**Face Detection is the technique of identifying human faces in digital images. While inherently a backbone to other applications, detecting a face is in fact impacted a lot in cluttered scenes, and examples of the same kind of problem can be thought of in a crowded setting, the use-case for which our mask detection algorithm is being built. For this reason, having tried various techniques from classical Computer Vision domain to using deep learning techniques, I needed to prioritize the mAP metric of detected faces in a crowded setting. Having said that, I tried techniques for face detection such as Haar cascading and MT-CNN which did not achieve a high recall. Finally, I sided with a pre-trained RetinaNet Face model, using focal loss which is able to handle the foreground-background class imbalance (an issue with one stage detectors which makes performance of single shot detectors inferior to two stage detectors for object detection) in the detected classes pretty well. ***An brief overview on the RetinaNet model for object detection:***- RetinaNet uses focal loss which focuses training on hard negatives, while down-weighting easy examples, using parameters alpha and gamma which provide the hyper-parameters for focusing on hard negatives and offset class imbalance of the number of examples. - As a backbone, the ResNet architecture is used to detect features from input images and a feature pyramid network is used as a fast and multiscale feature extractor on top of this backbone to create a multi-scale pyramid of features. - RetinaNet is able to outperform single stage detectors for object detection such as Single Shot Multibox Detectors, while attaining the accuracy of two stage detectors such as FPN Faster-RCNN with a ResNet-101 backbone.<jupyter_code>class_map = {
    0: 'With mask',
    1: 'Without mask'
    }

color_map_video = {
    0: (0,255,0),
    1:(0,0,255)
    }

color_map_image = {
    0: [0,1,0],
    1:[1,0,0]
    }<jupyter_output><empty_output><jupyter_text>**Improving the precision and recall** I was able to improve the precision and recall of my model by a high margin in the following ways:- Resizing the cropped face before providing as input to the classification model.- For cases wherein the dimensions for height and width of the face crops fall below a threshold, increasing the dimensions of the crop by some proportion.<jupyter_code>def visualize_detections(image, boxes):
  
  figsize=(7, 7)
  linewidth=1

  image = np.array(image, dtype=np.uint8)

  plt.figure(figsize=figsize)
  plt.axis('off')
  plt.imshow(image)

  ax = plt.gca()

  for box in boxes:
    x, y, w, h = box
  
    face_image = image[y:y+h,x:x+w]
    
    #To handle those cases where the  height and width of the generated cropped face become 0
    if face_image.shape[0] and face_image.shape[1]:

      face_image = tf.image.resize(face_image, [image_w, image_h])
      face_image = face_image/127.5-1

      _cls = model.predict(np.expand_dims(face_image,axis=0))
      _cls = np.argmax(_cls,axis=1)
      
      text = '{}'.format(class_map[_cls[0]])

      patch = plt.Rectangle([x, y], w, h, fill=False, 
                            edgecolor=color_map_image[_cls[0]], linewidth=linewidth)
      ax.add_patch(patch)
      ax.text(x, y, text, bbox={'facecolor':color_map_image[_cls[0]], 'alpha':0.2}, 
          clip_box=ax.clipbox, clip_on=True)
  

def visualize_detections_video(image, boxes):
  
  image = np.array(image, dtype=np.uint8)
  
  for box in boxes:
    x, y, w, h = box    
    face_image = image[y:y+h,x:x+w] 
    if w<90 or h<90:
      w = (w*0.5)+w
      h = (h*0.5)+h

    #To handle those cases where the  height and width of the generated cropped face become 0
    if face_image.shape[0] and face_image.shape[1]:
      face_image = tf.image.resize(face_image, [image_w, image_h])
      face_image = face_image/127.5-1
      
      _cls = model.predict(np.expand_dims(face_image,axis=0))
      _cls = np.argmax(_cls,axis=1)

      text = '{}'.format(class_map[_cls[0]])    
      start = tuple(np.array((x,y)).astype('int'))
      end = tuple(np.array((x+w,y+h)).astype('int'))
      
      cv2.rectangle(image,start,end,color_map_video[_cls[0]],1)
      cv2.putText(image, class_map[_cls[0]], start , cv2.FONT_HERSHEY_SIMPLEX, 0.4, color_map_video[_cls[0]], 1)
  return (image)<jupyter_output><empty_output><jupyter_text>**Proposed Model for Face Mask Detection:**On any given test image of a crowd based setting of people, our final mask detection model runs as follows:Apply the RetinaNet Face model for face detection to generate detected face crops from the input image.Xception model for classification into mask and no-mask categories for the detected face is applied upon the detections generated by RetinaNet model.The final output of these two would be the faces detected by RetinaNet along with the predicted category for each face, that is whether the subject is wearing a mask or not. >  **Testing on images of densely crowded places**<jupyter_code>detector = RetinaFace(backbone='RESNET50')

image_path = '/content/CrowdMaskDetection/*'

for i in glob(image_path):
  image = cv2.cvtColor(cv2.imread(i), cv2.COLOR_BGR2RGB)
  
  result = detector.detect(image)

  boxes = []
  for i in range(len(result)):
    boxes.append(result[i]['box'])
  boxes = np.array(boxes)

  visualize_detections(image, boxes)<jupyter_output><empty_output><jupyter_text>>  **Testing on videos of densely crowded places**<jupyter_code>video_source = "/content/crowd-people-using-mask-video-id1213759078"

cap = cv2.VideoCapture(video_source)
img_array = []

while True:
  _, image = cap.read()
  if not _:
    break
  w, h = image.shape[0], image.shape[1] 
  result = detector.detect(image)

  boxes = []
  for i in range(len(result)):
    boxes.append(result[i]['box'])
  boxes = np.array(boxes)
  img=visualize_detections_video(image, boxes)
  img_array.append(img)
 
out = cv2.VideoWriter('/content/mask.avi', 0, 24, (h,w))

for i in range(len(img_array)):
  out.write(img_array[i])

out.release()
#For convertng avi to mp4
!ffmpeg -i /content/mask.avi output.mp4
#You are required to sign in using your Google Id before running this cell

url = upload_public('/content/output.mp4')
HTML(f"""<video src={url} width=500 controls/>""")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08286756894082868, 'fraction_numerical': 0.01926185967019262, 'mean_word_length': 4.360237682594701, 'pattern_counts': {'":': 0, '<': 57, '<?xml version=': 0, '>': 59, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '113036', 'license': 'MIT', 'n_tokens_mistral': 6165, 'n_tokens_neox': 5813, 'n_words': 2479, 'path': 'Face-Mask-Detection/Face-Mask-Detection-using Xception/Notebook/Face_Mask_Detection.ipynb', 'repo_name': 'hercules261188/Face-X', 'size': '21649'}