Thursday, November 1, 2018

Resultados y rendimiento


Los resultados obtenidos y el funcionamiento del sistema van a depender de muchos factores, pero los dos más significativos van a ser la calidad del video y los fps del stream, esto es algo que en la camara hecha con la RPi tenemos control sobre estos parámetros y podemos modificar, en la camara IP no tenemos manera de cambiar estos parámetros. Otros factores como la cantidad de movimiento que hay en el entorno o la complejidad del lugar al que apunta la camara también va a repercutir en el rendimiento final. Suponemos que las cámaras van a estar en un entorno con poco movimiento (una casa) y no en un lugar público donde el algoritmo de detección se estaría ejecutando constatemente y ralentizaría el rendimiento.

Lo primero será analizar con que parámetros vamos a trabajar en la cámara IP, pódemos mirarlo con este comando.

ffprobe rtsp://192.168.1.104:554/onvif1

Input #0, rtsp, from 'rtsp://192.168.1.104:554/onvif1':
  Metadata:
    title           : H.264 Video, RtspServer_0.0.0.2
  Duration: N/A, start: 0.000000, bitrate: N/A
    Stream #0:0: Video: h264 (Baseline), yuv420p(progressive), 1280x720, 5 fps, 5 tbr, 90k tbn, 180k tbc
    Stream #0:1: Audio: pcm_alaw, 8000 Hz, 1 channels, s16, 64 kb/s


Podemos ver que se trata del codec H.264 y un vídeo hd de 720p y 5 fps.

Nuestra cámara hecha con la RPi podemos ajustar los parámetros con el comando raspivid que analizamos en el post  de "construcción de una cámara IP con RPi".

Ahora como podemos ver el rendimento real de nuestro programa, pues con la función FPS de la librería imutils.video. Esta librería usa un thread a parte para llevar la cuenta de frames, solo hace falta iniciar el thread con start antes del bucle y acabarlo con stop despues del bucle y cambiar el bucle infinito por un número concreto de frames (200 en todas las pruebas)

fps = FPS().start()

while fps._numFrames < 200
   ...
   fps.update()

fps.stop()
print("[INFO] elasped time: {:.2f}".format(fps.elapsed()))
print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))


Se obtuvo la siguiente baterías de prueba, con los siguientes parametros en las cámaras. Los de la cámara IP son los arriba descritos y para la cámara con RPi un vídeo H.264 de 600x400, los fps tras realizar varias pruebas se dejaron en 5, con 10 o más ya empezaba a encolar frames, lo cual tampoco es un problema pero de esta forma tenemos vídeo en tiempo real.

1º Prueba, procesado de vídeo mostrandolo por pantalla y sin mostralor (imshow)

Mostrando las imágenes por pantalla

[INFO] elasped time: 34.95
[INFO] approx. FPS: 5.72

Solo procesado

[INFO] elasped time: 34.70
[INFO] approx. FPS: 5.76

Sin demasiada diferencia, la idea de esto es que en vez de mostrar las imágenes podemos guardar los frames para construir otro stream rtsp en un servidor web, lo cual nos permite tener un historico de grabaciones que poder visualizar, sería interesante tener esto en cuenta para un trabajo futuro.

2º Prueba, una cámara con detección de movimiento.

La idea es generar movimiento y ver como lo procesa y el resultado obtenido.

[INFO] elasped time: 20.04
[INFO] approx. FPS: 9.98

Un momento, parece que el rendimiento es mejor que cuando solo se procesa el vídeo, ¿como es posible? Eso es debido a que el momento en el que hay movimiento y empieza con la lógica de detección necesita de un proceso computacional a mayores y empieza a encolar frames, estos frames que están en la cola no cuentan en el computo de los fps y por eso sale mejor rendimiento, aunque no es el caso, simplemente lo que hace encolar frames es aumentar la latencia y el retardo del vídeo.

¿Os acordais de la anterior función que comentaba en el primer post que hacía el procesado en el thread principal? Bueno pues estos son lo resultados solo para procesar el vídeo, es decir sin tener movimiento que detectar.

[INFO] elasped time: 61.72
[INFO] approx. FPS: 3.24

Y estos con deteccion de movimento

[INFO] elasped time: 90.41
[INFO] approx. FPS: 2.21

Parece una buena idea usar threads distintos.

Ahora probemos con ambas cámaras funcionando simultanemanete, basta con lanzar el script en segundo plano dos veces con la direcciones del stream de cada cámara.

Ambas cámaras funcionando (sin detección de movimiento)

Cámara 1
[INFO] elasped time: 34.50
[INFO] approx. FPS: 5.80

Cámara 2
[INFO] elasped time: 33.84
[INFO] approx. FPS: 5.91

En el siguiente post veremos como crear un bot en telegram y la lógica que le tenemos que añadir a nuestro programa para que nos lleguen notificaciones al móvil cuando detecta movimiento.

Friday, October 26, 2018

Detalles del código, uso de threads

Dentro de la librerías de opencv y imutils para python tenemos un gran surtido de funciones para leer el vídeo de un stream, probablemente la más utilizada para estas cosas está dentro de la librería cv2.VideoCapture. La función .read es la que se usa para este cometido, pero tiene un problema, es una función propiamente bloqueante, ya que no solo lee el stream, si no que lo tiene que decodificar, no importa en el formato en el que esté, todo este proceso lo hace en el thread principal, con lo cual mientras no acabe de leer y decodificar no puede hacer otra acción.

Esto no es un problema si disponemos de un hardware suficientemente potente, pero en una RPi necesitamos algo más eficiente si queremos tener resultados aceptables. La solución es usar threads, un thread que se encarge de la lectura y procesado del vídeo y el principal que se encarga de hacer la detección de movimiento. La diferencia entre hacerlo así o hacer ambas tareas en el mismo thread es significativa, (se verá la diferencia en el siguiente post). Existe una librería en imutils que hace la lectura del vídeo en otro thread, FileVideoStream.

Veamos un ejemplo de código.

# start the file video stream thread and allow the buffer to
# start to fill
print("[INFO] starting video file thread...")
fvs = FileVideoStream(args["video"]).start()
time.sleep(1.0) 


El código es bastante sencillo, se pasa la dirección del video como argumento y se inicia el thread con start(), se le da un segundo para que llene el buffer con frames del stream, finalmente leemos en el bucle principal.

while (1):
    frame = fvs.read()


En este caso concreto se utiliza también una cola a modo de buffer para frames, esto lo utiliza internamente la librería FileVideoStream, se puede configurar el tamaño de la misma, por defecto está a 200 frames. Se puede ver el uso de la misma con Q.qsize().

Lo siguiente es toda la lógica de detección del movimiento, irá en el thread principal dentro del bucle.

# grab the frame from the threaded video file stream, resize
# it, and convert it to grayscale (while still retaining 3
# channels)
frame = fvs.read()
frame = imutils.resize(frame, width=450)
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
frame = np.dstack([frame, frame, frame]) 


Ahora toca aplicar la función de la que hablamos en el anterior post, definimos un fondo fuera del bucle.

bg = cv2.bgsegm.createBackgroundSubtractorMOG()  

Dentro del bucle.

motion = bg.apply(frame, learningRate=0.005) 
kernel = np.ones((3, 3), np.uint8) 
motion = cv2.morphologyEx(motion, cv2.MORPH_CLOSE, kernel, iterations=1) 
motion = cv2.morphologyEx(motion, cv2.MORPH_OPEN, kernel, iterations=1) 
motion = cv2.dilate(motion,kernel,iterations = 1) 

Extrae una imagen en blanco y negro sin el fondo.
Crea una matriz de 3x3.
Las líneas siguientes eliminan ruído de la imagen con bloques de 3x3.

El siguiente paso es encontrar los contornos con cambios (movimiento) en el frame y pintar un rectangulo para identificarlos.

contours = cv2.findContours(motion, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnts = contours[0] if imutils.is_cv2() else contours[1]

for c in cnts:
   # if the contour is too small, ignore it
   if cv2.contourArea(c) < 5000:
      continue
   # compute the bounding box for the contour, draw it on the frame,
   # and update the text
   (x, y, w, h) = cv2.boundingRect(c)
   cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)


Finalmente mostramos el frame con los recuadros que identifican el movimiento en el vídeo

cv2.imshow("Frame", frame)


Este es el funcionamiento básico del algoritmo, podeis encontrar el código completo en github. En el siguiente post analizaremos los resultados obtenidos, principalmente analizaremos como afecta nuestro algoritmo al rendimiento del procesado del vídeo y cual es el fps óptimo para tener el vídeo en tiempo real.

Saturday, October 20, 2018

Construcción de una cámara IP con una RPi y herramientas necesarias para el servidor en RPI3

Un módulo de cámara para una RPi es un buen substituto de una cámara IP, en mi caso lo acompañe de un par de sensores de infrarrojos para que se pueda utilizar por la noche, en mi caso utilice una RPi zero por sus dimensiones reducidas así como su precio. Pero cualquier modelo de RPi sirve para este caso incluso la primera versión es más que capaz. Las últimas versiones de la RPi Zero bienen con un controlador WiFi integrado, solo es necesario instalar un cliente para conectarlo a la WiFi, para lo cual recomiendo wicd-ncurses o alguno similar que se pueda utilizar por consola sin necesidad de entorno gráfico.

Una vez conectado es hora de generar el stream y para eso usamos la herramienta raspivid que nos permite capturar el video del módulo de la cámara y concatenamos la salida con un comando del vlc para generar el stream por rtsp.

raspivid -o - -t 0 -n -w 600 -h 400 -fps 12 | cvlc -vvv stream:///dev/stdin --sout '#rtp{sdp=rtsp://:9554/low}' :demux=h264


Podemos ajustar a nuestras necesidades el tamaño del vídeo y los fps del stream, una vez lanzado el comando ya podemos acceder al stream por rtsp como si se tratara de una cámara IP.

Ahora toca la parte del servidor que va en la RPi 3, para ello hay que compilar OpenCV con soporte para ffmpeg, será la herramienta que se emplee para leer y decodifcar el video, y con soporte para python.

Compilar software es una tarea de por si bastante costosa en términos computacionales, más aun para una RPi donde cualquier cosa medianamente compleja puede tardar muchas horas en compilar, por eso creo que tener un entorno con las herramientas necesarias y poder empezar a programar y probar cosas al instante es imprescindible para no estancarse en el desarrollo. Por eso utilicé docker en mi PC, para los que no esteis familiarizados con el término, se trata de un sistema de contenedores donde puedes tener herramientas concretas generadas a traves de una imagen base, algo así como una máquina virtual pero a velocidad nativa. 

No voy a entrar en más detalles sobre como funciona docker, solo decir que como base para la compilación de OpenCV cree un contenedor a partir de este Dockerfile. Si lo analizais por encima veis que son una serie de comandos a partir de una imagen base de ubuntu donde instala las dependencias necesarias y compila el codigo fuente de OpenCV. El mismo proceso se puede llevar a cabo cuando se quiera meter en la RPi teniendo en cuenta que la base es un Raspbian y no un ubuntu y es posible que algunos paquetes no se llamen igual, Pero esta imagen nos permite trabajar directamente con las herramientas que necesitamos y empezar a desarrollar directamente.

Simplemente comentar que para que funcione la compilacion en la RPi solo me funcionó con la version 3.3.0 de OpenCV y a mayores necesité cambiar un aspecto del código fuente. Aparentemente cuando OpenCV abre una conexión para comunicarse con el stream rtsp la abre por tcp, nuestros streams tanto el de la camara IP como el de la RPi abren una conexión udp, en teoría se puede cambiar con la siguiente línea de código en python.

os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;udp" 


Pero no parecía funcionarme así que opté por cambiarlo en la librería del códgo fuente. El siguiente comando reemplaza en el código "tcp" por "udp" y de esta forma ya se puede leer nuestros streams desde el programa.

sed "s/\btcp\b/udp/g" -i /tmp/opencv/modules/videoio/src/cap_ffmpeg_impl.hpp


Una vez compilado podemos ejecutar la siguiente línea en una consola de python para ver si hicimos correcta la compilación de OpenCV

import cv2; print(cv2.getBuildInformation())

Video I/O:
DC1394 1.x: NO
DC1394 2.x: NO
FFMPEG: YES
avcodec: YES (ver 57.24.102)
avformat: YES (ver 57.25.100)
avutil: YES (ver 55.17.103)
swscale: YES (ver 4.0.100)
avresample: NO
GStreamer: NO
OpenNI: NO
OpenNI PrimeSensor Modules: NO
OpenNI2: NO
PvAPI: NO
GigEVisionSDK: NO
Aravis SDK: NO
UniCap: NO
UniCap ucil: NO
V4L/V4L2: NO/YES
XIMEA: NO
Xine: NO
gPhoto2: NO

La parte que nos interesa es la que pone FFMPEG: YES

Aunque no es imprescindible y en gran parte dependerá del uso que se le vaya a dar, opté por que la instalación del sistema base estuviera en una partición en usb. En condiciones normales no es que genere muchos accesos de escritura / lectura (se puede comprobar con iotop) Pero si que es cierto que la parte de detección de movimiento puede generar alguna carga. Para evitar que nuestra tarjeta sd se muera podemos editar en la partición de boot el fichero cmdline.txt para que arranque la partición del usb (blkid para conocer su UUID)

Esto no va afectar al rendimiento, en teoría no importa mucho el tipo de tarjeta sd o el usb que se use, siempre va haber una limitación probocada por el bus de entrada de la RPi y nunca se va alcanzar las tasas máximas que ofrece el dispostivo.

En el siguiente post veremos en detalle como funciona el código de detección de movimiento.

Wednesday, October 17, 2018

Sistema de videovigilancia con deteccion de movimiento en una RPi3


Existen cantidad de proyectos que se pueden llevar a cabo en una Raspberry solo es cuestión de imaginación y tiempo para dedicarle, pero en mucho de ellos nos encontramos con la limitación del hardware, admitámolos por el precio de una RPi3 que más podemos pedir, sin embargo analizando un poco el problema podemos dar con una solución al problema que supone la limitación del hardware como iremos viendo a lo largo de los posts.

Hace ya un tiempo que había comprado una cámara IP de oferta, una Sricam, es una marca china con lo que tampoco esperaba que tuviera una API accesible o un montón de herramientas para manejarla. Venía con lo justo, una app para android donde podías ver la cámara, rotarla usar el micro y poco más, pero tenía algo más interesante, usaba el protocolo onvif lo que por lo menos significaba que tenía un stream de video al que podía acceder via IP.

Esto me daba la posibilidad de reproducir el vídeo desde cualquier equipo de casa que estuviera conectado a la red, pero ¿y si le añadiera algo más para hacerlo más completo?, por ejemplo, detección de movimiento estaría bien. Existe muchas implementaciones en distintos lenguajes y numerosos algoritmos distintos para hacer uso de la detección. El algoritmo que se ha usado en este caso es el que ofrece la funcion de OpenCV cv2.BackgroundSubtractorMOG aunque existen más son similares y se basan en crear una separación del fondo y lo que sería el primer plano, pero ¿porque esta separación de dos planos? El fondo sería algo constante a lo largo de los frames y cualquier variación que se observe a lo largo de las iteraciones pertenece al primer plano y se considera movimiento.

Este método de detección es el más simple y se ve afectado por cambios en la iluminación, reflejos y sombras, básicamente cualquier cosa que varie la imagen de fondo se considera movimiento, pero también es menos costosa computacionalmente y dado que va a ir en una Raspberry sería lo ideal, la idea final es usarlo en una vivenda donde no habrá tanto movimiento como en un recinto público con lo que las veces que va hacer uso de este algoritmo van a ser reducidas.

Más adelante explicará este algoritmo en detalle. Volviendo a la parte del vídeo, tenemos una cámara IP que nos ofrece un flujo de vídeo por rtsp de la siguiente forma 'rtsp://x.x.x.x:554/onvif1', pero ¿y si no tenemos camara ip y queremos usar una RPi con un módulo de cámara? Pues lo suyo sería hacer un flujo rtsp de forma similar a como lo hace la cámara, de esta forma si algún día queremos meter una cámara IP en nuestro sistema no hay que adaptar el código para la detección.

En el siguiente post veremos como hacer una cámara IP a partir de una RPi y las herramientas que necesitamos en nuestro servidor para que funcione la detección de movimiento.