Cree una cámara web falsa para su reunión en línea usando Python

imaginemos Está en una reunión en línea y, por algún motivo, no desea encender la cámara. Pero si ves que todos los demás la encienden, crees que tú también tienes que hacerlo, así que péinate rápidamente, asegúrate de estar bien vestido y enciende la cámara de mala gana. Todos hemos pasado por esto.

Hay buenas noticias. Con la ayuda de Python, la cámara ya no se enciende a la fuerza. Le mostrará cómo crear una cámara web falsa para su reunión en línea como esta:

a2ee4065ea9b705d08e66ec59ccec36e.gif d82f44ce06650e6dbc7ec36e1d324c2e.gif aadc4345edf8cb12b0c7c494aa571a2a.gif

Por supuesto, esta cara no tiene que ser la de Bill Gates, también puede ser la tuya.

Ahora te mostraré cómo escribir código en Python. Al final del artículo, explicaré cómo usar esta cámara falsa para mí.

Crea una cámara web falsa simple

Primero, importaremos algunos módulos, especialmente openCV.

import cv2
import numpy as np
import pickle
import pyaudio
import struct
import math
import argparse
import os

A continuación, crearemos una función para extraer todos los cuadros del video:

def read_frames(file, video_folder):
    frames = []
    cap = cv2.VideoCapture(os.path.join('videos', video_folder, file))
    frame_rate = cap.get(cv2.CAP_PROP_FPS)
    if not cap.isOpened():
        print("Error opening video file")
    while cap.isOpened():
        ret, frame = cap.read()
        if ret:
            frames.append(frame)
        else:
            break
    cap.release()
    return frames, frame_rate

Ahora que tenemos los marcos, podemos crear un bucle para mostrarlos uno tras otro. Cuando se llega al último cuadro, reproducimos el video hacia atrás, luego, cuando llegamos al primer cuadro, lo reproducimos hacia adelante y repetimos el proceso para siempre. De esta manera no hay una transición repentina del último cuadro al primero. También haremos esto para que podamos presionar "q" para detener la cámara web.

frames, frame_rate = read_frames('normal.mov', 'bill_gates')

def next_frame_index(i, reverse):
    if i == len(frames) - 1:
        reverse = True
    if i == 0:
        reverse = False
    if not reverse:
        i += 1
    else:
        i -= 1
    return i, reverse


rev = False
i = 0
while True:
    frame = frames[i]
    cv2.imshow('Webcam', frame)
    pressed_key = cv2.waitKey(int(1000/frame_rate)) & 0xFF
    if pressed_key == ord("q"):
        break
    i, rev = next_frame_index(i, mode, rev)

Con esto, tenemos una cámara web simple que se reproduce sin problemas.

ae7978558760649526c6215382cb0ae3.gif

Pero no nos detenemos ahí.

añadir diferentes modos

Sería más convincente si nuestro avatar de cámara web falsa pudiera hacer algo más que mirar pasivamente. Por ejemplo, a veces en una reunión necesitas asentir con la cabeza, sonreír, hablar o hacer otra cosa.

Así que queremos que nuestra cámara web tenga múltiples "modos" entre los que podamos cambiar en cualquier momento presionando una tecla en el teclado.

Para hacer esto, deberá realizar una grabación breve para cada modo, como una en la que solo está sonriendo. Luego podemos leer fotogramas de cada video y almacenarlos en un diccionario. Cuando detectamos una pulsación de tecla (por ejemplo, "s" para cambiar al "modo de sonrisa"), cambiamos el modo activo al nuevo modo y comenzamos a reproducir el cuadro en el video correspondiente.

video_files = [file for file in os.listdir(os.path.join('videos', folder))
               if file not in ['transitions_dict.p', '.DS_Store']]
frames, frame_rates = {}, {}

for file in video_files:
    mode_name = file.split('.')[0]
    frames[mode_name], frame_rates[mode_name] = read_frames(file, folder)
modes = list(frames.keys())
commands = {mode[0]: mode for mode in modes if mode != 'normal'}

mode = "normal"
frame_rate = frame_rates[mode]
rev = False
i = 0
while True:
    frame = frames[mode][i]
    cv2.imshow('Webcam', frame)
    pressed_key = cv2.waitKey(int(1000/frame_rate)) & 0xFF
    if pressed_key == ord("q"):
        break
    for command, new_mode in commands.items():
        if pressed_key == ord(command):
            i, mode, frame_rate = change_mode(mode, new_mode, i)
    i, rev = next_frame_index(i, mode, rev)

De forma predeterminada, esto se hace para cambiar al modo especificado y el comando de teclado es la primera letra del nombre del modo. En este momento estoy tratando esta función 'cambiar_modo' como una caja negra, y la explicaré más adelante.

3fa04038727ab0d08e2acaca7b718c0a.gif

Optimizar la transición

Entonces queremos cambiar de un video a otro, digamos del modo normal al modo de asentir. ¿Cómo puedo hacer la transición de un modo a otro de la mejor manera posible (es decir, la transición es lo más suave posible)?

Cuando hacemos la transición, queremos movernos al cuadro del nuevo modo que más se parezca al que estamos actualmente.

Para hacer esto, primero podemos definir una métrica de distancia entre imágenes. Aquí se utiliza una distancia euclidiana simple, que analiza la diferencia entre cada píxel de las dos imágenes.

Con esta distancia, ahora podemos encontrar la imagen más cercana a nuestra actual y cambiar a esta. Por ejemplo, si queremos pasar del modo normal al modo de asentimiento y estamos en el cuadro 132 del video normal, sabremos que tenemos que ir al cuadro 86 del video de asentimiento para obtener la transición más fluida.

Podemos precalcular todas estas transiciones óptimas para cada fotograma y de cada modo a todos los demás modos. De esta manera no tenemos que volver a calcular cada vez que queremos cambiar de modo. Las imágenes también se comprimen para que el tiempo de ejecución del cálculo sea más corto. También almacenaremos la mejor distancia entre imágenes.

video_files = [file for file in os.listdir(os.path.join('videos', video_folder))
                       if file not in ['transitions_dict.p', '.DS_Store']]
frames = {}
for file in video_files:
    mode_name = file.split('.')[0]
    frames[mode_name] = read_frames(file, video_folder)
modes = list(frames.keys())

compression_ratio = 10
height, width = frames["normal"][0].shape[:2]
new_height, new_width = height // compression_ratio, width // compression_ratio, 

def compress_img(img):
    return cv2.resize(img.mean(axis=2), (new_width, new_height))

  
frames_compressed = {mode: np.array([compress_img(img) for img in frames[mode]]) for mode in modes}

transitions_dict = {mode:{} for mode in modes}

for i in range(len(modes)):
    for j in tqdm(range(i+1, len(modes))):
        mode_1, mode_2 = modes[i], modes[j]
        diff = np.expand_dims(frames_compressed[mode_1], axis=0) - np.expand_dims(frames_compressed[mode_2], axis=1)
        dists = np.linalg.norm(diff, axis=(2, 3))
        transitions_dict[mode_1][mode_2] = (dists.argmin(axis=0), dists.min(axis=0))
        transitions_dict[mode_2][mode_1] = (dists.argmin(axis=1), dists.min(axis=1))

pickle.dump(transitions_dict, open(os.path.join('videos', video_folder, 'transitions_dict.p'), 'wb'))

Ahora se puede mostrar la función "cambiar_modo", que recupera el mejor marco para convertir desde un diccionario precalculado. Esto se hace de modo que si presiona, por ejemplo, "s" para cambiar al modo de sonrisa, al presionarlo nuevamente se volverá al modo normal.

def change_mode(current_mode, toggled_mode, i):
    if current_mode == toggled_mode:
        toggled_mode = 'normal'

    new_i = transitions_dict[current_mode][toggled_mode][0][i]
    dist = transitions_dict[current_mode][toggled_mode][1][i]
    
    return new_i, toggled_mode, frame_rates[toggled_mode]

Hay otra mejora que podemos agregar para que nuestras transiciones sean más fluidas, en lugar de cambiar siempre de modo de inmediato, esperamos un tiempo para una mejor transición. Por ejemplo, si nuestro avatar está asintiendo, podemos esperar hasta que la cabeza pase la posición media antes de cambiar al modo normal. Para esto, introduciremos una ventana de tiempo (aquí la configuré en 0,5 segundos) para que esperemos el mejor momento para hacer la transición dentro de esta ventana antes de cambiar de modo.

switch_mode_max_delay_in_s = 0.5


def change_mode(current_mode, toggled_mode, i):
    if current_mode == toggled_mode:
        toggled_mode = 'normal'

    # Wait for the optimal frame to transition within acceptable window
    max_frames_delay = int(frame_rate * switch_mode_max_delay_in_s)
    global rev
    if rev:
        frames_to_wait = max_frames_delay-1 - transitions_dict[current_mode][toggled_mode][1][max(0, i+1 - max_frames_delay):i+1].argmin()
    else:
        frames_to_wait = transitions_dict[current_mode][toggled_mode][1][i:i + max_frames_delay].argmin()
    print(f'Wait {frames_to_wait} frames before transitioning')
    for _ in range(frames_to_wait):
        i, rev = next_frame_index(i, current_mode, rev)
        frame = frames[mode][i]
        cv2.imshow('Frame', frame)
        cv2.waitKey(int(1000 / frame_rate))

    new_i = transitions_dict[current_mode][toggled_mode][0][i]
    dist = transitions_dict[current_mode][toggled_mode][1][i]
    
    return new_i, toggled_mode, frame_rates[toggled_mode]

Ahora nuestra transición es más suave. Sin embargo, a veces pueden ser obvios. Entonces, otra idea es agregar deliberadamente bloqueos al video, como los que pueden ocurrir en conexiones inestables (es decir, el video se atasca si la red es inestable), y usarlos para enmascarar las transiciones (haremos que los bloqueos duren es proporcional a la distancia entre las dos imágenes). También agregaremos bloqueos aleatorios para que los patrones no se vuelvan evidentes. Así que agregamos este nuevo código:

# In the change_mode function:

    dist = transitions_dict[current_mode][toggled_mode][1][i]
    if freezes:
        freeze_duration = int(transition_freeze_duration_constant * dist)
        cv2.waitKey(freeze_duration)
    
    
# In the main loop:

    # Random freezes
    if freezes:
        if np.random.randint(frame_rate * 10) == 1:
            nb_frames_freeze = int(np.random.uniform(0.2, 1.5) * frame_rate)
            for _ in range(nb_frames_freeze):
                cv2.waitKey(int(1000 / frame_rate))
                i, rev = next_frame_index(i, mode, rev)

Usar o no usar estos bloqueos se deja como una opción.

Bien, ya hemos cubierto los conceptos básicos de estas transiciones. ¿Qué más podemos añadir a la webcam?

detección de voz

Otra cosa interesante es añadir detección de voz para que cuando hablemos hable el "yo" del vídeo.

Esto se hace usando pyaudio. Gracias a este hilo de stackoverflow (https://stackoverflow.com/questions/4160175/detect-tap-with-pyaudio-from-live-mic).

Básicamente, la idea es observar la amplitud promedio del sonido proveniente del micrófono durante un período de tiempo y si es lo suficientemente alta como para pensar que hemos estado hablando. Originalmente, este código estaba destinado a detectar ruido de golpeteo, pero también funciona bien para detectar el habla.

AMPLITUDE_THRESHOLD = 0.010
FORMAT = pyaudio.paInt16
SHORT_NORMALIZE = (1.0/32768.0)
CHANNELS = 1
RATE = 44100
INPUT_BLOCK_TIME = 0.025
INPUT_FRAMES_PER_BLOCK = int(RATE*INPUT_BLOCK_TIME)


def get_rms(block):
    count = len(block)/2
    format = "%dh" % count
    shorts = struct.unpack(format, block)

    sum_squares = 0.0
    for sample in shorts:
        n = sample * SHORT_NORMALIZE
        sum_squares += n*n
    return math.sqrt( sum_squares / count )


pa = pyaudio.PyAudio()

stream = pa.open(format=FORMAT,
                 channels=CHANNELS,
                 rate=RATE,
                 input=True,
                 frames_per_buffer=INPUT_FRAMES_PER_BLOCK)


def detect_voice():
    error_count = 0
    voice_detected = False
    
    try:
        block = stream.read(INPUT_FRAMES_PER_BLOCK, exception_on_overflow=False)
    except (IOError, e):
        error_count += 1
        print("(%d) Error recording: %s" % (error_count, e))

    amplitude = get_rms(block)
    if amplitude > AMPLITUDE_THRESHOLD:
        voice_detected = True
    return voice_detected

Ahora podemos agregarlo al bucle principal. Esto se hace para que no necesitemos detectar ningún sonido durante una cierta cantidad de fotogramas consecutivos antes de volver al modo normal para que no cambiemos con demasiada frecuencia.

# In the main loop:
  
  if voice_detection:
      if detect_voice():
          quiet_count = 0
          if mode != "talking":
              i, mode, frame_rate = change_mode(mode, "talking", i)
      else:
          if mode == "talking":
              quiet_count += 1
              if quiet_count > stop_talking_threshold:
                  quiet_count = 0
                  i, mode, frame_rate = change_mode(mode, "normal", i)

Ahora, cuando hablamos por el micrófono, podemos hacer que nuestro avatar empiece y deje de hablar. Hice esto para activar o desactivar la detección de voz presionando "v".

c3ed523cb2d1e7b5371f82af0366d501.gif

Estas son todas las características implementadas hasta ahora. Sugerencias para futuras mejoras son bienvenidas.

Cómo usar una cámara web falsa

Primero, descargue todo el código desde aquí: https://github.com/FrancoisLeRoux1/Fake-webcam

Lo que haces es grabar algunos videos propios (en mi Mac, usé la aplicación Photo Booth para esto) y ponerlos en una nueva carpeta dentro de la carpeta Videos. Podrás crear diferentes carpetas para diferentes escenarios, por ejemplo, donde puedes usar diferentes camisas o hacer que tu cabello luzca diferente.

Estos videos pueden y deben ser cortos (alrededor de 10 segundos de video); de lo contrario, si graba videos más largos, puede llevar mucho tiempo calcular la transición óptima. Necesita un video llamado "normal", que será su modo predeterminado.

Luego, si quieres que tu avatar hable, tienes que grabar un video llamado "hablando" donde dices galimatías al azar.

Después de esto, puede grabar cualquier otro patrón que desee (por ejemplo, "sonrisa", "asentir con la cabeza", "adiós"...). Por defecto, el comando para activar/desactivar estos modos será la primera letra de su nombre (por ejemplo, para "sonrisa", presione "s").

Entonces tienes que calcular la conversión óptima. Para hacer esto, simplemente ejecute el script compute-transitions.py

0c33cffa8d57d586448600ef3e48dea.png

Esto debería tomar unos minutos.

Luego, cuando haya terminado, puede encender su cámara web falsa. Para hacer esto, ejecute el script fake-webcam.py. Debe especificar la carpeta dentro de "Videos" donde se encuentra el video. También puede especificar si desea utilizar Congelar.

a66e2e3806b2c488a5927de1d929c29b.png f0e6a773239896a824f4b34beb1145c3.png

Así que ahora deberías poner en marcha tu cámara falsa. A continuación, puede configurarlo como una cámara web para reuniones en línea. Para esto usé OBS: https://obsproject.com/

Seleccione la ventana de Python correcta como fuente y haga clic en Iniciar cámara virtual.

ddb5e53707d388de9eb8726c03b272e9.png

¡Ahora debería poder seleccionar esta cámara virtual como su cámara web en su aplicación de reunión en línea favorita!

☆ FIN ☆

Si ves esto, significa que te gusta este artículo, por favor reenvíalo y dale me gusta. Búsqueda de Wechat "uncle_pn", bienvenido a agregar el "woshicver" de Wechat del editor y actualizar una publicación de blog de alta calidad en el círculo de amigos todos los días.

Escanear código QR para agregar editor↓

0e936fe3f8aac45f1db2aec19cc35a67.jpeg

Supongo que te gusta

Origin blog.csdn.net/woshicver/article/details/126515965
Recomendado
Clasificación