Entrenamiento de reconocimiento facial en tiempo real basado en FaceNet

Extracción de características faciales de FaceNet

FaceNet es una red neuronal profunda para extraer características de imágenes faciales. Fue propuesto por los investigadores de Google Schroff et al.

Dirección en papel: https://arxiv.org/abs/1503.03832

El principio de funcionamiento de FaceNet es ingresar una imagen facial, comprimirla y generarla como un vector que consta de 128 bits, que representa las características básicas de la cara. Este vector se llama incrustación (toda la información relevante de la imagen de la cara está incrustada en el vector).

Entonces, ¿cómo realizar el reconocimiento facial a través de FaceNet?

Un enfoque común es tomar una imagen incrustada y calcular la distancia a una imagen de una cara conocida. Por lo general, se calcula utilizando la ley de los cosenos o la fórmula de la distancia euclidiana. Si la distancia entre rostros calculada es lo suficientemente cercana a la incrustación de un rostro conocido, asumimos que el rostro pertenece a la misma persona.

Entonces, la pregunta es, ¿cómo sabe FaceNet qué extraer de una imagen facial?

Para entrenar un reconocedor de rostros, necesitamos muchas imágenes de rostros. Como todos los problemas de aprendizaje automático, el entrenamiento generalmente requiere miles de imágenes diferentes. Cuando comenzamos el proceso de entrenamiento, el modelo genera vectores aleatorios para cada imagen, lo que significa que las imágenes se distribuyen aleatoriamente.

Pasos de aprendizaje:

  1. Seleccione aleatoriamente una imagen ancla;
  2. Seleccione aleatoriamente imágenes de muestra positivas de la misma persona que la imagen base;
  3. Seleccionar aleatoriamente imágenes negativas de personas diferentes a la imagen principal;
  4. Ajuste los parámetros de la red neuronal FaceNet para que las muestras positivas estén más cerca del ancla que las muestras negativas.

Repetimos estos cuatro pasos hasta que los cambios ya no sean necesarios o sean tan pequeños que no tengan ningún efecto. Después del entrenamiento, todas las caras de la misma persona están cerca unas de otras en la distancia y lejos de diferentes caras.

faceNet.pyCódigo objeto completo:

# faceNet.py
import cv2import cv2
import stow
import typing
import numpy as np
import onnxruntime as ort

class FaceNet:
    """FaceNet class object, which can be used for simplified face recognition
    """
    def __init__(
        self, 
        detector: object,
        onnx_model_path: str = "models/faceNet.onnx", 
        anchors: typing.Union[str, dict] = 'faces',
        force_cpu: bool = False,
        threshold: float = 0.5,
        color: tuple = (255, 255, 255),
        thickness: int = 2,
        ) -> None:
        
        """Object for face recognition
        Params:
            detector: (object) - detector object to detect faces in image
            onnx_model_path: (str) - path to onnx model
            force_cpu: (bool) - if True, onnx model will be run on CPU
            anchors: (str or dict) - path to directory with faces or dictionary with anchor names as keys and anchor encodings as values
            threshold: (float) - threshold for face recognition
            color: (tuple) - color of bounding box and text
            thickness: (int) - thickness of bounding box and text
        """
        
        if not stow.exists(onnx_model_path):
            raise Exception(f"Model doesn't exists in {onnx_model_path}")

        self.detector = detector
        self.threshold = threshold
        self.color = color
        self.thickness = thickness

        providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']

        providers = providers if ort.get_device() == "GPU" and not force_cpu else providers[::-1]

        self.ort_sess = ort.InferenceSession(onnx_model_path, providers=providers)

        self.input_shape = self.ort_sess._inputs_meta[0].shape[1:3]
        
        self.anchors = self.load_anchors(anchors) if isinstance(anchors, str) else anchors

    def normalize(self, img: np.ndarray) -> np.ndarray:
        
        """Normalize image
        Args:
            img: (np.ndarray) - image to be normalized
        Returns:
            img: (np.ndarray) - normalized image
        """
        
        mean, std = img.mean(), img.std()
        return (img - mean) / std

    def l2_normalize(self, x: np.ndarray, axis: int = -1, epsilon: float = 1e-10) -> np.ndarray:
        
        """l2 normalization function
        Args:
            x: (np.ndarray) - input array
            axis: (int) - axis to normalize
            epsilon: (float) - epsilon to avoid division by zero
        Returns:
            x: (np.ndarray) - normalized array
        """
        
        output = x / np.sqrt(np.maximum(np.sum(np.square(x), axis=axis, keepdims=True), epsilon))
        return output

    def detect_save_faces(self, image: np.ndarray, output_dir: str = "faces"):
        
        """Detect faces in given image and save them to output_dir
        Args:
            image: (np.ndarray) - image to be processed
            output_dir: (str) - directory where faces will be saved
        Returns:
            bool: (bool) - True if faces were detected and saved
        """
        
        face_crops = [image[t:b, l:r] for t, l, b, r in self.detector(image, return_tlbr=True)]

        if face_crops == []: 
            return False

        stow.mkdir(output_dir)

        for index, crop in enumerate(face_crops):
            output_path = stow.join(output_dir, f"face_{str(index)}.png")
            cv2.imwrite(output_path, crop)
            print("Crop saved to:", output_path)

        self.anchors = self.load_anchors(output_dir)
        
        return True

    
    def load_anchors(self, faces_path: str):
        
        """Generate anchors for given faces path
        Args:
            faces_path: (str) - path to directory with faces
        Returns:
            anchors: (dict) - dictionary with anchor names as keys and anchor encodings as values
        """
        
        anchors = {}
        if not stow.exists(faces_path):
            return {}

        for face_path in stow.ls(faces_path):
            anchors[stow.basename(face_path)] = self.encode(cv2.imread(face_path.path))

        return anchors

    def encode(self, face_image: np.ndarray) -> np.ndarray:
        """Encode face image with FaceNet model
        Args 
            face_image: (np.ndarray) - face image to be encoded
            
        Returns:
            face_encoding: (np.ndarray) - face encoding
        """
        face = self.normalize(face_image)
        face = cv2.resize(face, self.input_shape).astype(np.float32)

        encode = self.ort_sess.run(None, {self.ort_sess._inputs_meta[0].name: np.expand_dims(face, axis=0)})[0][0]
        normalized_encode = self.l2_normalize(encode)

        return normalized_encode

    def cosine_distance(self, a: np.ndarray, b: typing.Union[np.ndarray, list]) -> np.ndarray:
        
        """Cosine distance between wectors a and b
        Args:
            a: (np.ndarray) - first vector
            b: (np.ndarray) - second list of vectors
        Returns:
            distance: (float) - cosine distance
        """
        
        if isinstance(a, list):
            a = np.array(a)

        if isinstance(b, list):
            b = np.array(b)

        return np.dot(a, b.T) / (np.linalg.norm(a) * np.linalg.norm(b))

    def draw(self, image: np.ndarray, face_crops: dict):
        
        """Draw face crops on image
        Args:
            image: (np.ndarray) - image to be drawn on
            face_crops: (dict) - dictionary with face crops as values and face names as keys
        Returns:
            image: (np.ndarray) - image with drawn face crops
        """
        
        for value in face_crops.values():
            t, l, b, r = value["tlbr"]
            cv2.rectangle(image, (l, t), (r, b), self.color, self.thickness)
            cv2.putText(image, stow.name(value['name']), (l, t - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, self.color, self.thickness)

        return image

    def __call__(self, frame: np.ndarray) -> np.ndarray:
        
        """Face recognition pipeline
        Args:
            frame: (np.ndarray) - image to be processed
        Returns:
            frame: (np.ndarray) - image with drawn face recognition results
        """
        
        face_crops = {index: {"name": "Unknown", "tlbr": tlbr} for index, tlbr in enumerate(self.detector(frame, return_tlbr=True))}
        for key, value in face_crops.items():
            t, l, b, r = value["tlbr"]
            face_encoding = self.encode(frame[t:b, l:r])
            distances = self.cosine_distance(face_encoding, list(self.anchors.values()))
            if np.max(distances) > self.threshold:
                face_crops[key]["name"] = list(self.anchors.keys())[np.argmax(distances)]

        frame = self.draw(frame, face_crops)

        return frame

Podemos usar faceNet/convert_to_onnx.pyun script para hacer la conversión:

# faceNet/convert_to_onnx.py
import os
import tensorflow as tf
import tf2onnx
from architecture import InceptionResNetV2

if __name__ == '__main__':
    """ weights can be downloaded from https://drive.google.com/drive/folders/1scGoVCQp-cNwKTKOUqevCP1N2LlyXU3l?usp=sharing
    Put facenet_keras_weights.h5 file in model folder
    """
    facenet_weights_path = "models/facenet_keras_weights.h5"
    onnx_model_output_path = "models/faceNet.onnx"

    if not os.path.exists(facenet_weights_path):
        raise Exception(f"Model doesn't exists in {facenet_weights_path}, download weights from \
            https://drive.google.com/drive/folders/1scGoVCQp-cNwKTKOUqevCP1N2LlyXU3l?usp=sharing")

    faceNet = InceptionResNetV2()
    faceNet.load_weights(facenet_weights_path) 

    spec = (tf.TensorSpec(faceNet.inputs[0].shape, tf.float32, name="image_input"),)
    tf2onnx.convert.from_keras(faceNet, output_path=onnx_model_output_path, input_signature=spec)
view raw

Primero, descargue los pesos del enlace dado en el código y colóquelos en la carpeta del modelo. Luego ejecute  faceNet/convert_to_onnx.pyel código con python, que puede convertir el modelo a .onnxformato.

Una vez que tenemos el modelo, podemos abrir main.pyel script y ejecutar el reconocimiento facial en tiempo real de la cámara web con el siguiente código:

# main.py
from utils import FPSmetric
from engine import Engine
from faceDetection import MPFaceDetection
from faceNet.faceNet import FaceNet

if __name__ == '__main__':
    facenet = FaceNet(
        detector = MPFaceDetection(),
        onnx_model_path = "models/faceNet.onnx", 
        anchors = "faces",
        force_cpu = True,
    )
    engine = Engine(webcam_id=0, show=True, custom_objects=[facenet, FPSmetric()])

    # save first face crop as anchor, otherwise don't use
    while not facenet.detect_save_faces(engine.process_webcam(return_frame=True), output_dir="faces"):
        continue

    engine.run()

Cuando se proporciona la ruta de almacenamiento del modelo. Le damos la ruta para guardar las anclas, debe ser una imagen con face crop. Garantiza que el modelo cargará este ancla y mostrará el nombre correspondiente cuando se encuentre una coincidencia.

A continuación, necesitamos crear un objeto de motor que sea responsable de procesar transmisiones de imágenes, videos o cámaras web; las cámaras web se pueden procesar opcionalmente. Utilice el parámetro "mostrar".

Además, podemos agregar un FPSmetric para saber qué tan rápido funciona el reconocimiento facial.

Finalmente, necesitamos pasar el objeto "facenet" al parámetro "custom_objects". Aquí podemos agregar más, "boceto a lápiz", "eliminación de fondo" u otras entidades que queramos.

También podemos crear una función que tome el primer marco de la cámara web y, si encuentra una cara en él, lo recorte y lo guarde:

while not facenet.detect_save_faces(engine.process_webcam(return_frame=True), output_dir="faces"):
    continue

De esta manera, creamos un sistema que puede hacer reconocimiento facial en tiempo real en nuestra CPU, y funciona a alrededor de 30 fps, ¡lo cual es más que suficiente para nosotros!

fuente:

Dirección de GitHub: https://github.com/pythonlessons/background_removal

Supongo que te gusta

Origin blog.csdn.net/m0_73122726/article/details/128507703
Recomendado
Clasificación