<jupyter_start><jupyter_text>Импорт пакетов- **opencv** - пакет для Computer Vision и дата препроцессинага для него.- **sys** - пакет для работы с выводом на консоль.- **numpy** - пакет для работы с массивами данных.- **os** - пакет для работы с операционной системой, используется в связке с sys.- **inspect** - пакет помогает извлекать информацию из лайвв объектов, таких как классы, функции и т.т.п.- **skimage** - пакет алгоритмов, предназначенных для работы с изображениями, в особенности с их препроцессингом. Дополнение к opencv. От сюда извлекаем модуль ***io*** (Утилиты для чтения и записи изображений в различных форматаx) и функцию ***resize*** (Изменяет размер изображения до указанного).- **matplotlib.pyplot** - модуль для графического отображения данных.- **IPython** - от сюда извлекается функция, которая помогает очищать вывод в jupyter.- **pandas** - пакет для обработки и анализа данных.<jupyter_code>%matplotlib inline
import cv2
import sys
import numpy as np
import os
import inspect
from skimage import io
import matplotlib.pyplot as plt
from skimage.transform import resize
from IPython.display import clear_output
import pandas as pd<jupyter_output><empty_output><jupyter_text>Глобальные переменные1. **img_size** - размер изображения, на выходе. Данный размер должен совпадать с размером изображений на котых обучается наша CNN.2. **faces_in_image_limit** - количество лиц, на одной фотографии.<jupyter_code>img_size = 256
faces_in_image_limit = 1<jupyter_output><empty_output><jupyter_text>Основной алгоритм. SSD: Single Shot MultiBox DetectorВыбор пал на SSD, т.к. по сравнению с Haar Cascade данный алгоритм работает в разы лучше. В дополнение он быстрее Faster R-CNN, а перформансы у них очень близки друг к другу. Сравнение алгоритмов можно найти [тут](https://towardsdatascience.com/face-detection-models-which-to-use-and-why-d263e82c302c). Полное описание SSD от их авторов можно найти на [arxiv'e](https://arxiv.org/pdf/1512.02325.pdf) наглядное описание можно посмотреть [тут](https://pythonawesome.com/ssd-single-shot-multibox-detector-a-pytorch-tutorial-to-object-detection/). Основная логикаНа вход поступает N 3-х канальных RGB картинок $X^T \in \mathbb{R}^N$, где $x^T = (w, h, 3)$. В зависимости от типа SSD нужно масшатабировать картинку, чтобы она была пригодна для input-layer. В нашем случае $x^T = (300, 300, 3)$. Далее каждая картинка прогоняется через свёрточные слои. Авторы использовали немного модифицированную VGG16.$$\begin{aligned}&\tilde{x}_1 \sim MaxPool_1(Conv_{1_2}(Conv_{1_1}(x)))\\&\tilde{x}_2 \sim MaxPool_2(Conv_{2_2}(Conv_{2_1}(\tilde{x}_1)))\\&\tilde{x}_3 \sim MaxPool_3(Conv_{3_3}(Conv_{3_2}(Conv_{3_1}(\tilde{x}_2))))\\&\tilde{x}_4 \sim MaxPool_4(Conv_{4_3}(Conv_{4_2}(Conv_{4_1}(\tilde{x}_3))))\\\end{aligned}$$Здесь, приметим, что $\tilde{x}_4^T \in \mathbb{R}^{38 \times 38 \times 512}$.$$\begin{aligned}&\tilde{x}_5 \sim MaxPool_5(Conv_{5_3}(Conv_{5_2}(Conv_{5_1}(\tilde{x}_4))))\\&\tilde{x}_6 \sim Conv_{6}(\tilde{x}_5)\\&\tilde{x}_7 \sim Conv_{7}(\tilde{x}_6)\end{aligned}$$Таким образом мы сможем извлечь даже мелкие детали. На данный момент $\tilde{x}_7^T \in \mathbb{R}^{19 \times 19 \times 1024}$.Далее на топ архитектуры добавляются вспомогательные слои:$$\tilde{x}_7^T \in \mathbb{R}^{19 \times 19 \times 1024} \rightarrow \underbrace{Conv_{8_2}(Conv_{8_1}(\tilde{x}_7))}_{\tilde{x}_8^T \in \mathbb{R}^{10 \times 10 \times 512}} \rightarrow \underbrace{Conv_{9_2}(Conv_{9_1}(\tilde{x}_8))}_{\tilde{x}_9^T \in \mathbb{R}^{5 \times 5 \times 256}} \rightarrow \underbrace{Conv_{10_2}(Conv_{10_1}(\tilde{x}_9))}_{\tilde{x}_{10}^T \in \mathbb{R}^{3 \times 3 \times 256}} \rightarrow \underbrace{Conv_{11_2}(Conv_{11_1}(\tilde{x}_{10}))}_{\tilde{x}_{11}^T \in \mathbb{R}^{1 \times 1 \times 256}}$$У нас есть 6 помеченных слоёв, на которых мы будем проводить детекцию с разным уровнем детализации. Теперь на свёрнутую картинку накладываются дефолтные [AnchorBoxes](https://www.mathworks.com/help/vision/ug/anchor-boxes-for-object-detection.html) (Приоры):* **Приоры накладываются на помеченные карты характеристик $x$**.* **Каждый приор имеет масштаб $s$, тогда площадь данного приора равна площади квадрата со стороной $\sqrt{s}$**. Например для самой большой карты характеристик (КХ) $\tilde{x}_4$ масштаб приора будет 10\% от размерности изображения. Для следующей КХ ($\tilde{x}_7$) 20\%, для следующей 30\% и так до 90\%.* **В каждой ячейке карты характеристик, находятся несколько приор с разным соотношением сторон**. Все КХ будут иметь приоры с соотношением сторон $\frac{1}{1},\ \frac{1}{2},\ \frac{2}{1}$. Промежуточные КХ ($\tilde{x}_7,\ \tilde{x}_8,\ \tilde{x}_9$) в дополнение имеют $\frac{1}{3},\ \frac{3}{2}$ приоры. И напоследок, каждая КХ имеет дополнительный приор $\frac{1}{1}$.| Карта характеристик |   Размерность  | Масштаб приора |                                  Соотношения сторон                                 | Количество приор на одну ячейку | Общее количество приор на КХ ||:-------------------:|:--------------:|:--------------:|:-----------------------------------------------------------------------------------:|:-------------------------------:|:----------------------------:||    $\tilde{x}_4$    | $38 \times 38$ |       0.1      |               $\frac{1}{1};\ \frac{1}{2};\ \frac{2}{1};$ + доп. приор               |                4                |             5766             ||    $\tilde{x}_7$    | $19 \times 19$ |       0.2      |               $\frac{1}{1};\ \frac{1}{2};\ \frac{2}{1};$ + доп. приор               |                6                |             2166             ||    $\tilde{x}_8$    | $10 \times 10$ |      0.375     | $\frac{1}{1};\ \frac{1}{2};\ \frac{2}{1};\ \frac{1}{3};\ \frac{3}{2};$ + доп. приор |                6                |              600             ||    $\tilde{x}_9$    |  $5 \times 5$  |      0.55      | $\frac{1}{1};\ \frac{1}{2};\ \frac{2}{1};\ \frac{1}{3};\ \frac{3}{2};$ + доп. приор |                6                |              150             ||   $\tilde{x}_{10}$  |  $3 \times 3$  |      0.725     |               $\frac{1}{1};\ \frac{1}{2};\ \frac{2}{1};$ + доп. приор               |                4                |              36              ||   $\tilde{x}_{11}$  |  $1 \times 1$  |       0.9      |               $\frac{1}{1};\ \frac{1}{2};\ \frac{2}{1};$ + доп. приор               |                4                |               4              ||   **Общее количество**  |        -       |        -       |                                          -                                          |                -                |          **8732 приора**         |Приоры объявлены с помощью масштабирующего фактора и соотношения сторон1. Со следующими, масштобирующем фактором и соотношением сторон:$$\begin{aligned}&w \cdot h = s^2\\&\frac{w}{h} = a\end{aligned}$$2. От сюда получаем:$$\begin{aligned}&w = s \cdot \sqrt{a}\\&h = \frac{s}{\sqrt{a}}\end{aligned}$$Для каждого приора расчитывается вероятность (confidence) для каждого объекта. В нашем случае только лицо, т.е. какова вероятность, что в пределах приора находится лицо. Далее мы пытаемся отрегулировать данный приор таким образом, чтобы данная вероятность была максимальной. Тем самым мы получаем следующие сдвиги.$$\begin{aligned}&g_{{c}_{x}} = \frac{c_x - \hat{с}_x}{\hat{w}}\\&g_{{c}_{y}} = \frac{c_y - \hat{с}_y}{\hat{h}}\\&g_w = \log{\left(\frac{w}{\hat{w}}\right)}\\&g_h = \log{\left(\frac{h}{\hat{h}}\right)}\end{aligned}$$где $(c_x, c_y, w, h)$ данные приора с наибольшей вероятностю содержания объекта.Как видите, каждое смещение нормализовано на соответствующий размер приора. Что логично, потому что определенное смещение будет менее значительным для более крупного приора, чем для меньшего.Чтобы определить саму область используется IoU (Intersection over Unioin) $\frac{A \cap B}{A \cup B}$. Если IoU приора и реального AnchorBox'a больше 0.5 это значит совпадение, иначе несовпадение.Для обучения используются следующие лоссы:1. Позиция:$$L_{loc}(x, l, g) = \sum_{i \in Pos}^N{\sum_{m \in \{c_x, c_y, w, h\}}{\mathbb{1}\{IoU(x_{ij}^k) > 0.5\}\text{L1}(l_i^m - \hat{g}_j^m})}$$2. Вероятности:$$L_{conf}(x,c) = - \sum_{i \in Pos}^N{\mathbb{1}\{IoU(x_{ij}^p) > 0.5\}\log{(\hat{c}_i^p)}} - \sum_{i \in N \in g}{\log{(\hat{c}_i^0)}}, \qquad \text{где }\hat{c}_i^p = \frac{e^{c_j^p}}{\sum_p{c_j^p}} $$3. Общая:$$L(x,c,l,g) = \frac{1}{N}(L_{conf}(x,c) + \alpha L_{loc}(x,l,g))$$<jupyter_code>def extract_faces(img):
    """This function extracts a face from a photo.
    
    :param img: the image from which we wanna to derive a face.
    
    :return: np.array of an extracted face and confidence that it is a human face.
    """
    model_file = "utils/opencv_face_detector_uint8.pb"
    config_file = "utils/opencv_face_detector.pbtxt"
    
    # This network has been created for the Caffe and Tensorflow, I used the second one
    net = cv2.dnn.readNetFromTensorflow(model_file, config_file)
    
    # Returning results
    image_data_fin = []
    confidence_res = None
    
    h, w = img.shape[:2]
    
    # https://www.pyimagesearch.com/2017/11/06/deep-learning-opencvs-blobfromimage-works/ blob description
    # First we resize the image to 300x300 according to the pretrained weights
    # Second, the scale factor (standard deviation in the z-scoring), I do not use the scale therefore set it as 1.0
    # Third, mean-tupple of RGB [mu-Red, mu-Green, mu-Blue] 
    # Forth, indicates that swap first and last channels in 3-channel image is necessary.
    # Fifth, indicates whether image will be cropped after resize or not
    blob = cv2.dnn.blobFromImage(cv2.resize(img, (300, 300)), 1.0, (300, 300), [104, 117, 123], False, False)
    
    # pass the blob through the network and obtain the detections and predictions
    net.setInput(blob)
    detections = net.forward()
    
    # loop over the detections
    for i in range(detections.shape[2]):
        # extract the confidence (i.e., probability) associated with the prediction
        # https://docs.opencv.org/trunk/d3/d63/classcv_1_1Mat.html
        confidence = detections[0, 0, i, 2]
        # If confidence is higher than 50% than 
        if confidence > 0.5:
            # compute the (x, y)-coordinates of the bounding box for the object
            box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
            (x, y, x1, y1) = box.astype("int")
            # create a new image (augmented image, in the way to cut off everything except a face)
            roi_color = img[y:y1, x:x1]
            im = resize(roi_color, (img_size, img_size))
            image_data_fin.append(im)
            confidence_res = confidence
    
    # If the only one face on a photo then return it (as np.array) and confidence that it is a human face.
    if len(image_data_fin) != faces_in_image_limit:
        return [], None
    else:
        return image_data_fin, confidence_res<jupyter_output><empty_output><jupyter_text>Дополнительные функции- **print_progress** - рисует прогресбар вида [--->    ] 50%.- **count_files** - считает количество файлов в директории.<jupyter_code>def print_progress(total, current, image, like_type, missing_imgs):
    """This function print progress whereas files are handling.
    
    :param total: total number of files
    :param current: current number of handled files
    :param image: an image's name
    :param like_type: the folder from where we are handling files
    :param missing_imgs: number of files which were missed. It's required in purpose to reflect a percentage properly. 
    """
    def progressBar(current, total, missing_imgs, barLength = 20):
        """Represent a progress bar, like that [--->    ] 50%
        
        :param total: total number of files
        :param current: current number of handled files
        :param missing_imgs: number of files which were missed. It's required in purpose to reflect a percentage properly. 
        :param barLength: required in purpose to show the bar of the same length (default 20 symbols)
        """
        percent = float(current) * 100 / (total - missing_imgs)
        arrow   = '-' * int(percent/100 * barLength) + '>'
        spaces  = ' ' * (barLength - len(arrow))
        sys.stdout.write('\rProgress: [%s%s] %d %%\n' % (arrow, spaces, percent + 1))
        
    sys.stdout.write('\r%d of %d %s files have been handling\n' % (current, total, like_type))
    sys.stdout.write('\rImage: %s\n' % image)
    progressBar(current, total, missing_imgs)
    sys.stdout.flush()

def count_files(path):
    """Count number of files in a folder (missin invisible files, like '.filename')
    
    :param path: path to folder.
    :return: Evaluated number of files
    """
    return len([name for name in path if not name[0] =="."])<jupyter_output><empty_output><jupyter_text>Обрабатывающая функцияОсновная функция, которая обрбатывает фотографии, извлекая из них лица. Итогом она возвращает небольшую статистику по обработанным фотографиям:- **toatal amount** - сколько фотографий обработанно всего.- **missed amount** - количество пропущенных фотографий (из которых не получилось извлечь лица)- **handled ratio** - процент успешно обработанных фотографий.- **handled likes** - количество успешно обработанных фотографий из понравившихся.- **handled dislikes** - количество успешно обработанных фотографий из непонравившихся.<jupyter_code># For each image, we want to know if each picture is attractive or unattractive

# list of images translated into np-array
images = []
# labels to each image
labels = []

def handle_images(name=''):
    """The function process all photos and prepares them for training.
    
    :param name: the name of an user of a folder (name1_like)
    """
    # The directory where this file is placed
    currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
    # Path to the folder with all samples folder
    data_path = os.path.join(os.path.dirname(currentdir), 'samples')
    
    name = name + '_' if name != '' else ''
    
    # List of files in like/dislike directory
    dislikes_images_stack = os.listdir(os.path.join(data_path, name + 'dislike'))
    likes_images_stack = os.listdir(os.path.join(data_path, name + 'like'))

    def process_folder(images_stack, like_type, name=''):
        """The function which processes a folder, by handling images an labeling them.
        
        :param images_stack: a list of images
        :param like_type: the type of folder which is processing.
        :param name: the name beside the like-type in folder name.
        :return: confidence-list (confidence that each passed image is a human face) , number of missed images, 
        files processed, total number of images
        """
        number_of_images = count_files(images_stack)
        files_processed = 0
        confidence_list = []
        number_of_missing_images = 0

        for img in images_stack:
            if not img.startswith('.'):
                # Print progress
                clear_output(wait=True)
                print_progress(number_of_images, files_processed, img, like_type, number_of_missing_images)
                try:
                    # obtain a face 
                    faces, confidence = extract_faces(cv2.imread(os.path.join(data_path, os.path.join(name + like_type, img))))
                except Exception as e:
                    raise e
                
                # Check if the only one face has been retrieved
                if len(faces) > 0 and len(faces) < 2:
                    confidence_list.append(confidence)
                elif len(faces) == 0:
                    number_of_missing_images += 1
                
                # Labeling
                for face in faces:
                    images.append(face)
                    if like_type == 'like':
                        labels.append(1)
                    else:
                        labels.append(0)
                    files_processed += 1
        return confidence_list, number_of_missing_images, files_processed, number_of_images

    # Gather infromation regard the processed files (along with processing)
    conf_list, NoMI, proc_files, NoI = process_folder(dislikes_images_stack, 'dislike', name)
    conf_list2, NoMI2, proc_files2, NoI2 = process_folder(likes_images_stack, 'like', name)
    conf_list.extend(conf_list2)
    conf_list = np.array(conf_list)
    NoMI += NoMI2
    NoI += NoI2
    return {'face_convincing': pd.DataFrame([['{:.2f} %'.format(np.mean(conf_list) * 100)], ['{:.2f} %'.format(np.amax(conf_list) * 100)], ['{:.2f} %'.format(np.amin(conf_list) * 100)], ['{:.2f} %'.format(np.std(conf_list) * 100)]], index=['mean', 'max', 'min', 'std'], columns=['percents']), 'images': pd.DataFrame([[NoI], [NoMI], ['{:.2f} %'.format((NoI - NoMI2)/NoI * 100)], [proc_files2], [proc_files]], index=['toatal amount', 'missed amount', 'handled ratio', 'handled likes', 'handled dislikes'], columns=['data'])}
recap = handle_images()
images = np.array(images)
labels = np.array(labels)
# images -- shows the information about handled photos
# face_convincing -- shows statistics about face retrieving
recap['images']
print(images.shape)
print(labels.shape)<jupyter_output>(8239, 256, 256, 3)
(8239,)<jupyter_text>Сохранение фотографий на жёстком диске<jupyter_code>def save_file(data, file_path_name):
    """Takes all our data here, images and labels. Compresses images in a numpy file. 
    
    :param data: the data we wanna to save
    :param file_path_name: path to file where we wanna to store the data
    """
    print("Saving {}.npy".format(file_path_name))
    np.save(file_path_name, data)

save_file(images, "processed_val_images")
save_file(labels, "processed_val_labels")<jupyter_output>Saving processed_val_images.npy
Saving processed_val_labels.npy