PhotoBooth en 50 líneas de Python!
MÚ RAPIDO
PhotoBooth en 50 líneas de Python!
2019-11-03
Por
Er ESpilver

El tiempo se me echaba encima y casi no me quedaban días para preparar la fiesta. Esta vez quería que todos los colegas quedaran inmortalizados y se me ocurrió montar uno de esos PhotoBooth o, como nos gusta decir a nosotros... FotoMatón. Pero casi no tenía tiempo.... La desesperación me invadió y entre modo pánico.

Y que hacemos todos cuando no tenemos tiempo para un proyecto?... Claro. Montamos algo rapidito en Python o una aplicación web. Yo me decanté por Python... los entresijos de WebRTC los dejaremos para otra ocasión. Y bueno, esto es lo que me salio.

Módulos

Una de las grandes ventajas de Python es que hay módulos para todo. En su día este era el caso de Perl y en ciertos ámbitos Java.... pero ahora es Python así que vamos a ello.

Para este proyecto decidí usar OpenCV para la captura de las imágenes, y PyGame, para el interfaz. La verdad que solo vamos a utiliza una ínfima parte de ambas librerías... pero es lo que hay si queremos que nuestro FotoMatón ocupe solo 50 líneas.

Como este artículo va de cosas mú rápido... Pues os miráis como instalar estos dos módulos en vuestros sistemas. En Ubuntu sería algo tal que así:

$ sudo apt-get install python-opencv
$ pip install pygame

Declaraciones e Inicializaciones

Lo primero que nos encontramos es el clásico bloque de imports y la declaración de algunas variables... Nada especial

import pygame
from pygame.locals import *
import cv2
import numpy as np
import time

display_width = 1920    # Resolución horizontal de la pantalla
display_height = 1080   # Resolución vertical de la pantalla
cnt = 1                 # Contador de captura 
show_capture = 0        # Flag que indica se mostramos el video o la captura

Los nombres son auto-explicativos... creo... si sabes algo de inglés.... Lo siento... en inglés todo ocupa menos.

A continuación inicializamos la captura de video utilizando el módulo cv2

# Inicializamos Captura de video y resolución de cámara
camera = cv2.VideoCapture(0)
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT,720)

También auto-explicativo no?. Como podéis imaginar, si disponéis de más de una cámara, podréis acceder a cada una de ellas cambiando el índice en la llamada a VideoCapture.

# Inicializamos pygame y pantalla
pygame.init()
pygame.mixer.quit() # Gracias Stackoverflow. Reduce use de CPU
screen = pygame.display.set_mode([display_width,display_height])

Finalmente inicializamos PyGame y creamos la ventana de la aplicación con la resolución que configuramos en las variables al principio del script. La llamada a pygame.mixer.quit() reduce el uso de CPU drásticamente. Parece ser algún tipo de bug, pero como siempre, stackoverflow nos ha salvado la vida.

El Bucle Principal

El bucle principal del programa es igualmente sencillo. Mientras no hayamos sacado una foto, captura imágenes de la cámara. Al pulsar la barra espaciadora, grabamos la imagen actual y dejamos de capturar por dos segundo de forma que podemos ver la foto que acabamos de sacar.

Todo muy sencillo. Aquí tenéis el código de la parte que captura las imágenes y las visualiza:

try:
    while True:
        if show_capture == 1:
            t1 = time.time()
            if t1 - t0 > 2:     # Mostramos la captura por 2 segundos
                show_capture = 0
        else:
            ret, frame_orig = camera.read()
            frame = cv2.cvtColor(frame_orig, cv2.COLOR_BGR2RGB)
            frame = np.rot90(frame)
            frame = cv2.resize (frame, (display_height, display_width))
            frame = pygame.surfarray.make_surface(frame)
            
        screen.blit(frame, (0,0)) 
        pygame.display.update()

Como podemos ver, para poder visualizar correctamente la imagen capturada, debemos hacerle un par de cosillas. Lo primero es re-ordenar las componentes de color de los pixels. Probad a eliminar esa línea. Bueno, OpenCV almacena las imágenes capturadas como BGR, pero pygame necesita RGB para generar las surfaces que luego mostraremos en la pantalla.

Igualmente, tenemos que rotar la imagen 90 grados para que la imagen se muestre correctamente.

Finalmente, la llamada a resize escala nuestra imagen al tamaño de la ventana, de forma que el video se muestra a pantalla completa.

Interactuando con el Usuario

La última parte del programa se encarga de procesar los eventos. Específicamente la tecla ESC para salir de la aplicación, y la tecla SPACE para capturar una imagen.

        # Procesa eventos
        for event in pygame.event.get():
        if event.type == KEYDOWN:
                if event.key == K_SPACE: # SPACE graba la foto e inicializa timer
                    show_capture = 1
                    t0 = time.time()
                    cv2.imwrite ("captura{:06}.jpg".format(cnt), frame_orig)
                    cnt +=1
                if event.key == K_ESCAPE: # ESC termina la aplicación
            raise (SystemExit)
except KeyboardInterrupt,SystemExit:
    pygame.quit()
    cv2.destroyAllWindows()

De nuevo, sin sorpresas aquí. Si pulsamos la barra, inicializamos el temporizador (tomamos la hora actual), indicamos que hemos tomado una foto, de forma que el otro bloque de código en el bucle principa pare de capturar imágenes por un rato y guardamos la imagen actual como un JPG en el disco. La variable cnt la utilizamos para generar nombres de fichero distintos. Probablemente fuera mejor idea utilizar la hora actual que, después de todo, estamos obteniendo justo antes de grabar la imagen. Pista: time.strftime.... ahí lo dejo.

Si pulsamos ESC lanzamos la excepción SystemExit, para salir del bloque try. Lo normal es utilizar sys.exit(0), pero para evitarnos el import sys al principio y ahorra una línea .... bueno, pues lo hemos hecho así.

Aires de Fiesta

Como diría Karina... Los chicos y chicas cargados de felicidad. Ya se me va la pinza otra vez. Al lío. Pulsar la barra está muy chulo, pero, asumámoslo, tener un montón de gente borracha tocando tu portátil y bebidas a punto de derramarse continuamente no es el entorno más seguro para nuestro preciado ordenador.

Así que vamos a añadir un botón a nuestro FotoMatón para que nos quede perfecto para nuestra fiesta. Y luego que ya cada uno ponga el botón físico que más guste.

Tenemos algunas opciones para hacer esto. Podríamos usar algún SBC y conectar nuestro botón a un GPIO y escribir un pequeño programa que haga uso de uinput. O podemos utilizar un microcontrolador que simule un teclado y que al pulsar el botón genere el código de la barra espaciadora.

La solución del microcontrolador ya la vimos en el número 6 de la primera época, así que vamos a explorar la opción de uinput, la cual, por otra parte nos abre un infinito mundo de posibilidades: desde botones remotos a disparo por voz!

Pulsando la Barra sin Pulsarla

Vamos a seguir con Python, porque ya sabéis que es mejor no mezclar. Para ello debemos instalar el módulo python-uinput

pip2 install python-uinput

Y ahora ya podemos crear un script que pulse la barra

#!/usr/bin/env python2

import uinput
import time

with uinput.Device([uinput.KEY_SPACE, uinput.KEY_ESC]) as device:
    time.sleep(1)
    
    device.emit_click(uinput.KEY_SPACE)

Si estáis usando algún SBC podéis combinar este código con la lectura de una GPIO para lanzar la captura de una imagen. Observad que hemos tenido que añadir una pequeña espera antes de enviar el código de la barra espaciadora. Parece ser que es necesario esperar un poco desde que el dispositivo se crea hasta que se puede utilizar.... Si no sabéis de que estamos hablando quedaros con que hay que esperar un rato... al menos en nuestro sistema.

Para probar este script la forma más sencilla es con una línea como esta:

$ ./fb_simple.py & (sleep 4; sudo ./barra.py)

Observad que el uso de uinput requiere permisos especiales, así que lo ejecutamos con sudo... debemos ejecutarlo al menos una vez para introducir el password que nos pide sudo. También podéis lanzar el sleep y el script desde otro terminal y luego lanzar el FotoMatón. Quizás necesitéis un retardo más largo.

Si queréis evitar el problema con sudo, deberéis hacer lo siguiente:

  • Crear un grupo de usuarios por ejemplo uinput
  • Añadir el usuario con el que queráis acceder al dispositivo a este grupo
  • Cambiar los permisos de dispositivo (/dev/uinput) para dar acceso al grupo
  • Normalmente tendréis que reiniciar la sesión para que los cambios en los grupos tengan efecto

Rizando el Rizo

Con estos dos scripts podemos hacer algunas virguerías interesantes como disparar nuestro FotoMatón usando voz o a través de la red... con nuestro teléfono por ejemplo.

Una forma sencilla de hacerlo sería la siguiente. En primer lugar, lanzamos nuestro FotoMaton. En otra consola ejecutamos el siguiente comando (suponemos que hemos arreglado lo de los permisos y no es necesario usar root... de lo contrario ejecuta todo el comando como root para probar):

$ nc -w 2 -v -l 5000 ; ./barra.py

Esto iniciará netcat en modo servidor con un timeout de 2 segundos. El timeout lo necesito yo para dar el focus a la ventana del FotoMaton (mover el ratón a la otra ventana :) una vez que haya intentado acceder a http://127.0.0.1:5000 desde mi explorador.... De lo contrario el espacio se enviaría al browser, en lugar de al Fotomatón. Si siempre me ha gustado el Sloopy Focus. Si Estáis utilizando otro dispositivo, como un teléfono móvil, el timeout no es necesario.

Mejorando el interfaz

Esta solución funciona perfectamente, pero es un poco fea ya que en nuestro explorador vamos a obtener un error de conexión y vamos a dar una imagen un poco cutre en nuestra fiesta. Así que vamos a hacer que netcat sirva una página por defecto para que la cosa tenga mejor pinta.

Para ello creamos un fichero con el siguiente contenido:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Server: FotoMatón Occam's

<!doctype html>
<html><body>
<center>
<h1>CHEESE!</h1>
<a href="index.html#">Foto</a>
</center>
</body></html>

Como podéis ver, el fichero contiene todo lo que queremos enviar al browser. Primero las cabeceras HTTP y luego la página HTML. Ahora podemos lanzar nuestro teclado virtual con una línea como esta:

$ cat respuesta.http | nc -w 2 -v -l 5000 ; ./barra.py

Lo cual funciona perfectamente.

Nuestro flamante interfaz web

Así que ahora solo tenemos que poner esta línea en un bucle y tira millas.

$  while (true); do cat respuesta.http | nc -w 2 -v -l 5000 ; sudo ./barra.py; done

Durante las pruebas utilicé firefox en mi ordenador... para no tener que andar con otro dispositivo a vueltas y observe una cosa muy rara. Cada vez pasaba el ratón sobre el enlace para sacar fotos, se producía una nueva conexión a netcat claro, se sacaba una foto. Este problema no lo tenemos con el teléfono ya que no existe el concepto de hover y chromium no lo hace, así que pude terminar mis pruebas sin mayores inconvenientes.

ESTO ES TODO.... O NO?

Aquí lo vamos a dejar, pero sin antes daros algunas ideas para extender este pequeño proyecto. Ahí van:

  • Añadir una galería
  • Control por voz
  • Botón remoto con ESP-32
  • Multi-Foto
  • Compartir directamente en redes sociales

Esperamos vuestras experiencias. No seáis tímidos. Hasta el próximo número

Header Image Credits: Jan Kolar

SOBRE Er ESpilver
Pepe "Espilver" López es un entusiasta del mundo audiovisual. Experto en multimedia es la persona que debes buscas para conocer los secretos más oscuros de los sistemas de audio y video. Pepe disfruta visionando películas de serie Z con su amigo "Er Claketas" cuando se jarta de estudiar algorithmos de compresión de video.