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:
- Seleccione aleatoriamente una imagen ancla;
- Seleccione aleatoriamente imágenes de muestra positivas de la misma persona que la imagen base;
- Seleccionar aleatoriamente imágenes negativas de personas diferentes a la imagen principal;
- 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.py
Có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.py
un 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.py
el código con python, que puede convertir el modelo a .onnx
formato.
Una vez que tenemos el modelo, podemos abrir main.py
el 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