python

Grabar vídeo de cámara con Python, Flask y OpenCV – Cámara de vigilancia

En este post te mostraré otro avance en cuanto a la lectura de una cámara web usando Python. Con el código que expongo aquí serás capaz de:

Trabajando con cámara en Python – Tomar fotos y grabar vídeos usando Flask y OpenCV
  1. Ver la cámara en tiempo real, con fecha y hora
  2. Descargar una foto
  3. Guardar la foto en el servidor
  4. Grabar vídeos y guardarlos en el servidor

Básicamente se podrá hacer todo lo que se haría en una cámara de vigilancia, pero ahora usando Python y una cámara conectada al dispositivo. Obviamente se le pueden agregar más cosas, mejorar el proyecto, etcétera.

El punto es que con esto podemos usar Python para acceder a la cámara web, verla, tomar fotos y grabar vídeos.

Mejoras en cuanto a la versión anterior

De hecho esto no es una nueva versión, esto es más como una mejora de la anterior. En el post anterior te enseñé a stremear la cámara y a tomar fotos.

Ahora fui un paso más allá y añadí la opción para grabar vídeos además de poner la fecha y hora en el vídeo.

Sin embargo, no voy a explicar todo desde cero. Así que te recomiendo ampliamente leer el post anterior y después volver a este para ver las mejoras.

No te preocupes, igual te mostraré el código completo. Pero es mejor que lo entiendas en caso de que necesites adaptarlo o mejorarlo.

Agregando fecha y hora

Debido a que las cámaras de vigilancia graban la fecha y hora quise implementar ese comportamiento. Así que modifiqué el frame antes de que fuera devuelto y le coloqué el texto usando una función:

# Marca de agua
# https://docs.opencv.org/master/d6/d6e/group__imgproc__draw.html#ga5126f47f883d730f633d74f07456c576
UBICACION_FECHA_HORA = (0, 15)
FUENTE_FECHA_Y_HORA = cv2.FONT_HERSHEY_PLAIN
ESCALA_FUENTE = 1
COLOR_FECHA_HORA = (255, 255, 255)
GROSOR_TEXTO = 1
TIPO_LINEA_TEXTO = cv2.LINE_AA

def agregar_fecha_hora_frame(frame):
    cv2.putText(frame, utiles.fecha_y_hora(), UBICACION_FECHA_HORA, FUENTE_FECHA_Y_HORA,
                ESCALA_FUENTE, COLOR_FECHA_HORA, GROSOR_TEXTO, TIPO_LINEA_TEXTO)

De este modo se agrega un texto al frame. Y ya sea que se tome la foto, se grabe vídeo o simplemente se visualice la cámara, el frame tendrá la fecha y hora actual.

Recuerda que, si bien no es un trabajo muy pesado hacer esto, puede que en dispositivos con pocos recursos esto agregue lag o pérdida de frames.

Como puedes ver usamos el método putText propio de OpenCV y le enviamos los argumentos, mismos que solo son constantes. Lo he hecho de este modo para que sea fácil cambiar los parámetros.

Grabando vídeo

Vídeo grabado con Python y OpenCV

Vamos a la parte más interesante del post: cómo grabar vídeos con Python usando la cámara web o una cámara conectada por USB.

En este caso simplemente tenemos que crear un archivo de vídeo en donde se van a guardar todos los frames y, en cada lectura, ir escribiendo al archivo.

La cosa se complica un poco cuando tenemos que stremear esos frames y crear vídeos cuando el usuario lo requiera. Pero con un poco de código se logró.

Por cierto, los vídeos se guardan en formato AVI en el directorio donde se ejecuta el script (tienen la fecha y hora como nombre). Más tarde se podría agregar una opción para ver el listado de vídeos o descargarlos.

El formato AVI fue elegido porque es un formato compatible con Windows; y esto cambia dependiendo del sistema operativo.

Configuraciones de grabación de vídeo

En fin, veamos las configuraciones del vídeo:

"""
    Configuraciones de vídeo
"""
FRAMES_VIDEO = 20.0
RESOLUCION_VIDEO = (640, 480)
# El código de 4 dígitos. En windows me parece que se soporta el XVID
fourcc = cv2.VideoWriter_fourcc(*'XVID')
archivo_video = None
grabando = False

Solo recuerda que entre mayor resolución, mayor es el peso. Y que entre más Frames, más peso, pero más fluidez. Debes buscar el equilibrio perfecto entre calidad del vídeo, almacenamiento disponible y todo eso (sobre todo si realmente vas a usar esto como cámara de vigilancia).

Iniciar grabación

Grabando vídeo de cámara usando Python

He expuesto esto en una ruta de Flask. Básicamente crea un nuevo archivo de vídeo y establece una bandera para que la otra función que devuelve los frames sepa que debe escribir el frame antes de regresarlo.

@app.route("/comenzar_grabacion")
def comenzar_grabacion():
    global grabando
    global archivo_video
    if grabando and archivo_video:
        return jsonify(False)
    nombre = utiles.fecha_y_hora_para_nombre_archivo() + ".avi"
    archivo_video = cv2.VideoWriter(
        nombre, fourcc, FRAMES_VIDEO, RESOLUCION_VIDEO)
    grabando = True
    return jsonify(True)

La escritura del frame en el archivo está en la línea 8. Fíjate en que estamos usando las configuraciones previamente establecidas.

Por cierto, el nombre del archivo está en la línea 7. También es importante que notes que estamos usando variables globales ya que todas las funciones deben compartir el estado de la grabación.

Escribiendo frame

Anteriormente te dije que la función que lee un frame de al cámara saber si debe guardarlo en el vídeo gracias a una bandera. El código queda así:

def obtener_frame_camara():
    ok, frame = camara.read()
    if not ok:
        return False, None
    agregar_fecha_hora_frame(frame)
    # Escribir en el vídeo en caso de que se esté grabando
    if grabando and archivo_video is not None:
        archivo_video.write(frame)
    # Codificar la imagen como JPG
    _, bufer = cv2.imencode(".jpg", frame)
    imagen = bufer.tobytes()

    return True, imagen

La magia está ocurriendo en la línea 6. Primero el frame se transforma agregándole la fecha y hora. Más tarde, se comprueba si el archivo de vídeo está definido y si se está grabando. En ese caso escribimos el frame, después lo codificamos como jpg y finalmente lo devolvemos.

Detener grabación

Para dejar de grabar también existe una ruta. En este caso simplemente libera el archivo de grabación para escribirlo, y establece la bandera en False.

@app.route("/detener_grabacion")
def detener_grabacion():
    global grabando
    global archivo_video
    if not grabando or not archivo_video:
        return jsonify(False)
    grabando = False
    archivo_video.release()
    archivo_video = None
    return jsonify(True)

Lado del cliente

El lado del cliente también juega una parte importante, pues aquí es en donde se empieza o detiene la grabación. Por ello es que además existe una ruta que dice si el vídeo se está grabando o no:

@app.route("/estado_grabacion")
def estado_grabacion():
    return jsonify(grabando)


Gracias a esto podemos conectarnos una vez al “panel de control” de la cámara, comenzar la grabación y después salir, ya que el estado del servidor se refleja para todos los clientes, y toda la grabación se realiza del lado del servidor.

Así que si la grabación ya ha sido iniciada por otro cliente y alguien nuevo se conecta, se le mostrará el botón para detener la grabación.

El código HTML queda así:

<!DOCTYPE html>
<html lang="es">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cámara de vigilancia con Python, Flask y OpenCV - By Parzibyte</title>
    <link rel="stylesheet" href="https://unpkg.com/bulma@0.9.1/css/bulma.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css"
        integrity="sha512-HK5fgLBL+xu6dm/Ii3z4xhlSUyZgTT9tuc/hSrtw6uzJOvgRr2a9jyxxT1ely+B+xFAmJKVSTbpM/CuL7qxO8w=="
        crossorigin="anonymous" />
</head>

<body>

    <nav class="navbar is-warning" role="navigation" aria-label="main navigation">
        <div class="navbar-brand">
            <a class="navbar-item" href="https://parzibyte.me/blog">
                <img alt=""
                    src="https://raw.githubusercontent.com/parzibyte/ejemplo-mern/master/src/img/parzibyte_logo.png"
                    style="max-height: 80px" />
            </a>
            <button class="navbar-burger is-warning button" aria-label="menu" aria-expanded="false"
                data-target="navbarBasicExample">
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
            </button>
        </div>
        <div class="navbar-menu">
            <div class="navbar-start">
                <a class="navbar-item" href="./">Vigilar cámara</a>
            </div>
            <div class="navbar-end">
                <div class="navbar-item">
                    <div class="buttons">
                        <a target="_blank" rel="noreferrer" href="https://parzibyte.me/l/fW8zGd"
                            class="button is-primary">
                            <strong>Soporte y ayuda</strong>
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </nav>
    <script type="text/javascript">
        document.addEventListener("DOMContentLoaded", () => {
            const boton = document.querySelector(".navbar-burger");
            const menu = document.querySelector(".navbar-menu");
            boton.onclick = () => {
                menu.classList.toggle("is-active");
                boton.classList.toggle("is-active");
            };
        });


    </script>
    <section class="section">
        <div class="columns">
            <div class="column has-text-centered">
                <figure class="image has-text-centered">

                    <img class="is-inline-block" src="./streaming_camara" style="width: auto">
                </figure>
            </div>
        </div>
        <div class="columns">
            <div class="column has-text-centered">
                <a href="./tomar_foto_descargar" target="_blank" class="button is-success mb-2">
                    <i class="fa fa-camera fa-xs"></i>&nbsp;
                    <i class="fa fa-download"></i>
                </a>
                <button id="btnTomarFotoServidor" class="button is-info mb-2">
                    <i class="fa fa-camera"></i>
                </button>
                <button id="btnIniciarGrabacion" class="button is-danger mb-2">
                    <i class="fa fa-circle"></i>
                </button>
                <button id="btnDetenerGrabacion" class="button is-primary mb-2">
                    <i class="fa fa-stop"></i>
                </button>
                <div class="notification is-primary mt-2" id="estado">Aquí aparecerá la información</div>
                <br>
                <a class="button is-danger my-2" href="//parzibyte.me/blog">Ver código fuente</a>
            </div>
        </div>
    </section>
</body>

</html>

He agregado los botones para grabar y detener la grabación de vídeo. Ahora, cada clic de esos botones tiene su listener:

const $btnTomarFotoServidor = document.querySelector("#btnTomarFotoServidor"),
    $btnIniciarGrabacion = document.querySelector("#btnIniciarGrabacion"),
    $btnDetenerGrabacion = document.querySelector("#btnDetenerGrabacion"),
    $estado = document.querySelector("#estado");

const obtenerEstadoDeGrabacionYRefrescarVista = async () => {
    const respuestaRaw = await fetch("./estado_grabacion");
    const grabando = await respuestaRaw.json();
    if (grabando) {
        $btnIniciarGrabacion.style.display = "none";
        $btnDetenerGrabacion.style.display = "inline";
    } else {
        $btnIniciarGrabacion.style.display = "inline";
        $btnDetenerGrabacion.style.display = "none";
    }
};
obtenerEstadoDeGrabacionYRefrescarVista();
/*
    En el clic del botón hacemos una petición a ./tomar_foto_guardar 
*/$btnTomarFotoServidor.onclick = async () => {
    $estado.textContent = "Tomando foto...";
    const respuestaRaw = await fetch("./tomar_foto_guardar");
    const respuesta = await respuestaRaw.json();
    let mensaje = "";
    if (respuesta.ok) {
        mensaje = `Foto guardada como ${respuesta.nombre_foto}`;
    } else {
        mensaje = `Error tomando foto`;
    }
    $estado.textContent = mensaje;
};
/*
    Iniciar grabación
*/$btnIniciarGrabacion.onclick = async () => {
    $estado.textContent = "Iniciando grabación...";
    const respuestaRaw = await fetch("./comenzar_grabacion");
    const respuesta = await respuestaRaw.json();
    if (respuesta) {
        $estado.textContent = "Grabación iniciada";
        obtenerEstadoDeGrabacionYRefrescarVista();
    } else {
        $estado.textContent = "Error iniciando grabación";
        obtenerEstadoDeGrabacionYRefrescarVista();
    }
};

$btnDetenerGrabacion.onclick = async () => {
    $estado.textContent = "Deteniendo grabación...";
    const respuestaRaw = await fetch("./detener_grabacion");
    const respuesta = await respuestaRaw.json();
    if (respuesta) {
        $estado.textContent = "Grabación detenida";
        obtenerEstadoDeGrabacionYRefrescarVista();
    } else {
        $estado.textContent = "Error deteniendo grabación";
        obtenerEstadoDeGrabacionYRefrescarVista();
    }
};

Básicamente invocan a las rutas expuestas con Flask en el fondo, dando una experiencia de tiempo real. Por cierto, en la línea 6 estoy definiendo una función que muestra u oculta los botones de iniciar y detener grabación dependiendo del estado.

Problemas conocidos

Existe un problema que he experimentado cuando hay 2 clientes visualizando la cámara en tiempo real. Si uno de ellos inicia la grabación y luego la detiene, al detenerla se corta la transmisión de vídeo para todos, y se genera una excepción.

Si Flask está en modo de producción simplemente hay que refrescar la página y todo volverá a la normalidad. En caso de que se quieran usar varios clientes y grabar vídeos, tal vez se debería desactivar la visualización en tiempo real.

Poniendo todo junto

Trabajando con cámara en Python – Tomar fotos y grabar vídeos usando Flask y OpenCV

El código completo lo dejaré en GitHub. Recuerda que debes contar con Python y PIP. Después, instala las dependencias:

pip install opencv-python

pip install flask

Y finalmente ejecutar la app con:

python app.py

Ahora visita localhost:5000 y todo debería funcionar. Recuerda que también puedes acceder desde otro dispositivo únicamente colocando la IP del servidor y el puerto.

Por aquí te dejo más tutoriales sobre Python.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

  • Hola, cambie la camara por una camara IP realizada con ESP32 y OV267 y la pantalla se congela cuando fucniona la aplicación ¿cómo puedo solucionarlo?

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

3 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

3 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

3 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

3 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

3 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

4 días hace

Esta web usa cookies.