Crea tu Cámara Virtual con Desenfoque de Caras en Linux Mint (Python + OpenCV)

¿Necesitas anonimizar caras en tus videollamadas o grabaciones en tiempo real? ¿Estás haciendo una demostración y quieres proteger la privacidad de las personas que puedan aparecer en pantalla? ¿O simplemente buscas un efecto visual diferente para tu cámara? ¡Tenemos la solución! Usando el poder de Python, las bibliotecas OpenCV y MediaPipe para visión por computadora, y el módulo del kernel v4l2loopback en Linux, podemos crear una cámara virtual personalizada.

Lo mejor de todo es que esta versión te permite ajustar la intensidad del desenfoque y cuánto se expande el área desenfocada alrededor de la cara, ¡directamente desde la línea de comandos!

Este tutorial te guiará paso a paso para configurar una cámara virtual en Linux Mint (y sistemas similares como Ubuntu/Debian) que detecta automáticamente las caras en el video de tu webcam, les aplica un efecto de desenfoque configurable, y envía el resultado a una cámara virtual que puedes usar en cualquier aplicación.

Tecnologías que usaremos:

  • Python 3: Nuestro lenguaje de programación.
  • OpenCV (opencv-python): Para capturar y manipular el video de la webcam.
  • MediaPipe: Una fantástica biblioteca de Google para detección de caras rápida y eficiente.
  • PyVirtualCam: Una biblioteca Python para enviar video a una cámara virtual.
  • v4l2loopback: Un módulo del kernel de Linux que nos permite crear dispositivos de cámara virtual (/dev/videoX).
  • argparse: Módulo de Python para crear interfaces de línea de comandos fáciles de usar.

Prerrequisitos:

  • Un sistema Linux Mint instalado y actualizado.
  • Una webcam conectada y funcionando.
  • Acceso a la terminal.
  • Conocimientos básicos de cómo usar la terminal.
  • Python 3 y pip instalados (generalmente vienen por defecto en Mint). Puedes verificar con python3 --version y pip3 --version.

He creado un repositorio en github con una versión más completa: https://github.com/soyunomas/face-python-fx-virtual-cam

¡Manos a la obra para configurar tu cámara con desenfoque de caras ajustable!

Paso 1: Actualizar tu Sistema

Siempre es una buena práctica empezar asegurándote de que tu sistema está actualizado. Abre una terminal (Ctrl+Alt+T) y ejecuta:

sudo apt update && sudo apt upgrade -y

Paso 2: Instalar el Módulo v4l2loopback

Este módulo es la clave para crear el dispositivo de cámara virtual en /dev/. Lo instalaremos usando DKMS para que se reconstruya automáticamente si actualizas el kernel.

sudo apt install v4l2loopback-dkms linux-headers-$(uname -r) -y

Paso 3: Cargar el Módulo v4l2loopback (¡Importante!)

Ahora que está instalado, necesitamos cargarlo en el kernel para crear el dispositivo virtual. Este paso es crucial y requiere una opción específica.

  1. Revisa tus cámaras actuales: 
    Antes de crear una nueva, mira qué dispositivos /dev/video* tienes:
    ls /dev/video*
    Anota los números existentes (ej. /dev/video0/dev/video1).
    Necesitamos elegir un número que no esté en uso para nuestra cámara virtual. Usaremos 10 como ejemplo predeterminado en el script, pero puedes elegir otro si 10 está ocupado.
  2. (Opcional) Descarga el módulo si ya estaba cargado:
    Si tienes dudas o quieres asegurarte de empezar limpio, descarga el módulo:sudo modprobe -r v4l2loopback
  3. Carga el módulo con exclusive_caps=1:
    Esta opción es fundamental para que pyvirtualcam y muchas aplicaciones de videollamada (especialmente las basadas en navegador/WebRTC) reconozcan el dispositivo como una salida válida a la que se puede escribir video.
    Si omites esto, probablemente obtendrás el error not a video output device.

    # --- ¡ASEGÚRATE DE USAR exclusive_caps=1! ---
    # Cambia video_nr=10 si ese número ya está en uso o si quieres usar otro
    sudo modprobe v4l2loopback devices=1 video_nr=10 card_label="My FaceBlur Cam" exclusive_caps=1


  4. Verifica la creación: 
    Vuelve a listar los dispositivos:

    ls /dev/video*

    Ahora deberías ver tu nuevo dispositivo (ej. /dev/video10).

Paso 4: Configurar Permisos

Tu usuario necesita permiso para escribir datos en el nuevo dispositivo de cámara virtual.

sudo usermod -a -G video $USER

¡MUY IMPORTANTE! Para que este cambio de grupo tenga efecto, necesitas cerrar sesión por completo y volver a iniciarla, o simplemente reiniciar tu computadora. De lo contrario, el script Python probablemente fallará con un error de permisos.

Paso 5: Crear un Entorno Virtual de Python

Aísla las dependencias de tu proyecto.

  1. Asegúrate de tener python3-venv:
    sudo apt install python3-venv -y
  2. Crea y navega a la carpeta del proyecto:
    mkdir ~/faceblur_cam_project cd ~/faceblur_cam_project
  3. Crea el entorno virtual:
    python3 -m venv vcam_env
  4. Activa el entorno:
    source vcam_env/bin/activate

    Tu prompt de la terminal debería cambiar, mostrando (vcam_env) al principio.

Paso 6: Instalar las Dependencias de Python

Con el entorno virtual activado, instala las bibliotecas necesarias:

pip install opencv-python mediapipe pyvirtualcam numpy

Paso 7: Obtener el Script de Python Ajustable

Crea un archivo llamado face_blur_webcam.py dentro de tu carpeta ~/faceblur_cam_project y pega el siguiente código completo:

import cv2
import mediapipe as mp
import numpy as np
import pyvirtualcam
import time
import sys
import signal
import argparse # <--- Incluido para argumentos CLI

# --- Variables globales para la limpieza ---
cap = None
keep_running = True

# --- Manejador para salida limpia (Ctrl+C) ---
def signal_handler(sig, frame):
    global keep_running
    print("\nINFO: Señal de interrupción recibida. Deteniendo...")
    keep_running = False

signal.signal(signal.SIGINT, signal_handler) # Captura Ctrl+C

# --- Inicialización de Componentes de MediaPipe ---
mp_face_detection = mp.solutions.face_detection
mp_drawing = mp.solutions.drawing_utils

def initialize_webcam(device_index):
    """Inicializa la cámara web y obtiene sus propiedades."""
    global cap
    print(f"INFO: Intentando inicializar cámara web en el índice {device_index}...")
    cap = cv2.VideoCapture(device_index)
    if not cap.isOpened():
        print(f"ERROR: No se pudo abrir la cámara web en el índice {device_index}.")
        print("       Asegúrate de que esté conectada y no esté en uso.")
        return None, None, None, None

    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fps_in = cap.get(cv2.CAP_PROP_FPS)

    if width <= 0 or height <= 0:
        print("WARN: No se pudieron obtener dimensiones válidas directamente. Intentando leer un fotograma...")
        ret, frame = cap.read()
        if not ret or frame is None:
            print("ERROR: No se pudo leer un fotograma inicial para obtener dimensiones.")
            if cap.isOpened(): cap.release()
            cap = None
            return None, None, None, None
        height, width = frame.shape[:2]
        print(f"INFO: Dimensiones obtenidas del primer fotograma: {width}x{height}")
        if fps_in <= 0:
            fps_in = 30
            print(f"WARN: No se pudo obtener FPS válido. Usando {fps_in:.2f} FPS por defecto.")

    fps_out = min(fps_in if fps_in > 0 else 30.0, 30.0)
    print(f"INFO: Cámara web inicializada: {width}x{height} @ {fps_in:.2f} FPS (Virtual Cam target @ {fps_out:.2f} FPS)")
    return cap, width, height, fps_out

def main(args):
    """Función principal de la aplicación."""
    global keep_running, cap

    blur_kernel_size = args.blur_kernel
    if blur_kernel_size <= 0:
        print("ERROR: El tamaño del kernel de blur debe ser positivo.")
        sys.exit(1)
    if blur_kernel_size % 2 == 0:
        blur_kernel_size += 1
        print(f"WARN: El tamaño del kernel de blur debe ser impar. Ajustado a {blur_kernel_size}.")

    if args.padding_x < 0 or args.padding_y < 0:
        print("ERROR: Los factores de padding no pueden ser negativos.")
        sys.exit(1)

    cap_local, width, height, fps_out = initialize_webcam(args.webcam_id)
    if cap_local is None:
        sys.exit(1)

    vcam_device = f'/dev/video{args.vcam_id}'
    print(f"INFO: Intentando usar cámara virtual en {vcam_device}")
    print(f"INFO: Padding X: {args.padding_x*100:.1f}%, Padding Y: {args.padding_y*100:.1f}%, Blur Kernel: {blur_kernel_size}")

    print("INFO: Inicializando MediaPipe Face Detection...")
    try:
        with mp_face_detection.FaceDetection(
            model_selection=0, min_detection_confidence=0.5
        ) as face_detection, \
             pyvirtualcam.Camera(width=width, height=height, fps=fps_out,
                                device=vcam_device,
                                print_fps=True) as cam:

            print(f"INFO: Cámara virtual '{cam.device}' lista. Puedes seleccionarla en tu app.")
            print("INFO: Ejecutando... Presiona 'q' en la ventana de OpenCV o Ctrl+C en la terminal para salir.")

            while keep_running:
                ret, frame_bgr = cap_local.read()
                if not ret or frame_bgr is None:
                    if cap_local.get(cv2.CAP_PROP_POS_FRAMES) == cap_local.get(cv2.CAP_PROP_FRAME_COUNT):
                        print("INFO: Fin del stream de video alcanzado.")
                    else:
                        print("WARN: No se pudo leer el fotograma de la cámara. Deteniendo...")
                    keep_running = False
                    break

                frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
                frame_rgb.flags.writeable = False
                results = face_detection.process(frame_rgb)
                frame_rgb.flags.writeable = True

                output_frame_bgr = frame_bgr.copy()

                if results.detections:
                    img_h, img_w, _ = output_frame_bgr.shape

                    for detection in results.detections:
                        bbox_relative = detection.location_data.relative_bounding_box

                        try:
                            x_min_orig = int(bbox_relative.xmin * img_w)
                            y_min_orig = int(bbox_relative.ymin * img_h)
                            face_w_orig = int(bbox_relative.width * img_w)
                            face_h_orig = int(bbox_relative.height * img_h)
                            x_max_orig = x_min_orig + face_w_orig
                            y_max_orig = y_min_orig + face_h_orig

                            pad_x = int(face_w_orig * args.padding_x)
                            pad_y = int(face_h_orig * args.padding_y)

                            x_min_final = max(0, x_min_orig - pad_x)
                            y_min_final = max(0, y_min_orig - pad_y)
                            x_max_final = min(img_w, x_max_orig + pad_x)
                            y_max_final = min(img_h, y_max_orig + pad_y)

                            if x_min_final < x_max_final and y_min_final < y_max_final:
                                face_roi = output_frame_bgr[y_min_final:y_max_final, x_min_final:x_max_final]
                                if face_roi.size > 0:
                                    blurred_face = cv2.GaussianBlur(face_roi, (blur_kernel_size, blur_kernel_size), 0)
                                    output_frame_bgr[y_min_final:y_max_final, x_min_final:x_max_final] = blurred_face

                        except Exception as e:
                            print(f"ERROR: Excepción al procesar/blurear cara: {e}")

                output_frame_rgb = cv2.cvtColor(output_frame_bgr, cv2.COLOR_BGR2RGB)
                cam.send(output_frame_rgb)
                cv2.imshow('Face Blur VCam - Output (Press q to quit)', output_frame_bgr)

                if cv2.waitKey(1) & 0xFF == ord('q'):
                    print("INFO: Tecla 'q' presionada. Saliendo...")
                    keep_running = False
                    break
                cam.sleep_until_next_frame()

    except ImportError as e:
        print(f"ERROR: Faltan dependencias: {e}. ¿Activaste el entorno virtual?")
        print(f"       Asegúrate de haber ejecutado: pip install opencv-python mediapipe pyvirtualcam numpy")
        sys.exit(1)
    except FileNotFoundError:
        print(f"ERROR: No se encontró el dispositivo de cámara virtual '{vcam_device}'.")
        print(f"       ¿Está cargado el módulo v4l2loopback con video_nr={args.vcam_id} y exclusive_caps=1?")
        print(f"       Prueba: sudo modprobe -r v4l2loopback && sudo modprobe v4l2loopback devices=1 video_nr={args.vcam_id} card_label='My FaceBlur Cam' exclusive_caps=1")
        sys.exit(1)
    except RuntimeError as e:
         # Captura el error específico que tuviste
        if "not a video output device" in str(e):
             print(f"ERROR: {e}")
             print("       Esto usualmente significa que 'v4l2loopback' no se cargó con 'exclusive_caps=1'.")
             print(f"       Prueba: sudo modprobe -r v4l2loopback && sudo modprobe v4l2loopback devices=1 video_nr={args.vcam_id} card_label='My FaceBlur Cam' exclusive_caps=1")
        else:
             print(f"ERROR: Ocurrió una excepción inesperada de Runtime: {e}")
             import traceback
             traceback.print_exc()
        sys.exit(1)
    except Exception as e:
        print(f"ERROR: Ocurrió una excepción inesperada: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)
    finally:
        print("INFO: Limpiando recursos...")
        if cap is not None and cap.isOpened():
            cap.release()
            print("INFO: Cámara web liberada.")
        cv2.destroyAllWindows()
        print("INFO: Ventanas de OpenCV cerradas.")
        print("INFO: Aplicación terminada.")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Crea una cámara virtual con desenfoque de caras ajustable.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    parser.add_argument(
        "--webcam-id", type=int, default=0,
        help="Índice del dispositivo de la cámara web a usar (ej. 0, 1, ...)"
    )
    parser.add_argument(
        "--vcam-id", type=int, default=10,
        help="Número del dispositivo de la cámara virtual a usar (ej. 10 para /dev/video10)"
    )
    parser.add_argument(
        "--blur-kernel", type=int, default=99,
        help="Tamaño del kernel para el desenfoque Gaussiano (debe ser impar y positivo)"
    )
    parser.add_argument(
        "--padding-x", type=float, default=0.15,
        help="Factor de padding horizontal. 0 = sin padding, 0.2 = añade 20%% del ancho de la cara a cada lado."
    )
    parser.add_argument(
        "--padding-y", type=float, default=0.20,
        help="Factor de padding vertical. 0 = sin padding, 0.3 = añade 30%% del alto de la cara arriba y abajo."
    )
    parsed_args = parser.parse_args()
    main(parsed_args)

Paso 8: Ejecutar el Script y Usar las Opciones

Ahora viene la parte flexible. Asegúrate de que tu entorno virtual esté activado ((vcam_env) en el prompt).

  1. Ver la Ayuda: Para entender las opciones disponibles y sus valores por defecto, ejecuta:

    python face_blur_webcam.py --help
  2. Ejecutar con Valores Predeterminados: 
    Si cargaste v4l2loopback con video_nr=10 y quieres usar tu webcam por defecto (índice 0) con el blur y padding predeterminados:

    python face_blur_webcam.py
  3. Ejecutar con Opciones Personalizadas:
    • Menos blur y más padding, usando la cámara virtual /dev/video11:

      # Asegúrate de haber cargado v4l2loopback con video_nr=11

      python face_blur_webcam.py --vcam-id 11 --blur-kernel 45 --padding-x 0.3 --padding-y 0.4
    • Sin padding (solo la cara detectada) y usando la webcam con índice 1:

      python face_blur_webcam.py --webcam-id 1 --padding-x 0 --padding-y 0

Al ejecutar el script:

  • Verás mensajes en la terminal sobre la inicialización.
  • Aparecerá una ventana de OpenCV mostrando la vista previa con el desenfoque aplicado.
  • Verás mensajes de FPS indicando que se está enviando video a la cámara virtual.

Para detener el script: Presiona q con la ventana de OpenCV seleccionada, o Ctrl+C en la terminal.

Paso 9: Usar tu Nueva Cámara Virtual Ajustable

  1. Abre tu aplicación de videollamadas (Zoom, Meet, Discord, etc.). En firefox detecta mejor la cámara.
  2. Ve a la configuración de Audio/Video.
  3. En la selección de cámara, busca y elige “My FaceBlur Cam” (o el card_label que configuraste).
  4. ¡Listo! Tu video mostrará el efecto de desenfoque de caras con los ajustes que especificaste al ejecutar el script. Si quieres cambiar los ajustes, detén el script (Ctrl+C o ‘q’) y vuelve a lanzarlo con diferentes parámetros.

Recuerda: El script Python debe estar ejecutándose en segundo plano mientras usas la cámara virtual.

Paso 10: Desactivar el Entorno Virtual (Opcional)

Cuando termines, sal del entorno virtual:

deactivate

Solución de Problemas Comunes

  • Error not a video output device: ¡El más común! Casi siempre significa que olvidaste exclusive_caps=1 al cargar v4l2loopback (Paso 3). Descarga (sudo modprobe -r v4l2loopback) y recarga el módulo correctamente.
  • Error Permission denied: Olvidaste cerrar/iniciar sesión o reiniciar después de añadir tu usuario al grupo video (Paso 4).
  • Error FileNotFoundError: [Errno 2] No such file or directory: '/dev/videoX': El vcam-id que pasaste al script no coincide con el video_nr usado al cargar v4l2loopback, o el módulo no está cargado. Verifica ls /dev/video* y recarga el módulo si es necesario.
  • Error ImportError: Olvidaste activar el entorno virtual (source vcam_env/bin/activate) antes de ejecutar pip install o python ....
  • Video lento/lag: Reduce --blur-kernel o la resolución de tu webcam (si es posible). Cierra otras apps pesadas.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

− 1 = 2