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:
- Ver la cámara en tiempo real, con fecha y hora
- Descargar una foto
- Guardar la foto en el servidor
- 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
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
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>
<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
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.
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?